fastapi-injected 0.3.0__tar.gz → 0.3.1__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 (65) hide show
  1. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/PKG-INFO +14 -2
  2. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/README.md +13 -1
  3. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/__init__.py +3 -0
  4. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_bind.py +3 -2
  5. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_given.py +1 -2
  6. fastapi_injected-0.3.1/fastapi_injected/_params.py +19 -0
  7. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/deps.py +2 -1
  8. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/types.py +2 -1
  9. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/pyproject.toml +1 -1
  10. fastapi_injected-0.3.1/tests/_typing/params.py +30 -0
  11. fastapi_injected-0.3.1/tests/test_params.py +78 -0
  12. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/uv.lock +4 -4
  13. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.github/dependabot.yml +0 -0
  14. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.github/workflows/automerge.yml +0 -0
  15. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.github/workflows/lint.yml +0 -0
  16. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.github/workflows/publish.yml +0 -0
  17. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.github/workflows/test.yml +0 -0
  18. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.gitignore +0 -0
  19. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/.pre-commit-config.yaml +0 -0
  20. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/LICENSE +0 -0
  21. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_cache.py +0 -0
  22. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_dataclass.py +0 -0
  23. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_deps_tp.py +0 -0
  24. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_fastapi_lifecycle.py +0 -0
  25. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_injected.py +0 -0
  26. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_overrides.py +0 -0
  27. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/_rlock.py +0 -0
  28. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/inject.py +0 -0
  29. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/overrides.py +0 -0
  30. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/resolve.py +0 -0
  31. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/scope.py +0 -0
  32. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/fastapi_injected/sign.py +0 -0
  33. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/__init__.py +0 -0
  34. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/__init__.py +0 -0
  35. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/annotations.py +0 -0
  36. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/arg.py +0 -0
  37. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/bind.py +0 -0
  38. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/dataclass.py +0 -0
  39. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/deps.py +0 -0
  40. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/errors.py +0 -0
  41. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/given.py +0 -0
  42. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/inject.py +0 -0
  43. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/injected.py +0 -0
  44. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/integration.py +0 -0
  45. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/markers.py +0 -0
  46. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/overrides.py +0 -0
  47. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/resolve.py +0 -0
  48. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/_typing/scope.py +0 -0
  49. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/deps.py +0 -0
  50. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/ext/__init__.py +0 -0
  51. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/ext/test_pydantic_ai.py +0 -0
  52. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_bind.py +0 -0
  53. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_cache.py +0 -0
  54. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_concurrency.py +0 -0
  55. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_dataclass.py +0 -0
  56. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_dependant_cache.py +0 -0
  57. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_errors.py +0 -0
  58. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_fastapi.py +0 -0
  59. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_given.py +0 -0
  60. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_inject.py +0 -0
  61. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_injected.py +0 -0
  62. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_overrides.py +0 -0
  63. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_resolve.py +0 -0
  64. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_rlock.py +0 -0
  65. {fastapi_injected-0.3.0 → fastapi_injected-0.3.1}/tests/test_scope.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fastapi-injected
3
- Version: 0.3.0
3
+ Version: 0.3.1
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>
@@ -253,6 +253,18 @@ class Config(MakeDataclass, frozen=True, kw_only=True):
253
253
  retries: int = 3
254
254
  ```
255
255
 
256
+ The same is true of what holds a dependency. FastAPI's `Depends` and `Security` are frozen dataclasses, so they hash by field — and `Security(get_user, scopes=["items:read"])`, the documented spelling, holds a list, which makes the whole annotation unhashable and fails as a bare `TypeError` from `typing`. `fastapi_injected` ships both as `MakeDataclass` subclasses, which is the same class in every way that matters to FastAPI, and one that can be cached:
257
+
258
+ ```python
259
+ from fastapi_injected import Security, resolve
260
+
261
+ type CurrentUser = Annotated[User, Security(get_user, scopes=["items:read"])]
262
+
263
+ await resolve(CurrentUser)
264
+ ```
265
+
266
+ Scopes are kept as a tuple, so the same scopes written as another list stay one dependency. Everything the package builds internally is held this way too, so a dependency object that cannot be hashed by fields no longer decides whether the annotation around it can be.
267
+
256
268
  ### When resolution fails
257
269
 
258
270
  A dependency can fail to resolve for the same reasons it would in a route — a missing header, a query parameter that does not validate. `DependencyResolutionError` carries those errors in the shape pydantic produced them, and turns into the response FastAPI would have returned:
@@ -357,7 +369,7 @@ Everything the package supports is importable from `fastapi_injected` itself, an
357
369
 
358
370
  | | |
359
371
  | --- | --- |
360
- | Markers | `Dep`, `DepFactory`, `DepOf`, `Arg`, `Given`, `Injected` |
372
+ | Markers | `Dep`, `DepFactory`, `DepOf`, `Arg`, `Given`, `Injected`, `Depends`, `Security` |
361
373
  | Resolving | `inject`, `resolve`, `bind_deps`, `signature_with_deps`, `remap_dep_args`, `clear_dependant_cache` |
362
374
  | Scopes and overrides | `InjectScope`, `push_inject_scope`, `inside_inject_scope`, `push_overrides`, `Overrides`, `OverridesProvider`, `ValueOverride`, `FactoryOverride` |
363
375
  | Building on top | `MakeDataclass`, `MakeInjected`, `HasDependsHook`, `ArgMarker`, `is_arg`, `is_dep`, `unwrap_dep_tp`, `unwrap_dep_dependency` |
@@ -232,6 +232,18 @@ class Config(MakeDataclass, frozen=True, kw_only=True):
232
232
  retries: int = 3
233
233
  ```
234
234
 
235
+ The same is true of what holds a dependency. FastAPI's `Depends` and `Security` are frozen dataclasses, so they hash by field — and `Security(get_user, scopes=["items:read"])`, the documented spelling, holds a list, which makes the whole annotation unhashable and fails as a bare `TypeError` from `typing`. `fastapi_injected` ships both as `MakeDataclass` subclasses, which is the same class in every way that matters to FastAPI, and one that can be cached:
236
+
237
+ ```python
238
+ from fastapi_injected import Security, resolve
239
+
240
+ type CurrentUser = Annotated[User, Security(get_user, scopes=["items:read"])]
241
+
242
+ await resolve(CurrentUser)
243
+ ```
244
+
245
+ Scopes are kept as a tuple, so the same scopes written as another list stay one dependency. Everything the package builds internally is held this way too, so a dependency object that cannot be hashed by fields no longer decides whether the annotation around it can be.
246
+
235
247
  ### When resolution fails
236
248
 
237
249
  A dependency can fail to resolve for the same reasons it would in a route — a missing header, a query parameter that does not validate. `DependencyResolutionError` carries those errors in the shape pydantic produced them, and turns into the response FastAPI would have returned:
@@ -336,7 +348,7 @@ Everything the package supports is importable from `fastapi_injected` itself, an
336
348
 
337
349
  | | |
338
350
  | --- | --- |
339
- | Markers | `Dep`, `DepFactory`, `DepOf`, `Arg`, `Given`, `Injected` |
351
+ | Markers | `Dep`, `DepFactory`, `DepOf`, `Arg`, `Given`, `Injected`, `Depends`, `Security` |
340
352
  | Resolving | `inject`, `resolve`, `bind_deps`, `signature_with_deps`, `remap_dep_args`, `clear_dependant_cache` |
341
353
  | Scopes and overrides | `InjectScope`, `push_inject_scope`, `inside_inject_scope`, `push_overrides`, `Overrides`, `OverridesProvider`, `ValueOverride`, `FactoryOverride` |
342
354
  | Building on top | `MakeDataclass`, `MakeInjected`, `HasDependsHook`, `ArgMarker`, `is_arg`, `is_dep`, `unwrap_dep_tp`, `unwrap_dep_dependency` |
@@ -4,6 +4,7 @@ from ._deps_tp import is_arg, is_dep, unwrap_dep_dependency, unwrap_dep_tp
4
4
  from ._fastapi_lifecycle import MissingDependencyCacheError, add_injected_scope, init_inject_scope
5
5
  from ._given import Given
6
6
  from ._injected import MakeInjected
7
+ from ._params import Depends, Security
7
8
  from .deps import (
8
9
  DependencyResolutionError,
9
10
  HasDependsHook,
@@ -24,6 +25,7 @@ __all__ = [
24
25
  "DepFactory",
25
26
  "DepOf",
26
27
  "DependencyResolutionError",
28
+ "Depends",
27
29
  "FactoryOverride",
28
30
  "Given",
29
31
  "HasDependsHook",
@@ -36,6 +38,7 @@ __all__ = [
36
38
  "NotADependencyError",
37
39
  "Overrides",
38
40
  "OverridesProvider",
41
+ "Security",
39
42
  "UnboundDepArgsError",
40
43
  "UnboundScopeError",
41
44
  "ValueOverride",
@@ -3,10 +3,11 @@ from collections.abc import Callable, Sequence
3
3
  from functools import wraps
4
4
  from typing import Annotated, Any, Concatenate, overload
5
5
 
6
+ from fastapi import params
6
7
  from fastapi.dependencies.utils import get_typed_signature
7
- from fastapi.params import Depends
8
8
 
9
9
  from ._deps_tp import is_dep
10
+ from ._params import Depends
10
11
  from .sign import update_func_sign
11
12
  from .types import AsyncFunc, DepOf, Func
12
13
 
@@ -34,7 +35,7 @@ def _dep_arg_index(name: str, /) -> int | None:
34
35
 
35
36
 
36
37
  def _dep_annotation(dep: DepOf[Any] | Callable[..., Any], /) -> Any:
37
- if isinstance(dep, Depends):
38
+ if isinstance(dep, params.Depends):
38
39
  return Annotated[Any, dep]
39
40
 
40
41
  # a marker is an annotation already, while a callable is a dependency the same way
@@ -1,8 +1,7 @@
1
1
  from typing import Annotated, Any, cast
2
2
 
3
- from fastapi import Depends
4
-
5
3
  from ._dataclass import MakeDataclass
4
+ from ._params import Depends
6
5
  from .types import DepOf
7
6
 
8
7
 
@@ -0,0 +1,19 @@
1
+ from fastapi import params
2
+
3
+ from ._dataclass import MakeDataclass
4
+
5
+
6
+ class Depends(MakeDataclass, params.Depends, frozen=True):
7
+ pass
8
+
9
+
10
+ class Security(Depends, params.Security, frozen=True):
11
+ def __post_init__(self) -> None:
12
+ if self.scopes is not None:
13
+ object.__setattr__(self, "scopes", tuple(self.scopes))
14
+
15
+
16
+ __all__ = [
17
+ "Depends",
18
+ "Security",
19
+ ]
@@ -4,12 +4,13 @@ from contextlib import AsyncExitStack
4
4
  from functools import lru_cache, wraps
5
5
  from typing import Annotated, Any, Literal, Protocol, cast, overload, runtime_checkable
6
6
 
7
- from fastapi import Depends, params
7
+ from fastapi import params
8
8
  from fastapi.dependencies.models import Dependant
9
9
  from fastapi.dependencies.utils import get_dependant, get_typed_signature, solve_dependencies
10
10
  from fastapi.exceptions import RequestValidationError
11
11
 
12
12
  from ._deps_tp import is_dep, unwrap_tp
13
+ from ._params import Depends
13
14
  from .scope import InjectScope
14
15
  from .sign import prepare_sign, update_func_sign
15
16
  from .types import Coro, DependencyCache, HasSignature
@@ -2,10 +2,11 @@ import inspect
2
2
  from collections.abc import AsyncIterator, Callable, Coroutine, Iterator, Mapping
3
3
  from typing import TYPE_CHECKING, Annotated, Any, Protocol, TypeVar, runtime_checkable
4
4
 
5
- from fastapi import Depends
6
5
  from fastapi.types import DependencyCacheKey
7
6
  from typing_extensions import TypeForm, sentinel
8
7
 
8
+ from ._params import Depends
9
+
9
10
  if TYPE_CHECKING:
10
11
  Injected: Any = object()
11
12
  else:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-injected"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Yet another library to reuse fastapi dependency injection"
5
5
  authors = [
6
6
  { name = "Yurii Karabas", email = "1998uriyyo@gmail.com" },
@@ -0,0 +1,30 @@
1
+ from typing import Annotated
2
+
3
+ from fastapi import params
4
+
5
+ from fastapi_injected import Depends, Security
6
+
7
+ from .deps import (
8
+ ContextState,
9
+ ctx_dep,
10
+ is_assignable_to,
11
+ is_subtype_of,
12
+ static_assert,
13
+ )
14
+
15
+ # they are the fastapi params they wrap, so anything taking one takes these
16
+ static_assert(is_subtype_of(Depends, params.Depends))
17
+ static_assert(is_subtype_of(Security, params.Security))
18
+ static_assert(is_subtype_of(Security, Depends))
19
+ static_assert(is_assignable_to(Annotated[ContextState, Security(ctx_dep, scopes=["me"])], ContextState))
20
+
21
+
22
+ def _params() -> None:
23
+ Depends()
24
+ Depends(ctx_dep, use_cache=False)
25
+ Security(ctx_dep, scopes=["me"], use_cache=False)
26
+
27
+
28
+ def _negatives() -> None:
29
+ Depends(use_cache="yes") # type: ignore[ty:invalid-argument-type]
30
+ Security(ctx_dep, scopes=1) # type: ignore[ty:invalid-argument-type]
@@ -0,0 +1,78 @@
1
+ from typing import Annotated
2
+
3
+ import pytest
4
+ from fastapi import params
5
+ from fastapi.security import SecurityScopes
6
+
7
+ from fastapi_injected import Depends, Security, is_dep, push_inject_scope, resolve
8
+
9
+
10
+ async def scopes_dep(scopes: SecurityScopes) -> list[str]:
11
+ return scopes.scopes
12
+
13
+
14
+ class Unhashable:
15
+ __hash__ = None # type: ignore[ty:invalid-assignment]
16
+
17
+ async def __call__(self) -> str:
18
+ return "value"
19
+
20
+
21
+ def test_they_are_fastapi_params():
22
+ assert isinstance(Depends(scopes_dep), params.Depends)
23
+ assert isinstance(Security(scopes_dep), params.Security)
24
+ assert isinstance(Security(scopes_dep), params.Depends)
25
+
26
+
27
+ def test_scopes_are_written_as_documented():
28
+ security = Security(scopes_dep, scopes=["items:read"])
29
+
30
+ assert security.scopes == ("items:read",)
31
+ assert is_dep(Annotated[list[str], security])
32
+
33
+
34
+ def test_equal_params_hash_equally():
35
+ assert hash(Depends(scopes_dep)) == hash(Depends(scopes_dep))
36
+ assert Security(scopes_dep, scopes=["me"]) == Security(scopes_dep, scopes=["me"])
37
+ assert hash(Security(scopes_dep, scopes=["me"])) == hash(Security(scopes_dep, scopes=("me",)))
38
+
39
+
40
+ def test_a_scoped_annotation_can_be_hashed():
41
+ # the annotation is the key a dependant is cached by, and a list of scopes used to break it
42
+ assert hash(Annotated[list[str], Security(scopes_dep, scopes=["items:read", "me"])])
43
+
44
+ with pytest.raises(TypeError, match="unhashable type: 'list'"):
45
+ hash(Annotated[list[str], params.Security(scopes_dep, scopes=["items:read", "me"])])
46
+
47
+
48
+ def test_unhashable_dependencies_fall_back_to_identity():
49
+ depends = Depends(Unhashable())
50
+
51
+ assert hash(depends) == object.__hash__(depends)
52
+ assert hash(Annotated[str, depends])
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_a_scoped_dependency_resolves():
57
+ scoped = Annotated[list[str], Security(scopes_dep, scopes=["items:read", "me"])]
58
+
59
+ assert await resolve(scoped) == ["items:read", "me"]
60
+
61
+
62
+ @pytest.mark.asyncio
63
+ async def test_equal_params_are_one_dependency():
64
+ calls = 0
65
+
66
+ async def counted() -> int:
67
+ nonlocal calls
68
+ calls += 1
69
+ return calls
70
+
71
+ async with push_inject_scope():
72
+ assert await resolve(Annotated[int, Security(counted, scopes=["me"])]) == 1
73
+ # the same scopes written as another list are the same dependency, so it is cached
74
+ assert await resolve(Annotated[int, Security(counted, scopes=["me"])]) == 1
75
+ # different scopes are not
76
+ assert await resolve(Annotated[int, Security(counted, scopes=["other"])]) == 2
77
+
78
+ assert calls == 2
@@ -186,7 +186,7 @@ wheels = [
186
186
 
187
187
  [[package]]
188
188
  name = "fastapi-injected"
189
- version = "0.3.0"
189
+ version = "0.3.1"
190
190
  source = { editable = "." }
191
191
  dependencies = [
192
192
  { name = "fastapi" },
@@ -249,11 +249,11 @@ wheels = [
249
249
 
250
250
  [[package]]
251
251
  name = "griffelib"
252
- version = "2.1.0"
252
+ version = "2.2.0"
253
253
  source = { registry = "https://pypi.org/simple" }
254
- sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" }
254
+ sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" }
255
255
  wheels = [
256
- { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" },
256
+ { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" },
257
257
  ]
258
258
 
259
259
  [[package]]