fauth 0.2.2__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 (62) hide show
  1. {fauth-0.2.2 → fauth-0.3.1}/.pre-commit-config.yaml +2 -2
  2. {fauth-0.2.2 → fauth-0.3.1}/CHANGELOG.md +87 -0
  3. {fauth-0.2.2 → fauth-0.3.1}/PKG-INFO +1 -1
  4. {fauth-0.2.2 → fauth-0.3.1}/fauth/__init__.py +2 -0
  5. fauth-0.3.1/fauth/middleware/__init__.py +3 -0
  6. fauth-0.3.1/fauth/middleware/auth.py +88 -0
  7. {fauth-0.2.2 → fauth-0.3.1}/fauth/providers/provider.py +9 -1
  8. fauth-0.3.1/fauth/py.typed +1 -0
  9. {fauth-0.2.2 → fauth-0.3.1}/pyproject.toml +1 -1
  10. fauth-0.3.1/tests/middleware/conftest.py +157 -0
  11. fauth-0.3.1/tests/middleware/test_auth_middleware.py +105 -0
  12. fauth-0.3.1/tests/testing/__init__.py +0 -0
  13. {fauth-0.2.2 → fauth-0.3.1}/uv.lock +55 -55
  14. {fauth-0.2.2 → fauth-0.3.1}/.github/actions/setup-python-env/action.yml +0 -0
  15. {fauth-0.2.2 → fauth-0.3.1}/.github/dependabot.yml +0 -0
  16. {fauth-0.2.2 → fauth-0.3.1}/.github/workflows/cicd.yml +0 -0
  17. {fauth-0.2.2 → fauth-0.3.1}/.github/workflows/pre-commit-autoupdate.yml +0 -0
  18. {fauth-0.2.2 → fauth-0.3.1}/.gitignore +0 -0
  19. {fauth-0.2.2 → fauth-0.3.1}/LICENSE +0 -0
  20. {fauth-0.2.2 → fauth-0.3.1}/README.md +0 -0
  21. {fauth-0.2.2 → fauth-0.3.1}/fauth/api/__init__.py +0 -0
  22. {fauth-0.2.2 → fauth-0.3.1}/fauth/api/router.py +0 -0
  23. {fauth-0.2.2 → fauth-0.3.1}/fauth/core/__init__.py +0 -0
  24. {fauth-0.2.2 → fauth-0.3.1}/fauth/core/config.py +0 -0
  25. {fauth-0.2.2 → fauth-0.3.1}/fauth/core/exceptions.py +0 -0
  26. {fauth-0.2.2 → fauth-0.3.1}/fauth/core/schemas.py +0 -0
  27. {fauth-0.2.2 → fauth-0.3.1}/fauth/crypto/__init__.py +0 -0
  28. {fauth-0.2.2 → fauth-0.3.1}/fauth/crypto/jwt.py +0 -0
  29. {fauth-0.2.2 → fauth-0.3.1}/fauth/crypto/password.py +0 -0
  30. {fauth-0.2.2 → fauth-0.3.1}/fauth/providers/__init__.py +0 -0
  31. {fauth-0.2.2 → fauth-0.3.1}/fauth/providers/protocols.py +0 -0
  32. {fauth-0.2.2 → fauth-0.3.1}/fauth/testing/__init__.py +0 -0
  33. {fauth-0.2.2 → fauth-0.3.1}/fauth/testing/config.py +0 -0
  34. {fauth-0.2.2 → fauth-0.3.1}/fauth/testing/fakes.py +0 -0
  35. {fauth-0.2.2 → fauth-0.3.1}/fauth/testing/provider.py +0 -0
  36. {fauth-0.2.2 → fauth-0.3.1}/fauth/transports/__init__.py +0 -0
  37. {fauth-0.2.2 → fauth-0.3.1}/fauth/transports/base.py +0 -0
  38. {fauth-0.2.2 → fauth-0.3.1}/fauth/transports/bearer.py +0 -0
  39. {fauth-0.2.2 → fauth-0.3.1}/fauth/utils/__init__.py +0 -0
  40. {fauth-0.2.2 → fauth-0.3.1}/fauth/utils/logging.py +0 -0
  41. {fauth-0.2.2 → fauth-0.3.1}/pytest.ini +0 -0
  42. {fauth-0.2.2 → fauth-0.3.1}/tests/__init__.py +0 -0
  43. {fauth-0.2.2 → fauth-0.3.1}/tests/api/__init__.py +0 -0
  44. {fauth-0.2.2 → fauth-0.3.1}/tests/api/conftest.py +0 -0
  45. {fauth-0.2.2 → fauth-0.3.1}/tests/api/test_openapi.py +0 -0
  46. {fauth-0.2.2 → fauth-0.3.1}/tests/api/test_router.py +0 -0
  47. {fauth-0.2.2 → fauth-0.3.1}/tests/conftest.py +0 -0
  48. {fauth-0.2.2 → fauth-0.3.1}/tests/core/__init__.py +0 -0
  49. {fauth-0.2.2 → fauth-0.3.1}/tests/core/conftest.py +0 -0
  50. {fauth-0.2.2 → fauth-0.3.1}/tests/core/test_config.py +0 -0
  51. {fauth-0.2.2 → fauth-0.3.1}/tests/core/test_exceptions.py +0 -0
  52. {fauth-0.2.2 → fauth-0.3.1}/tests/crypto/__init__.py +0 -0
  53. {fauth-0.2.2 → fauth-0.3.1}/tests/crypto/conftest.py +0 -0
  54. {fauth-0.2.2 → fauth-0.3.1}/tests/crypto/test_jwt.py +0 -0
  55. {fauth-0.2.2 → fauth-0.3.1}/tests/crypto/test_password.py +0 -0
  56. {fauth-0.2.2/tests/providers → fauth-0.3.1/tests/middleware}/__init__.py +0 -0
  57. {fauth-0.2.2/tests/testing → fauth-0.3.1/tests/providers}/__init__.py +0 -0
  58. {fauth-0.2.2 → fauth-0.3.1}/tests/providers/conftest.py +0 -0
  59. {fauth-0.2.2 → fauth-0.3.1}/tests/providers/test_provider.py +0 -0
  60. {fauth-0.2.2 → fauth-0.3.1}/tests/testing/test_testing.py +0 -0
  61. {fauth-0.2.2 → fauth-0.3.1}/tests/utils/conftest.py +0 -0
  62. {fauth-0.2.2 → fauth-0.3.1}/tests/utils/test_logging.py +0 -0
@@ -17,7 +17,7 @@ repos:
17
17
  - id: gitleaks
18
18
 
19
19
  - repo: https://github.com/astral-sh/ruff-pre-commit
20
- rev: 'v0.15.9'
20
+ rev: 'v0.15.10'
21
21
  hooks:
22
22
  - id: ruff-check
23
23
  name: ruff
@@ -44,7 +44,7 @@ repos:
44
44
  - fastapi==0.135.1
45
45
 
46
46
  - repo: https://github.com/pre-commit/mirrors-mypy
47
- rev: v1.20.0
47
+ rev: v1.20.1
48
48
  hooks:
49
49
  - id: mypy
50
50
  additional_dependencies:
@@ -1,6 +1,93 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.3.1 (2026-04-15)
5
+
6
+ ### Bug Fixes
7
+
8
+ - Add py.typed marker to enable type checking support
9
+ ([`1787519`](https://github.com/justmatias/fauth/commit/1787519027c7dfd6e57e1fddad45b7cdffe97e48))
10
+
11
+
12
+ ## v0.3.0 (2026-04-15)
13
+
14
+ ### Bug Fixes
15
+
16
+ - Correct typo in pylint disable directive within auth middleware
17
+ ([`bc06572`](https://github.com/justmatias/fauth/commit/bc0657237d634e604eaa6be046fbc68fa86a8396))
18
+
19
+ ### Chores
20
+
21
+ - Disable pylint complexity checks for auth middleware dispatch method
22
+ ([`7c335aa`](https://github.com/justmatias/fauth/commit/7c335aa3165bd5de2234084b6df6a9361f815922))
23
+
24
+ - **config**: Update pre-commit hooks
25
+ ([`34ca640`](https://github.com/justmatias/fauth/commit/34ca64099763b3d5faca1c1a2b56673584c4c321))
26
+
27
+ - **config**: Update pre-commit hooks
28
+ ([`e6225f9`](https://github.com/justmatias/fauth/commit/e6225f946ef5a3f89c6a15cd490656bdb57f3425))
29
+
30
+ - **deps**: Bump cryptography from 46.0.6 to 46.0.7
31
+ ([`c57f70f`](https://github.com/justmatias/fauth/commit/c57f70f21998cb49b83673cc7c47e8c3826dc65c))
32
+
33
+ Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.6 to 46.0.7. -
34
+ [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) -
35
+ [Commits](https://github.com/pyca/cryptography/compare/46.0.6...46.0.7)
36
+
37
+ --- updated-dependencies: - dependency-name: cryptography dependency-version: 46.0.7
38
+
39
+ dependency-type: indirect ...
40
+
41
+ Signed-off-by: dependabot[bot] <support@github.com>
42
+
43
+ - **deps**: Bump pygments from 2.19.2 to 2.20.0
44
+ ([`c022333`](https://github.com/justmatias/fauth/commit/c022333b5f61003c07242810126fec443bd93a91))
45
+
46
+ Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to 2.20.0. - [Release
47
+ notes](https://github.com/pygments/pygments/releases) -
48
+ [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES) -
49
+ [Commits](https://github.com/pygments/pygments/compare/2.19.2...2.20.0)
50
+
51
+ --- updated-dependencies: - dependency-name: pygments dependency-version: 2.20.0
52
+
53
+ dependency-type: indirect ...
54
+
55
+ Signed-off-by: dependabot[bot] <support@github.com>
56
+
57
+ - **deps**: Bump pyjwt from 2.11.0 to 2.12.0
58
+ ([`0c443a9`](https://github.com/justmatias/fauth/commit/0c443a92b921cea23f1ac71ba837fc335fed183f))
59
+
60
+ Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.11.0 to 2.12.0. - [Release
61
+ notes](https://github.com/jpadilla/pyjwt/releases) -
62
+ [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) -
63
+ [Commits](https://github.com/jpadilla/pyjwt/compare/2.11.0...2.12.0)
64
+
65
+ --- updated-dependencies: - dependency-name: pyjwt dependency-version: 2.12.0
66
+
67
+ dependency-type: indirect ...
68
+
69
+ Signed-off-by: dependabot[bot] <support@github.com>
70
+
71
+ - **deps-dev**: Bump pytest from 9.0.2 to 9.0.3
72
+ ([`f1372ad`](https://github.com/justmatias/fauth/commit/f1372addbf826fe305623d713cb492ee51e6ced8))
73
+
74
+ Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to 9.0.3. - [Release
75
+ notes](https://github.com/pytest-dev/pytest/releases) -
76
+ [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) -
77
+ [Commits](https://github.com/pytest-dev/pytest/compare/9.0.2...9.0.3)
78
+
79
+ --- updated-dependencies: - dependency-name: pytest dependency-version: 9.0.3
80
+
81
+ dependency-type: direct:development ...
82
+
83
+ Signed-off-by: dependabot[bot] <support@github.com>
84
+
85
+ ### Features
86
+
87
+ - Implement AuthMiddleware for JWT-based request authentication and user loading
88
+ ([`0527038`](https://github.com/justmatias/fauth/commit/0527038d3f5e897fc77258820a26c062d3fc075e))
89
+
90
+
4
91
  ## v0.2.2 (2026-04-04)
5
92
 
6
93
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fauth
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: Ergonomic, lightweight JWT authentication for FastAPI. Secure your routes instantly with plug-and-play dependency injection
5
5
  Project-URL: Homepage, https://github.com/justmatias/fauth
6
6
  Project-URL: Repository, https://github.com/justmatias/fauth
@@ -18,6 +18,7 @@ from .crypto import (
18
18
  hash_password,
19
19
  verify_password,
20
20
  )
21
+ from .middleware import AuthMiddleware
21
22
  from .providers import AuthProvider, UserLoader
22
23
  from .transports import BearerTransport, Transport
23
24
 
@@ -25,6 +26,7 @@ __version__ = version("fauth")
25
26
 
26
27
  __all__ = [
27
28
  "AuthConfig",
29
+ "AuthMiddleware",
28
30
  "AuthProvider",
29
31
  "BearerTransport",
30
32
  "FAuthError",
@@ -0,0 +1,3 @@
1
+ from .auth import AuthMiddleware
2
+
3
+ __all__ = ["AuthMiddleware"]
@@ -0,0 +1,88 @@
1
+ from collections.abc import Awaitable, Callable
2
+
3
+ from starlette.middleware.base import BaseHTTPMiddleware
4
+ from starlette.requests import Request
5
+ from starlette.responses import JSONResponse, Response
6
+ from starlette.types import ASGIApp
7
+
8
+ from fauth.core import AuthConfig, InvalidTokenError, TokenExpiredError, TokenPayload
9
+ from fauth.crypto import decode_token
10
+ from fauth.providers.protocols import UserLoader
11
+ from fauth.transports import BearerTransport, Transport
12
+ from fauth.utils import logger
13
+
14
+
15
+ class AuthMiddleware(BaseHTTPMiddleware):
16
+ """
17
+ ASGI middleware that extracts and validates the Bearer token from the
18
+ Authorization header, then stores the loaded user in ``request.state.user``
19
+ and the decoded payload in ``request.state.token_payload``.
20
+
21
+ When ``auto_error=True`` (default), any missing, expired, or invalid token
22
+ produces a 401 JSON response before the route handler is reached. Set
23
+ ``auto_error=False`` to let unauthenticated requests pass through so that
24
+ route-level dependencies can decide how to handle them.
25
+
26
+ Use ``exclude_paths`` to bypass middleware enforcement for specific paths
27
+ such as ``/login`` or ``/health``.
28
+ """
29
+
30
+ def __init__( # pylint: disable=too-many-arguments, too-many-positional-arguments
31
+ self,
32
+ app: ASGIApp,
33
+ config: AuthConfig,
34
+ user_loader: UserLoader,
35
+ transport: Transport | None = None,
36
+ token_payload_schema: type[TokenPayload] = TokenPayload,
37
+ auto_error: bool = True,
38
+ exclude_paths: list[str] | None = None,
39
+ ):
40
+ super().__init__(app)
41
+ self.config = config
42
+ self.user_loader = user_loader
43
+ self.transport = transport or BearerTransport()
44
+ self.token_payload_schema = token_payload_schema
45
+ self.auto_error = auto_error
46
+ self.exclude_paths = set(exclude_paths or [])
47
+
48
+ async def dispatch( # pylint: disable=too-complex, too-many-return-statements
49
+ self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
50
+ ) -> Response:
51
+ if request.url.path in self.exclude_paths:
52
+ return await call_next(request)
53
+
54
+ token = await self.transport(request)
55
+ request.state.token = token
56
+
57
+ if not token:
58
+ if self.auto_error:
59
+ await logger.error("Authentication failed", reason="Missing token")
60
+ return JSONResponse({"detail": "Not authenticated"}, status_code=401)
61
+ return await call_next(request)
62
+
63
+ try:
64
+ payload = decode_token(token, self.config, self.token_payload_schema)
65
+ except TokenExpiredError:
66
+ if self.auto_error:
67
+ await logger.warning("Authentication failed", reason="Token expired")
68
+ return JSONResponse({"detail": "Token expired"}, status_code=401)
69
+ return await call_next(request)
70
+ except InvalidTokenError:
71
+ if self.auto_error:
72
+ await logger.warning("Authentication failed", reason="Invalid token")
73
+ return JSONResponse({"detail": "Invalid token"}, status_code=401)
74
+ return await call_next(request)
75
+
76
+ user = await self.user_loader(payload)
77
+ if not user:
78
+ if self.auto_error:
79
+ await logger.warning(
80
+ "Authentication failed", reason="User not found", sub=payload.sub
81
+ )
82
+ return JSONResponse({"detail": "User does not exist"}, status_code=401)
83
+ return await call_next(request)
84
+
85
+ request.state.user = user
86
+ request.state.token_payload = payload
87
+ await logger.debug("User authenticated via middleware", sub=payload.sub)
88
+ return await call_next(request)
@@ -51,7 +51,15 @@ class AuthProvider(Generic[T]):
51
51
  self.transport = transport
52
52
 
53
53
  async def require_user(self, request: Request) -> T:
54
- """Utility to get the currently authenticated user."""
54
+ """Utility to get the currently authenticated user.
55
+
56
+ If AuthMiddleware is registered, reads directly from request.state (fast path).
57
+ Otherwise extracts and validates the Bearer token from the Authorization header.
58
+ """
59
+ user: T | None = getattr(request.state, "user", None)
60
+ if user:
61
+ return user
62
+
55
63
  token = await self.transport(request)
56
64
  if not token:
57
65
  await logger.error("Authentication failed", reason="Missing token")
@@ -0,0 +1 @@
1
+ # PEP 561 type hint marker.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fauth"
3
- version = "0.2.2"
3
+ version = "0.3.1"
4
4
  description = "Ergonomic, lightweight JWT authentication for FastAPI. Secure your routes instantly with plug-and-play dependency injection"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,157 @@
1
+ # pylint: disable=duplicate-code
2
+ import time
3
+ from typing import Any
4
+ from uuid import UUID, uuid4
5
+
6
+ import pytest
7
+ from fastapi import Depends, FastAPI, Request
8
+ from fastapi.testclient import TestClient
9
+ from pydantic import BaseModel, Field
10
+
11
+ from fauth.core import AuthConfig
12
+ from fauth.crypto import create_access_token, hash_password
13
+ from fauth.middleware import AuthMiddleware
14
+ from fauth.providers import AuthProvider
15
+ from fauth.testing import FakeUserLoader
16
+
17
+
18
+ class DummyUser(BaseModel):
19
+ id_: UUID = Field(default_factory=uuid4)
20
+ hashed_password: str = Field(default=hash_password("secret_password"))
21
+ is_active: bool = Field(default=True)
22
+
23
+
24
+ @pytest.fixture
25
+ def user() -> DummyUser:
26
+ return DummyUser()
27
+
28
+
29
+ @pytest.fixture
30
+ def user_loader() -> FakeUserLoader[DummyUser]:
31
+ return FakeUserLoader()
32
+
33
+
34
+ @pytest.fixture
35
+ def provider(
36
+ auth_config: AuthConfig,
37
+ user_loader: FakeUserLoader[DummyUser],
38
+ ) -> AuthProvider[DummyUser]:
39
+ return AuthProvider(config=auth_config, user_loader=user_loader)
40
+
41
+
42
+ @pytest.fixture
43
+ def app(
44
+ auth_config: AuthConfig,
45
+ user_loader: FakeUserLoader[DummyUser],
46
+ ) -> FastAPI:
47
+ _app = FastAPI()
48
+ _app.add_middleware(
49
+ AuthMiddleware,
50
+ config=auth_config,
51
+ user_loader=user_loader,
52
+ )
53
+
54
+ @_app.get("/protected")
55
+ async def protected(request: Request) -> dict[str, Any]:
56
+ return {"id": str(request.state.user.id_)}
57
+
58
+ @_app.get("/public")
59
+ async def public() -> dict[str, Any]:
60
+ return {"msg": "ok"}
61
+
62
+ return _app
63
+
64
+
65
+ @pytest.fixture
66
+ def app_auto_error_off(
67
+ auth_config: AuthConfig,
68
+ user_loader: FakeUserLoader[DummyUser],
69
+ ) -> FastAPI:
70
+ _app = FastAPI()
71
+ _app.add_middleware(
72
+ AuthMiddleware,
73
+ config=auth_config,
74
+ user_loader=user_loader,
75
+ auto_error=False,
76
+ )
77
+
78
+ @_app.get("/maybe-protected")
79
+ async def maybe_protected(request: Request) -> dict[str, Any]:
80
+ user = getattr(request.state, "user", None)
81
+ token = getattr(request.state, "token", None)
82
+ return {"authenticated": user is not None, "has_token": token is not None}
83
+
84
+ return _app
85
+
86
+
87
+ @pytest.fixture
88
+ def app_with_excludes(
89
+ auth_config: AuthConfig,
90
+ user_loader: FakeUserLoader[DummyUser],
91
+ ) -> FastAPI:
92
+ _app = FastAPI()
93
+ _app.add_middleware(
94
+ AuthMiddleware,
95
+ config=auth_config,
96
+ user_loader=user_loader,
97
+ exclude_paths=["/health"],
98
+ )
99
+
100
+ @_app.get("/health")
101
+ async def health() -> dict[str, Any]:
102
+ return {"status": "ok"}
103
+
104
+ @_app.get("/protected")
105
+ async def protected(request: Request) -> dict[str, Any]:
106
+ return {"id": str(request.state.user.id_)}
107
+
108
+ return _app
109
+
110
+
111
+ @pytest.fixture
112
+ def app_with_require_user(
113
+ auth_config: AuthConfig,
114
+ user_loader: FakeUserLoader[DummyUser],
115
+ provider: AuthProvider[DummyUser],
116
+ ) -> FastAPI:
117
+ _app = FastAPI()
118
+ _app.add_middleware(
119
+ AuthMiddleware,
120
+ config=auth_config,
121
+ user_loader=user_loader,
122
+ )
123
+
124
+ @_app.get("/me")
125
+ async def me(
126
+ current_user: DummyUser = Depends(provider.require_user),
127
+ ) -> dict[str, Any]:
128
+ return {"id": str(current_user.id_)}
129
+
130
+ return _app
131
+
132
+
133
+ @pytest.fixture
134
+ def client(app: FastAPI) -> TestClient:
135
+ return TestClient(app)
136
+
137
+
138
+ @pytest.fixture
139
+ def user_token(auth_config: AuthConfig, user: DummyUser) -> str:
140
+ return create_access_token(sub=str(user.id_), config=auth_config)
141
+
142
+
143
+ @pytest.fixture
144
+ def expired_token(auth_config: AuthConfig) -> str:
145
+ expired_config = AuthConfig(
146
+ secret_key=auth_config.secret_key,
147
+ algorithm=auth_config.algorithm,
148
+ access_token_expire_minutes=0,
149
+ )
150
+ token = create_access_token(sub="any-user", config=expired_config)
151
+ time.sleep(1)
152
+ return token
153
+
154
+
155
+ @pytest.fixture
156
+ def _populate_user(user_loader: FakeUserLoader[DummyUser], user: DummyUser) -> None:
157
+ user_loader.add_user(str(user.id_), user)
@@ -0,0 +1,105 @@
1
+ import pytest
2
+ from fastapi import FastAPI
3
+ from fastapi.testclient import TestClient
4
+
5
+ from .conftest import DummyUser
6
+
7
+
8
+ def test_missing_token_returns_401(client: TestClient) -> None:
9
+ response = client.get("/protected")
10
+ assert response.status_code == 401
11
+ assert response.json()["detail"] == "Not authenticated"
12
+
13
+
14
+ def test_malformed_token_returns_401(client: TestClient) -> None:
15
+ response = client.get(
16
+ "/protected", headers={"Authorization": "Bearer not-a-valid-jwt"}
17
+ )
18
+ assert response.status_code == 401
19
+ assert response.json()["detail"] == "Invalid token"
20
+
21
+
22
+ def test_expired_token_returns_401(client: TestClient, expired_token: str) -> None:
23
+ response = client.get(
24
+ "/protected", headers={"Authorization": f"Bearer {expired_token}"}
25
+ )
26
+ assert response.status_code == 401
27
+ assert response.json()["detail"] == "Token expired"
28
+
29
+
30
+ @pytest.mark.usefixtures("_populate_user")
31
+ def test_valid_token_populates_state(
32
+ client: TestClient, user: DummyUser, user_token: str
33
+ ) -> None:
34
+ response = client.get(
35
+ "/protected", headers={"Authorization": f"Bearer {user_token}"}
36
+ )
37
+ assert response.status_code == 200
38
+ assert response.json()["id"] == str(user.id_)
39
+
40
+
41
+ def test_unknown_user_returns_401(client: TestClient, user_token: str) -> None:
42
+ # user_loader is empty — token is valid but user does not exist
43
+ response = client.get(
44
+ "/protected", headers={"Authorization": f"Bearer {user_token}"}
45
+ )
46
+ assert response.status_code == 401
47
+ assert response.json()["detail"] == "User does not exist"
48
+
49
+
50
+ def test_auto_error_off_passes_unauthenticated_request(
51
+ app_auto_error_off: FastAPI,
52
+ ) -> None:
53
+ client = TestClient(app_auto_error_off)
54
+ response = client.get("/maybe-protected")
55
+ assert response.status_code == 200
56
+ assert response.json()["authenticated"] is False
57
+ assert response.json()["has_token"] is False
58
+
59
+
60
+ @pytest.mark.usefixtures("_populate_user")
61
+ def test_auto_error_off_populates_state_when_token_valid(
62
+ app_auto_error_off: FastAPI, user_token: str
63
+ ) -> None:
64
+ client = TestClient(app_auto_error_off)
65
+ response = client.get(
66
+ "/maybe-protected", headers={"Authorization": f"Bearer {user_token}"}
67
+ )
68
+ assert response.status_code == 200
69
+ assert response.json()["authenticated"] is True
70
+ assert response.json()["has_token"] is True
71
+
72
+
73
+ def test_raw_token_stored_on_state_even_when_decode_fails(
74
+ app_auto_error_off: FastAPI,
75
+ ) -> None:
76
+ client = TestClient(app_auto_error_off)
77
+ response = client.get(
78
+ "/maybe-protected", headers={"Authorization": "Bearer not-a-valid-jwt"}
79
+ )
80
+ assert response.status_code == 200
81
+ assert response.json()["authenticated"] is False
82
+ assert response.json()["has_token"] is True
83
+
84
+
85
+ def test_excluded_path_bypasses_enforcement(app_with_excludes: FastAPI) -> None:
86
+ client = TestClient(app_with_excludes)
87
+ response = client.get("/health")
88
+ assert response.status_code == 200
89
+ assert response.json()["status"] == "ok"
90
+
91
+
92
+ def test_non_excluded_path_still_enforced(app_with_excludes: FastAPI) -> None:
93
+ client = TestClient(app_with_excludes)
94
+ response = client.get("/protected")
95
+ assert response.status_code == 401
96
+
97
+
98
+ @pytest.mark.usefixtures("_populate_user")
99
+ def test_require_user_reads_from_state_when_middleware_set(
100
+ app_with_require_user: FastAPI, user: DummyUser, user_token: str
101
+ ) -> None:
102
+ client = TestClient(app_with_require_user)
103
+ response = client.get("/me", headers={"Authorization": f"Bearer {user_token}"})
104
+ assert response.status_code == 200
105
+ assert response.json()["id"] == str(user.id_)
File without changes
@@ -250,55 +250,55 @@ wheels = [
250
250
 
251
251
  [[package]]
252
252
  name = "cryptography"
253
- version = "46.0.6"
253
+ version = "46.0.7"
254
254
  source = { registry = "https://pypi.org/simple" }
255
255
  dependencies = [
256
256
  { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
257
257
  ]
258
- sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
259
- wheels = [
260
- { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
261
- { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
262
- { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
263
- { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
264
- { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
265
- { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
266
- { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
267
- { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
268
- { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
269
- { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
270
- { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
271
- { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
272
- { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
273
- { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
274
- { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" },
275
- { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" },
276
- { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" },
277
- { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" },
278
- { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" },
279
- { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" },
280
- { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" },
281
- { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" },
282
- { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" },
283
- { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" },
284
- { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" },
285
- { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" },
286
- { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" },
287
- { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" },
288
- { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
289
- { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
290
- { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
291
- { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
292
- { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
293
- { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
294
- { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
295
- { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
296
- { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
297
- { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
298
- { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
299
- { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
300
- { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
301
- { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
258
+ sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
259
+ wheels = [
260
+ { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
261
+ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
262
+ { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
263
+ { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
264
+ { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
265
+ { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
266
+ { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
267
+ { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
268
+ { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
269
+ { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
270
+ { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
271
+ { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
272
+ { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
273
+ { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
274
+ { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
275
+ { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
276
+ { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
277
+ { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
278
+ { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
279
+ { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
280
+ { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
281
+ { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
282
+ { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
283
+ { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
284
+ { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
285
+ { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
286
+ { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
287
+ { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
288
+ { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
289
+ { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
290
+ { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
291
+ { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
292
+ { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
293
+ { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
294
+ { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
295
+ { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
296
+ { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
297
+ { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
298
+ { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
299
+ { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
300
+ { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
301
+ { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
302
302
  ]
303
303
 
304
304
  [[package]]
@@ -340,7 +340,7 @@ wheels = [
340
340
 
341
341
  [[package]]
342
342
  name = "fauth"
343
- version = "0.2.1"
343
+ version = "0.2.2"
344
344
  source = { editable = "." }
345
345
  dependencies = [
346
346
  { name = "fastapi" },
@@ -670,20 +670,20 @@ wheels = [
670
670
 
671
671
  [[package]]
672
672
  name = "pygments"
673
- version = "2.19.2"
673
+ version = "2.20.0"
674
674
  source = { registry = "https://pypi.org/simple" }
675
- sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
675
+ sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
676
676
  wheels = [
677
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
677
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
678
678
  ]
679
679
 
680
680
  [[package]]
681
681
  name = "pyjwt"
682
- version = "2.11.0"
682
+ version = "2.12.0"
683
683
  source = { registry = "https://pypi.org/simple" }
684
- sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
684
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/10/e8192be5f38f3e8e7e046716de4cae33d56fd5ae08927a823bb916be36c1/pyjwt-2.12.0.tar.gz", hash = "sha256:2f62390b667cd8257de560b850bb5a883102a388829274147f1d724453f8fb02", size = 102511, upload-time = "2026-03-12T17:15:30.831Z" }
685
685
  wheels = [
686
- { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
686
+ { url = "https://files.pythonhosted.org/packages/15/70/70f895f404d363d291dcf62c12c85fdd47619ad9674ac0f53364d035925a/pyjwt-2.12.0-py3-none-any.whl", hash = "sha256:9bb459d1bdd0387967d287f5656bf7ec2b9a26645d1961628cda1764e087fd6e", size = 29700, upload-time = "2026-03-12T17:15:29.257Z" },
687
687
  ]
688
688
 
689
689
  [package.optional-dependencies]
@@ -693,7 +693,7 @@ crypto = [
693
693
 
694
694
  [[package]]
695
695
  name = "pytest"
696
- version = "9.0.2"
696
+ version = "9.0.3"
697
697
  source = { registry = "https://pypi.org/simple" }
698
698
  dependencies = [
699
699
  { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -702,9 +702,9 @@ dependencies = [
702
702
  { name = "pluggy" },
703
703
  { name = "pygments" },
704
704
  ]
705
- sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
705
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
706
706
  wheels = [
707
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
707
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
708
708
  ]
709
709
 
710
710
  [[package]]
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