From 6ac64ad293c39d14f5b226fd3a85c78d52add04b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Thu, 2 May 2024 16:46:44 +0200 Subject: [PATCH 01/25] Enable UV installer in Hatch config --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3f17d6a7..cbc0ea24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ commit_extra_args = ["-e"] path = "fastapi_users/__init__.py" [tool.hatch.envs.default] +installer = "uv" features = [ "sqlalchemy", "beanie", From abfa9a1c47a786eddf84a297667ee96ca0f2aca9 Mon Sep 17 00:00:00 2001 From: Alexander Zinov <33320473+sashkent3@users.noreply.github.com> Date: Sun, 14 Jul 2024 17:04:13 +0400 Subject: [PATCH 02/25] Improve type hints (#1401) * Add type parameters to `AuthenticationBackend` * add more type-hints --- examples/beanie-oauth/app/users.py | 4 ++-- examples/sqlalchemy-oauth/app/users.py | 4 ++-- examples/sqlalchemy/app/users.py | 4 ++-- fastapi_users/authentication/authenticator.py | 20 +++++++++---------- fastapi_users/fastapi_users.py | 8 ++++---- fastapi_users/router/auth.py | 4 ++-- fastapi_users/router/oauth.py | 4 ++-- fastapi_users/router/users.py | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/beanie-oauth/app/users.py b/examples/beanie-oauth/app/users.py index 81ad5ca4..cd7097c4 100644 --- a/examples/beanie-oauth/app/users.py +++ b/examples/beanie-oauth/app/users.py @@ -3,7 +3,7 @@ from beanie import PydanticObjectId from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers +from fastapi_users import BaseUserManager, FastAPIUsers, models from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, @@ -47,7 +47,7 @@ async def get_user_manager(user_db: BeanieUserDatabase = Depends(get_user_db)): bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") -def get_jwt_strategy() -> JWTStrategy: +def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: return JWTStrategy(secret=SECRET, lifetime_seconds=3600) diff --git a/examples/sqlalchemy-oauth/app/users.py b/examples/sqlalchemy-oauth/app/users.py index 0b61b2a5..a7337e7f 100644 --- a/examples/sqlalchemy-oauth/app/users.py +++ b/examples/sqlalchemy-oauth/app/users.py @@ -3,7 +3,7 @@ from typing import Optional from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, @@ -47,7 +47,7 @@ async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") -def get_jwt_strategy() -> JWTStrategy: +def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: return JWTStrategy(secret=SECRET, lifetime_seconds=3600) diff --git a/examples/sqlalchemy/app/users.py b/examples/sqlalchemy/app/users.py index 479c49e2..f37f0ac3 100644 --- a/examples/sqlalchemy/app/users.py +++ b/examples/sqlalchemy/app/users.py @@ -2,7 +2,7 @@ from typing import Optional from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, models from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, @@ -40,7 +40,7 @@ async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") -def get_jwt_strategy() -> JWTStrategy: +def get_jwt_strategy() -> JWTStrategy[models.UP, models.ID]: return JWTStrategy(secret=SECRET, lifetime_seconds=3600) diff --git a/fastapi_users/authentication/authenticator.py b/fastapi_users/authentication/authenticator.py index 7fab4b78..6117765f 100644 --- a/fastapi_users/authentication/authenticator.py +++ b/fastapi_users/authentication/authenticator.py @@ -1,6 +1,6 @@ import re from inspect import Parameter, Signature -from typing import Callable, List, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Generic, List, Optional, Sequence, Tuple, cast from fastapi import Depends, HTTPException, status from makefun import with_signature @@ -31,10 +31,10 @@ class DuplicateBackendNamesError(Exception): pass -EnabledBackendsDependency = DependencyCallable[Sequence[AuthenticationBackend]] +EnabledBackendsDependency = DependencyCallable[Sequence[AuthenticationBackend[models.UP, models.ID]]] -class Authenticator: +class Authenticator(Generic[models.UP, models.ID]): """ Provides dependency callables to retrieve authenticated user. @@ -46,11 +46,11 @@ class Authenticator: :param get_user_manager: User manager dependency callable. """ - backends: Sequence[AuthenticationBackend] + backends: Sequence[AuthenticationBackend[models.UP, models.ID]] def __init__( self, - backends: Sequence[AuthenticationBackend], + backends: Sequence[AuthenticationBackend[models.UP, models.ID]], get_user_manager: UserManagerDependency[models.UP, models.ID], ): self.backends = backends @@ -62,7 +62,7 @@ def current_user_token( active: bool = False, verified: bool = False, superuser: bool = False, - get_enabled_backends: Optional[EnabledBackendsDependency] = None, + get_enabled_backends: Optional[EnabledBackendsDependency[models.UP, models.ID]] = None, ): """ Return a dependency callable to retrieve currently authenticated user and token. @@ -88,7 +88,7 @@ def current_user_token( signature = self._get_dependency_signature(get_enabled_backends) @with_signature(signature) - async def current_user_token_dependency(*args, **kwargs): + async def current_user_token_dependency(*args: Any, **kwargs: Any): return await self._authenticate( *args, optional=optional, @@ -106,7 +106,7 @@ def current_user( active: bool = False, verified: bool = False, superuser: bool = False, - get_enabled_backends: Optional[EnabledBackendsDependency] = None, + get_enabled_backends: Optional[EnabledBackendsDependency[models.UP, models.ID]] = None, ): """ Return a dependency callable to retrieve currently authenticated user. @@ -132,7 +132,7 @@ def current_user( signature = self._get_dependency_signature(get_enabled_backends) @with_signature(signature) - async def current_user_dependency(*args, **kwargs): + async def current_user_dependency(*args: Any, **kwargs: Any): user, _ = await self._authenticate( *args, optional=optional, @@ -157,7 +157,7 @@ async def _authenticate( ) -> Tuple[Optional[models.UP], Optional[str]]: user: Optional[models.UP] = None token: Optional[str] = None - enabled_backends: Sequence[AuthenticationBackend] = kwargs.get( + enabled_backends: Sequence[AuthenticationBackend[models.UP, models.ID]] = kwargs.get( "enabled_backends", self.backends ) for backend in self.backends: diff --git a/fastapi_users/fastapi_users.py b/fastapi_users/fastapi_users.py index 10ca1465..85edb1bd 100644 --- a/fastapi_users/fastapi_users.py +++ b/fastapi_users/fastapi_users.py @@ -35,12 +35,12 @@ class FastAPIUsers(Generic[models.UP, models.ID]): with a specific set of parameters. """ - authenticator: Authenticator + authenticator: Authenticator[models.UP, models.ID] def __init__( self, get_user_manager: UserManagerDependency[models.UP, models.ID], - auth_backends: Sequence[AuthenticationBackend], + auth_backends: Sequence[AuthenticationBackend[models.UP, models.ID]], ): self.authenticator = Authenticator(auth_backends, get_user_manager) self.get_user_manager = get_user_manager @@ -72,7 +72,7 @@ def get_reset_password_router(self) -> APIRouter: return get_reset_password_router(self.get_user_manager) def get_auth_router( - self, backend: AuthenticationBackend, requires_verification: bool = False + self, backend: AuthenticationBackend[models.UP, models.ID], requires_verification: bool = False ) -> APIRouter: """ Return an auth router for a given authentication backend. @@ -91,7 +91,7 @@ def get_auth_router( def get_oauth_router( self, oauth_client: BaseOAuth2, - backend: AuthenticationBackend, + backend: AuthenticationBackend[models.UP, models.ID], state_secret: SecretType, redirect_url: Optional[str] = None, associate_by_email: bool = False, diff --git a/fastapi_users/router/auth.py b/fastapi_users/router/auth.py index c61770f0..57f397d0 100644 --- a/fastapi_users/router/auth.py +++ b/fastapi_users/router/auth.py @@ -11,9 +11,9 @@ def get_auth_router( - backend: AuthenticationBackend, + backend: AuthenticationBackend[models.UP, models.ID], get_user_manager: UserManagerDependency[models.UP, models.ID], - authenticator: Authenticator, + authenticator: Authenticator[models.UP, models.ID], requires_verification: bool = False, ) -> APIRouter: """Generate a router with login/logout routes for an authentication backend.""" diff --git a/fastapi_users/router/oauth.py b/fastapi_users/router/oauth.py index 9300c603..12cdf325 100644 --- a/fastapi_users/router/oauth.py +++ b/fastapi_users/router/oauth.py @@ -29,7 +29,7 @@ def generate_state_token( def get_oauth_router( oauth_client: BaseOAuth2, - backend: AuthenticationBackend, + backend: AuthenticationBackend[models.UP, models.ID], get_user_manager: UserManagerDependency[models.UP, models.ID], state_secret: SecretType, redirect_url: Optional[str] = None, @@ -156,7 +156,7 @@ async def callback( def get_oauth_associate_router( oauth_client: BaseOAuth2, - authenticator: Authenticator, + authenticator: Authenticator[models.UP, models.ID], get_user_manager: UserManagerDependency[models.UP, models.ID], user_schema: Type[schemas.U], state_secret: SecretType, diff --git a/fastapi_users/router/users.py b/fastapi_users/router/users.py index b3cc4351..179230aa 100644 --- a/fastapi_users/router/users.py +++ b/fastapi_users/router/users.py @@ -12,7 +12,7 @@ def get_users_router( get_user_manager: UserManagerDependency[models.UP, models.ID], user_schema: Type[schemas.U], user_update_schema: Type[schemas.UU], - authenticator: Authenticator, + authenticator: Authenticator[models.UP, models.ID], requires_verification: bool = False, ) -> APIRouter: """Generate a router with the authentication routes.""" From 5cc57fac07364bb60e95fe6492715607b6800352 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:04:34 +0200 Subject: [PATCH 03/25] docs: add sashkent3 as a contributor for code (#1411) * docs: update README.md [skip ci] * docs: update .all-contributorsrc [skip ci] --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> --- .all-contributorsrc | 9 +++++++++ README.md | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index 003e1a5d..4066cf1a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -741,6 +741,15 @@ "contributions": [ "doc" ] + }, + { + "login": "sashkent3", + "name": "Alexander Zinov", + "avatar_url": "https://avatars.githubusercontent.com/u/33320473?v=4", + "profile": "https://github.com/sashkent3", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/README.md b/README.md index 460e7007..637e4f36 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![PyPI version](https://badge.fury.io/py/fastapi-users.svg)](https://badge.fury.io/py/fastapi-users) [![Downloads](https://pepy.tech/badge/fastapi-users)](https://pepy.tech/project/fastapi-users) -[![All Contributors](https://img.shields.io/badge/all_contributors-79-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-80-orange.svg?style=flat-square)](#contributors-)

@@ -183,6 +183,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d raindata5
raindata5

📖 Mark Donnelly
Mark Donnelly

📖 + Alexander Zinov
Alexander Zinov

đŸ’ģ From c0c4da9a6caaa51c8b14c1d2439a43485bd2fb6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:25:34 +0200 Subject: [PATCH 04/25] Update email-validator requirement from <2.2,>=1.1.0 to >=1.1.0,<2.3 (#1399) Updates the requirements on [email-validator](https://github.com/JoshData/python-email-validator) to permit the latest version. - [Release notes](https://github.com/JoshData/python-email-validator/releases) - [Changelog](https://github.com/JoshData/python-email-validator/blob/main/CHANGELOG.md) - [Commits](https://github.com/JoshData/python-email-validator/compare/v1.1.0...v2.2.0) --- updated-dependencies: - dependency-name: email-validator dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cbc0ea24..968afe92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,7 @@ requires-python = ">=3.8" dependencies = [ "fastapi >=0.65.2", "pwdlib[argon2,bcrypt] ==0.2.0", - "email-validator >=1.1.0,<2.2", + "email-validator >=1.1.0,<2.3", "pyjwt[crypto] ==2.8.0", "python-multipart ==0.0.9", "makefun >=1.11.2,<2.0.0", From 42ddc241b965475390e2bce887b084152ae1a2cd Mon Sep 17 00:00:00 2001 From: Marios Pitsiali <54856433+Mpitsiali@users.noreply.github.com> Date: Fri, 13 Sep 2024 18:46:25 +0300 Subject: [PATCH 05/25] Fix typo in auth docs stategies/database.md (#1437) --- docs/configuration/authentication/strategies/database.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/authentication/strategies/database.md b/docs/configuration/authentication/strategies/database.md index 40594e21..ec82f2c7 100644 --- a/docs/configuration/authentication/strategies/database.md +++ b/docs/configuration/authentication/strategies/database.md @@ -1,6 +1,6 @@ # Database -The most natural way for storing tokens is of course the very same database you're using for your application. In this strategy, we set up a table (or collection) for storing those tokens with the associated user id. On each request, we try to retrive this token from the database to get the corresponding user id. +The most natural way for storing tokens is of course the very same database you're using for your application. In this strategy, we set up a table (or collection) for storing those tokens with the associated user id. On each request, we try to retrieve this token from the database to get the corresponding user id. ## Configuration From f37a48f97d6c68384ffc5060808e655741156a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sat, 19 Oct 2024 15:16:40 +0200 Subject: [PATCH 06/25] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 637e4f36..85bc3338 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,13 @@ Add quickly a registration and authentication system to your [FastAPI](https://f ## In a hurry? Discover Fief, the open-source authentication platform

- Fief + Fief

-Fief +Fief **Implementing registration, login, social auth is hard and painful. We know it. With our highly secure and open-source users management platform, you can focus on your app while staying in control of your users data.** -* Based on **FastAPI Users**! * **Open-source**: self-host it for free * **Pre-built login and registration pages**: clean and fast authentication so you don't have to do it yourself * **Official Python client** with built-in **FastAPI integration** @@ -68,9 +67,9 @@ Add quickly a registration and authentication system to your [FastAPI](https://f

- +

-

It's free!

+

It's free and open-source

## Contributors and sponsors âœ¨â˜•ī¸ From 7f92a82e076b38ee6893f0dc01fb350a832ed34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 12:46:46 +0000 Subject: [PATCH 07/25] Fix linting --- fastapi_users/authentication/authenticator.py | 16 +++++++++++----- fastapi_users/fastapi_users.py | 4 +++- tests/test_manager.py | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/fastapi_users/authentication/authenticator.py b/fastapi_users/authentication/authenticator.py index 6117765f..897228a2 100644 --- a/fastapi_users/authentication/authenticator.py +++ b/fastapi_users/authentication/authenticator.py @@ -31,7 +31,9 @@ class DuplicateBackendNamesError(Exception): pass -EnabledBackendsDependency = DependencyCallable[Sequence[AuthenticationBackend[models.UP, models.ID]]] +EnabledBackendsDependency = DependencyCallable[ + Sequence[AuthenticationBackend[models.UP, models.ID]] +] class Authenticator(Generic[models.UP, models.ID]): @@ -62,7 +64,9 @@ def current_user_token( active: bool = False, verified: bool = False, superuser: bool = False, - get_enabled_backends: Optional[EnabledBackendsDependency[models.UP, models.ID]] = None, + get_enabled_backends: Optional[ + EnabledBackendsDependency[models.UP, models.ID] + ] = None, ): """ Return a dependency callable to retrieve currently authenticated user and token. @@ -106,7 +110,9 @@ def current_user( active: bool = False, verified: bool = False, superuser: bool = False, - get_enabled_backends: Optional[EnabledBackendsDependency[models.UP, models.ID]] = None, + get_enabled_backends: Optional[ + EnabledBackendsDependency[models.UP, models.ID] + ] = None, ): """ Return a dependency callable to retrieve currently authenticated user. @@ -157,8 +163,8 @@ async def _authenticate( ) -> Tuple[Optional[models.UP], Optional[str]]: user: Optional[models.UP] = None token: Optional[str] = None - enabled_backends: Sequence[AuthenticationBackend[models.UP, models.ID]] = kwargs.get( - "enabled_backends", self.backends + enabled_backends: Sequence[AuthenticationBackend[models.UP, models.ID]] = ( + kwargs.get("enabled_backends", self.backends) ) for backend in self.backends: if backend in enabled_backends: diff --git a/fastapi_users/fastapi_users.py b/fastapi_users/fastapi_users.py index 85edb1bd..980b674c 100644 --- a/fastapi_users/fastapi_users.py +++ b/fastapi_users/fastapi_users.py @@ -72,7 +72,9 @@ def get_reset_password_router(self) -> APIRouter: return get_reset_password_router(self.get_user_manager) def get_auth_router( - self, backend: AuthenticationBackend[models.UP, models.ID], requires_verification: bool = False + self, + backend: AuthenticationBackend[models.UP, models.ID], + requires_verification: bool = False, ) -> APIRouter: """ Return an auth router for a given authentication backend. diff --git a/tests/test_manager.py b/tests/test_manager.py index 30fa5a5e..2eab6c96 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -139,7 +139,7 @@ async def test_regular_user( ): user = UserCreate(email=email, password="guinevere") created_user = await user_manager.create(user) - assert type(created_user) == UserModel + assert isinstance(created_user, UserModel) assert user_manager.on_after_register.called is True @@ -151,7 +151,7 @@ async def test_superuser( email="lancelot@camelot.b", password="guinevere", is_superuser=True ) created_user = await user_manager.create(user, safe) - assert type(created_user) == UserModel + assert isinstance(created_user, UserModel) assert created_user.is_superuser is result assert user_manager.on_after_register.called is True @@ -164,7 +164,7 @@ async def test_is_active( email="lancelot@camelot.b", password="guinevere", is_active=False ) created_user = await user_manager.create(user, safe) - assert type(created_user) == UserModel + assert isinstance(created_user, UserModel) assert created_user.is_active is result assert user_manager.on_after_register.called is True From caa17889e1cb3e704f493c439c48c236011958dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 12:51:32 +0000 Subject: [PATCH 08/25] Drop Python 3.8 support --- .github/workflows/build.yml | 6 +- .github/workflows/documentation.yml | 2 +- docs/src/db_beanie_oauth.py | 4 +- docs/src/db_sqlalchemy.py | 2 +- docs/src/db_sqlalchemy_access_tokens.py | 2 +- docs/src/db_sqlalchemy_oauth.py | 4 +- examples/beanie-oauth/app/db.py | 4 +- examples/sqlalchemy-oauth/app/db.py | 4 +- examples/sqlalchemy/app/db.py | 2 +- fastapi_users/authentication/authenticator.py | 7 +- .../authentication/strategy/db/adapter.py | 6 +- .../authentication/strategy/db/strategy.py | 4 +- fastapi_users/authentication/strategy/jwt.py | 4 +- fastapi_users/db/base.py | 10 +- fastapi_users/fastapi_users.py | 15 +- fastapi_users/jwt.py | 8 +- fastapi_users/manager.py | 6 +- fastapi_users/models.py | 4 +- fastapi_users/openapi.py | 4 +- fastapi_users/password.py | 6 +- fastapi_users/router/auth.py | 4 +- fastapi_users/router/common.py | 4 +- fastapi_users/router/oauth.py | 18 +- fastapi_users/router/register.py | 6 +- fastapi_users/router/users.py | 6 +- fastapi_users/router/verify.py | 4 +- fastapi_users/schemas.py | 12 +- fastapi_users/types.py | 11 +- pyproject.toml | 5 +- tests/conftest.py | 29 +-- tests/test_authentication_authenticator.py | 5 +- tests/test_authentication_backend.py | 6 +- tests/test_authentication_strategy_db.py | 8 +- tests/test_authentication_strategy_redis.py | 4 +- tests/test_fastapi_users.py | 3 +- tests/test_router_auth.py | 35 ++-- tests/test_router_oauth.py | 12 +- tests/test_router_register.py | 13 +- tests/test_router_reset.py | 9 +- tests/test_router_users.py | 173 +++++++++--------- tests/test_router_verify.py | 11 +- 41 files changed, 231 insertions(+), 251 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98d7c83c..af1e4999 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python_version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python_version: [3.9, '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python_version: [3.8, 3.9, '3.10', '3.11', '3.12'] + python_version: [3.9, '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 @@ -64,7 +64,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies shell: bash run: | diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 65adb395..51f638d3 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies shell: bash run: | diff --git a/docs/src/db_beanie_oauth.py b/docs/src/db_beanie_oauth.py index e7179a6d..b1786869 100644 --- a/docs/src/db_beanie_oauth.py +++ b/docs/src/db_beanie_oauth.py @@ -1,5 +1,3 @@ -from typing import List - import motor.motor_asyncio from beanie import Document from fastapi_users.db import BaseOAuthAccount, BeanieBaseUser, BeanieUserDatabase @@ -17,7 +15,7 @@ class OAuthAccount(BaseOAuthAccount): class User(BeanieBaseUser, Document): - oauth_accounts: List[OAuthAccount] = Field(default_factory=list) + oauth_accounts: list[OAuthAccount] = Field(default_factory=list) async def get_user_db(): diff --git a/docs/src/db_sqlalchemy.py b/docs/src/db_sqlalchemy.py index 1ff831d3..1bff0fd6 100644 --- a/docs/src/db_sqlalchemy.py +++ b/docs/src/db_sqlalchemy.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator +from collections.abc import AsyncGenerator from fastapi import Depends from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase diff --git a/docs/src/db_sqlalchemy_access_tokens.py b/docs/src/db_sqlalchemy_access_tokens.py index 97fc20bd..79c75816 100644 --- a/docs/src/db_sqlalchemy_access_tokens.py +++ b/docs/src/db_sqlalchemy_access_tokens.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator +from collections.abc import AsyncGenerator from fastapi import Depends from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase diff --git a/docs/src/db_sqlalchemy_oauth.py b/docs/src/db_sqlalchemy_oauth.py index 827dc38a..93a38e11 100644 --- a/docs/src/db_sqlalchemy_oauth.py +++ b/docs/src/db_sqlalchemy_oauth.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator, List +from collections.abc import AsyncGenerator from fastapi import Depends from fastapi_users.db import ( @@ -21,7 +21,7 @@ class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): class User(SQLAlchemyBaseUserTableUUID, Base): - oauth_accounts: Mapped[List[OAuthAccount]] = relationship( + oauth_accounts: Mapped[list[OAuthAccount]] = relationship( "OAuthAccount", lazy="joined" ) diff --git a/examples/beanie-oauth/app/db.py b/examples/beanie-oauth/app/db.py index e7179a6d..b1786869 100644 --- a/examples/beanie-oauth/app/db.py +++ b/examples/beanie-oauth/app/db.py @@ -1,5 +1,3 @@ -from typing import List - import motor.motor_asyncio from beanie import Document from fastapi_users.db import BaseOAuthAccount, BeanieBaseUser, BeanieUserDatabase @@ -17,7 +15,7 @@ class OAuthAccount(BaseOAuthAccount): class User(BeanieBaseUser, Document): - oauth_accounts: List[OAuthAccount] = Field(default_factory=list) + oauth_accounts: list[OAuthAccount] = Field(default_factory=list) async def get_user_db(): diff --git a/examples/sqlalchemy-oauth/app/db.py b/examples/sqlalchemy-oauth/app/db.py index 827dc38a..93a38e11 100644 --- a/examples/sqlalchemy-oauth/app/db.py +++ b/examples/sqlalchemy-oauth/app/db.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator, List +from collections.abc import AsyncGenerator from fastapi import Depends from fastapi_users.db import ( @@ -21,7 +21,7 @@ class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): class User(SQLAlchemyBaseUserTableUUID, Base): - oauth_accounts: Mapped[List[OAuthAccount]] = relationship( + oauth_accounts: Mapped[list[OAuthAccount]] = relationship( "OAuthAccount", lazy="joined" ) diff --git a/examples/sqlalchemy/app/db.py b/examples/sqlalchemy/app/db.py index 1ff831d3..1bff0fd6 100644 --- a/examples/sqlalchemy/app/db.py +++ b/examples/sqlalchemy/app/db.py @@ -1,4 +1,4 @@ -from typing import AsyncGenerator +from collections.abc import AsyncGenerator from fastapi import Depends from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase diff --git a/fastapi_users/authentication/authenticator.py b/fastapi_users/authentication/authenticator.py index 897228a2..bc30a0bf 100644 --- a/fastapi_users/authentication/authenticator.py +++ b/fastapi_users/authentication/authenticator.py @@ -1,6 +1,7 @@ import re +from collections.abc import Sequence from inspect import Parameter, Signature -from typing import Any, Callable, Generic, List, Optional, Sequence, Tuple, cast +from typing import Any, Callable, Generic, Optional, cast from fastapi import Depends, HTTPException, status from makefun import with_signature @@ -160,7 +161,7 @@ async def _authenticate( verified: bool = False, superuser: bool = False, **kwargs, - ) -> Tuple[Optional[models.UP], Optional[str]]: + ) -> tuple[Optional[models.UP], Optional[str]]: user: Optional[models.UP] = None token: Optional[str] = None enabled_backends: Sequence[AuthenticationBackend[models.UP, models.ID]] = ( @@ -203,7 +204,7 @@ def _get_dependency_signature( This way, each security schemes are detected by the OpenAPI generator. """ try: - parameters: List[Parameter] = [ + parameters: list[Parameter] = [ Parameter( name="user_manager", kind=Parameter.POSITIONAL_OR_KEYWORD, diff --git a/fastapi_users/authentication/strategy/db/adapter.py b/fastapi_users/authentication/strategy/db/adapter.py index 6ed1a02b..c5b999b2 100644 --- a/fastapi_users/authentication/strategy/db/adapter.py +++ b/fastapi_users/authentication/strategy/db/adapter.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Dict, Generic, Optional, Protocol +from typing import Any, Generic, Optional, Protocol from fastapi_users.authentication.strategy.db.models import AP @@ -13,11 +13,11 @@ async def get_by_token( """Get a single access token by token.""" ... # pragma: no cover - async def create(self, create_dict: Dict[str, Any]) -> AP: + async def create(self, create_dict: dict[str, Any]) -> AP: """Create an access token.""" ... # pragma: no cover - async def update(self, access_token: AP, update_dict: Dict[str, Any]) -> AP: + async def update(self, access_token: AP, update_dict: dict[str, Any]) -> AP: """Update an access token.""" ... # pragma: no cover diff --git a/fastapi_users/authentication/strategy/db/strategy.py b/fastapi_users/authentication/strategy/db/strategy.py index d7c3c7a3..da438438 100644 --- a/fastapi_users/authentication/strategy/db/strategy.py +++ b/fastapi_users/authentication/strategy/db/strategy.py @@ -1,6 +1,6 @@ import secrets from datetime import datetime, timedelta, timezone -from typing import Any, Dict, Generic, Optional +from typing import Any, Generic, Optional from fastapi_users import exceptions, models from fastapi_users.authentication.strategy.base import Strategy @@ -50,6 +50,6 @@ async def destroy_token(self, token: str, user: models.UP) -> None: if access_token is not None: await self.database.delete(access_token) - def _create_access_token_dict(self, user: models.UP) -> Dict[str, Any]: + def _create_access_token_dict(self, user: models.UP) -> dict[str, Any]: token = secrets.token_urlsafe() return {"token": token, "user_id": user.id} diff --git a/fastapi_users/authentication/strategy/jwt.py b/fastapi_users/authentication/strategy/jwt.py index bf3fd903..d790ca79 100644 --- a/fastapi_users/authentication/strategy/jwt.py +++ b/fastapi_users/authentication/strategy/jwt.py @@ -1,4 +1,4 @@ -from typing import Generic, List, Optional +from typing import Generic, Optional import jwt @@ -22,7 +22,7 @@ def __init__( self, secret: SecretType, lifetime_seconds: Optional[int], - token_audience: List[str] = ["fastapi-users:auth"], + token_audience: list[str] = ["fastapi-users:auth"], algorithm: str = "HS256", public_key: Optional[SecretType] = None, ): diff --git a/fastapi_users/db/base.py b/fastapi_users/db/base.py index 5c0b675c..c90ad6b2 100644 --- a/fastapi_users/db/base.py +++ b/fastapi_users/db/base.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Generic, Optional +from typing import Any, Generic, Optional from fastapi_users.models import ID, OAP, UOAP, UP from fastapi_users.types import DependencyCallable @@ -19,11 +19,11 @@ async def get_by_oauth_account(self, oauth: str, account_id: str) -> Optional[UP """Get a single user by OAuth account id.""" raise NotImplementedError() - async def create(self, create_dict: Dict[str, Any]) -> UP: + async def create(self, create_dict: dict[str, Any]) -> UP: """Create a user.""" raise NotImplementedError() - async def update(self, user: UP, update_dict: Dict[str, Any]) -> UP: + async def update(self, user: UP, update_dict: dict[str, Any]) -> UP: """Update a user.""" raise NotImplementedError() @@ -32,7 +32,7 @@ async def delete(self, user: UP) -> None: raise NotImplementedError() async def add_oauth_account( - self: "BaseUserDatabase[UOAP, ID]", user: UOAP, create_dict: Dict[str, Any] + self: "BaseUserDatabase[UOAP, ID]", user: UOAP, create_dict: dict[str, Any] ) -> UOAP: """Create an OAuth account and add it to the user.""" raise NotImplementedError() @@ -41,7 +41,7 @@ async def update_oauth_account( self: "BaseUserDatabase[UOAP, ID]", user: UOAP, oauth_account: OAP, - update_dict: Dict[str, Any], + update_dict: dict[str, Any], ) -> UOAP: """Update an OAuth account on a user.""" raise NotImplementedError() diff --git a/fastapi_users/fastapi_users.py b/fastapi_users/fastapi_users.py index 980b674c..1161d9f3 100644 --- a/fastapi_users/fastapi_users.py +++ b/fastapi_users/fastapi_users.py @@ -1,4 +1,5 @@ -from typing import Generic, Optional, Sequence, Type +from collections.abc import Sequence +from typing import Generic, Optional from fastapi import APIRouter @@ -20,7 +21,7 @@ from fastapi_users.router import get_oauth_router from fastapi_users.router.oauth import get_oauth_associate_router except ModuleNotFoundError: # pragma: no cover - BaseOAuth2 = Type # type: ignore + BaseOAuth2 = type # type: ignore class FastAPIUsers(Generic[models.UP, models.ID]): @@ -47,7 +48,7 @@ def __init__( self.current_user = self.authenticator.current_user def get_register_router( - self, user_schema: Type[schemas.U], user_create_schema: Type[schemas.UC] + self, user_schema: type[schemas.U], user_create_schema: type[schemas.UC] ) -> APIRouter: """ Return a router with a register route. @@ -59,7 +60,7 @@ def get_register_router( self.get_user_manager, user_schema, user_create_schema ) - def get_verify_router(self, user_schema: Type[schemas.U]) -> APIRouter: + def get_verify_router(self, user_schema: type[schemas.U]) -> APIRouter: """ Return a router with e-mail verification routes. @@ -126,7 +127,7 @@ def get_oauth_router( def get_oauth_associate_router( self, oauth_client: BaseOAuth2, - user_schema: Type[schemas.U], + user_schema: type[schemas.U], state_secret: SecretType, redirect_url: Optional[str] = None, requires_verification: bool = False, @@ -154,8 +155,8 @@ def get_oauth_associate_router( def get_users_router( self, - user_schema: Type[schemas.U], - user_update_schema: Type[schemas.UU], + user_schema: type[schemas.U], + user_update_schema: type[schemas.UU], requires_verification: bool = False, ) -> APIRouter: """ diff --git a/fastapi_users/jwt.py b/fastapi_users/jwt.py index 4278ee8f..05214101 100644 --- a/fastapi_users/jwt.py +++ b/fastapi_users/jwt.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta, timezone -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import jwt from pydantic import SecretStr @@ -30,9 +30,9 @@ def generate_jwt( def decode_jwt( encoded_jwt: str, secret: SecretType, - audience: List[str], - algorithms: List[str] = [JWT_ALGORITHM], -) -> Dict[str, Any]: + audience: list[str], + algorithms: list[str] = [JWT_ALGORITHM], +) -> dict[str, Any]: return jwt.decode( encoded_jwt, _get_secret_value(secret), diff --git a/fastapi_users/manager.py b/fastapi_users/manager.py index 65da8113..7d783b75 100644 --- a/fastapi_users/manager.py +++ b/fastapi_users/manager.py @@ -1,5 +1,5 @@ import uuid -from typing import Any, Dict, Generic, Optional, Union +from typing import Any, Generic, Optional, Union import jwt from fastapi import Request, Response @@ -514,7 +514,7 @@ async def on_after_register( async def on_after_update( self, user: models.UP, - update_dict: Dict[str, Any], + update_dict: dict[str, Any], request: Optional[Request] = None, ) -> None: """ @@ -662,7 +662,7 @@ async def authenticate( return user - async def _update(self, user: models.UP, update_dict: Dict[str, Any]) -> models.UP: + async def _update(self, user: models.UP, update_dict: dict[str, Any]) -> models.UP: validated_update_dict = {} for field, value in update_dict.items(): if field == "email" and value != user.email: diff --git a/fastapi_users/models.py b/fastapi_users/models.py index b65b64bd..16680b40 100644 --- a/fastapi_users/models.py +++ b/fastapi_users/models.py @@ -1,4 +1,4 @@ -from typing import Generic, List, Optional, Protocol, TypeVar +from typing import Generic, Optional, Protocol, TypeVar ID = TypeVar("ID") @@ -39,7 +39,7 @@ class UserOAuthProtocol(UserProtocol[ID], Generic[ID, OAP]): is_active: bool is_superuser: bool is_verified: bool - oauth_accounts: List[OAP] + oauth_accounts: list[OAP] UOAP = TypeVar("UOAP", bound=UserOAuthProtocol) diff --git a/fastapi_users/openapi.py b/fastapi_users/openapi.py index 98e3c063..68a5d67a 100644 --- a/fastapi_users/openapi.py +++ b/fastapi_users/openapi.py @@ -1,3 +1,3 @@ -from typing import Any, Dict, Union +from typing import Any, Union -OpenAPIResponseType = Dict[Union[int, str], Dict[str, Any]] +OpenAPIResponseType = dict[Union[int, str], dict[str, Any]] diff --git a/fastapi_users/password.py b/fastapi_users/password.py index 4dbb4c95..71cbd2b2 100644 --- a/fastapi_users/password.py +++ b/fastapi_users/password.py @@ -1,5 +1,5 @@ import secrets -from typing import Optional, Protocol, Tuple, Union +from typing import Optional, Protocol, Union from pwdlib import PasswordHash from pwdlib.hashers.argon2 import Argon2Hasher @@ -9,7 +9,7 @@ class PasswordHelperProtocol(Protocol): def verify_and_update( self, plain_password: str, hashed_password: str - ) -> Tuple[bool, Union[str, None]]: ... # pragma: no cover + ) -> tuple[bool, Union[str, None]]: ... # pragma: no cover def hash(self, password: str) -> str: ... # pragma: no cover @@ -30,7 +30,7 @@ def __init__(self, password_hash: Optional[PasswordHash] = None) -> None: def verify_and_update( self, plain_password: str, hashed_password: str - ) -> Tuple[bool, Union[str, None]]: + ) -> tuple[bool, Union[str, None]]: return self.password_hash.verify_and_update(plain_password, hashed_password) def hash(self, password: str) -> str: diff --git a/fastapi_users/router/auth.py b/fastapi_users/router/auth.py index 57f397d0..7097b76d 100644 --- a/fastapi_users/router/auth.py +++ b/fastapi_users/router/auth.py @@ -1,5 +1,3 @@ -from typing import Tuple - from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.security import OAuth2PasswordRequestForm @@ -83,7 +81,7 @@ async def login( "/logout", name=f"auth:{backend.name}.logout", responses=logout_responses ) async def logout( - user_token: Tuple[models.UP, str] = Depends(get_current_user_token), + user_token: tuple[models.UP, str] = Depends(get_current_user_token), strategy: Strategy[models.UP, models.ID] = Depends(backend.get_strategy), ): user, token = user_token diff --git a/fastapi_users/router/common.py b/fastapi_users/router/common.py index 8a329485..51441e85 100644 --- a/fastapi_users/router/common.py +++ b/fastapi_users/router/common.py @@ -1,11 +1,11 @@ from enum import Enum -from typing import Dict, Union +from typing import Union from pydantic import BaseModel class ErrorModel(BaseModel): - detail: Union[str, Dict[str, str]] + detail: Union[str, dict[str, str]] class ErrorCodeReasonModel(BaseModel): diff --git a/fastapi_users/router/oauth.py b/fastapi_users/router/oauth.py index 12cdf325..b14aa95f 100644 --- a/fastapi_users/router/oauth.py +++ b/fastapi_users/router/oauth.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple, Type +from typing import Optional import jwt from fastapi import APIRouter, Depends, HTTPException, Query, Request, status @@ -21,7 +21,7 @@ class OAuth2AuthorizeResponse(BaseModel): def generate_state_token( - data: Dict[str, str], secret: SecretType, lifetime_seconds: int = 3600 + data: dict[str, str], secret: SecretType, lifetime_seconds: int = 3600 ) -> str: data["aud"] = STATE_TOKEN_AUDIENCE return generate_jwt(data, secret, lifetime_seconds) @@ -57,14 +57,14 @@ def get_oauth_router( response_model=OAuth2AuthorizeResponse, ) async def authorize( - request: Request, scopes: List[str] = Query(None) + request: Request, scopes: list[str] = Query(None) ) -> OAuth2AuthorizeResponse: if redirect_url is not None: authorize_redirect_url = redirect_url else: authorize_redirect_url = str(request.url_for(callback_route_name)) - state_data: Dict[str, str] = {} + state_data: dict[str, str] = {} state = generate_state_token(state_data, state_secret) authorization_url = await oauth_client.get_authorization_url( authorize_redirect_url, @@ -100,7 +100,7 @@ async def authorize( ) async def callback( request: Request, - access_token_state: Tuple[OAuth2Token, str] = Depends( + access_token_state: tuple[OAuth2Token, str] = Depends( oauth2_authorize_callback ), user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), @@ -158,7 +158,7 @@ def get_oauth_associate_router( oauth_client: BaseOAuth2, authenticator: Authenticator[models.UP, models.ID], get_user_manager: UserManagerDependency[models.UP, models.ID], - user_schema: Type[schemas.U], + user_schema: type[schemas.U], state_secret: SecretType, redirect_url: Optional[str] = None, requires_verification: bool = False, @@ -190,7 +190,7 @@ def get_oauth_associate_router( ) async def authorize( request: Request, - scopes: List[str] = Query(None), + scopes: list[str] = Query(None), user: models.UP = Depends(get_current_active_user), ) -> OAuth2AuthorizeResponse: if redirect_url is not None: @@ -198,7 +198,7 @@ async def authorize( else: authorize_redirect_url = str(request.url_for(callback_route_name)) - state_data: Dict[str, str] = {"sub": str(user.id)} + state_data: dict[str, str] = {"sub": str(user.id)} state = generate_state_token(state_data, state_secret) authorization_url = await oauth_client.get_authorization_url( authorize_redirect_url, @@ -232,7 +232,7 @@ async def authorize( async def callback( request: Request, user: models.UP = Depends(get_current_active_user), - access_token_state: Tuple[OAuth2Token, str] = Depends( + access_token_state: tuple[OAuth2Token, str] = Depends( oauth2_authorize_callback ), user_manager: BaseUserManager[models.UP, models.ID] = Depends(get_user_manager), diff --git a/fastapi_users/router/register.py b/fastapi_users/router/register.py index 33facd46..7f3c9c7e 100644 --- a/fastapi_users/router/register.py +++ b/fastapi_users/router/register.py @@ -1,5 +1,3 @@ -from typing import Type - from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi_users import exceptions, models, schemas @@ -9,8 +7,8 @@ def get_register_router( get_user_manager: UserManagerDependency[models.UP, models.ID], - user_schema: Type[schemas.U], - user_create_schema: Type[schemas.UC], + user_schema: type[schemas.U], + user_create_schema: type[schemas.UC], ) -> APIRouter: """Generate a router with the register route.""" router = APIRouter() diff --git a/fastapi_users/router/users.py b/fastapi_users/router/users.py index 179230aa..d6c21e18 100644 --- a/fastapi_users/router/users.py +++ b/fastapi_users/router/users.py @@ -1,5 +1,3 @@ -from typing import Type - from fastapi import APIRouter, Depends, HTTPException, Request, Response, status from fastapi_users import exceptions, models, schemas @@ -10,8 +8,8 @@ def get_users_router( get_user_manager: UserManagerDependency[models.UP, models.ID], - user_schema: Type[schemas.U], - user_update_schema: Type[schemas.UU], + user_schema: type[schemas.U], + user_update_schema: type[schemas.UU], authenticator: Authenticator[models.UP, models.ID], requires_verification: bool = False, ) -> APIRouter: diff --git a/fastapi_users/router/verify.py b/fastapi_users/router/verify.py index 299bdc19..33a87853 100644 --- a/fastapi_users/router/verify.py +++ b/fastapi_users/router/verify.py @@ -1,5 +1,3 @@ -from typing import Type - from fastapi import APIRouter, Body, Depends, HTTPException, Request, status from pydantic import EmailStr @@ -10,7 +8,7 @@ def get_verify_router( get_user_manager: UserManagerDependency[models.UP, models.ID], - user_schema: Type[schemas.U], + user_schema: type[schemas.U], ): router = APIRouter() diff --git a/fastapi_users/schemas.py b/fastapi_users/schemas.py index 1a618410..8cc7f1b2 100644 --- a/fastapi_users/schemas.py +++ b/fastapi_users/schemas.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Generic, List, Optional, Type, TypeVar +from typing import Any, Generic, Optional, TypeVar from pydantic import BaseModel, ConfigDict, EmailStr from pydantic.version import VERSION as PYDANTIC_VERSION @@ -11,18 +11,18 @@ if PYDANTIC_V2: # pragma: no cover - def model_dump(model: BaseModel, *args, **kwargs) -> Dict[str, Any]: + def model_dump(model: BaseModel, *args, **kwargs) -> dict[str, Any]: return model.model_dump(*args, **kwargs) # type: ignore - def model_validate(schema: Type[SCHEMA], obj: Any, *args, **kwargs) -> SCHEMA: + def model_validate(schema: type[SCHEMA], obj: Any, *args, **kwargs) -> SCHEMA: return schema.model_validate(obj, *args, **kwargs) # type: ignore else: # pragma: no cover # type: ignore - def model_dump(model: BaseModel, *args, **kwargs) -> Dict[str, Any]: + def model_dump(model: BaseModel, *args, **kwargs) -> dict[str, Any]: return model.dict(*args, **kwargs) # type: ignore - def model_validate(schema: Type[SCHEMA], obj: Any, *args, **kwargs) -> SCHEMA: + def model_validate(schema: type[SCHEMA], obj: Any, *args, **kwargs) -> SCHEMA: return schema.from_orm(obj) # type: ignore @@ -104,4 +104,4 @@ class Config: class BaseOAuthAccountMixin(BaseModel): """Adds OAuth accounts list to a User model.""" - oauth_accounts: List[BaseOAuthAccount] = [] + oauth_accounts: list[BaseOAuthAccount] = [] diff --git a/fastapi_users/types.py b/fastapi_users/types.py index 29c0c5c2..94d3c724 100644 --- a/fastapi_users/types.py +++ b/fastapi_users/types.py @@ -1,12 +1,5 @@ -from typing import ( - AsyncGenerator, - AsyncIterator, - Callable, - Coroutine, - Generator, - TypeVar, - Union, -) +from collections.abc import AsyncGenerator, AsyncIterator, Coroutine, Generator +from typing import Callable, TypeVar, Union RETURN_TYPE = TypeVar("RETURN_TYPE") diff --git a/pyproject.toml b/pyproject.toml index 968afe92..55f5b7c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ markers = [ ] [tool.ruff] -target-version = "py38" +target-version = "py39" [tool.ruff.lint] extend-select = ["UP", "TRY"] @@ -132,7 +132,6 @@ classifiers = [ "Framework :: FastAPI", "Framework :: AsyncIO", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -140,7 +139,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Topic :: Internet :: WWW/HTTP :: Session", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "fastapi >=0.65.2", "pwdlib[argon2,bcrypt] ==0.2.0", diff --git a/tests/conftest.py b/tests/conftest.py index 0cf63a46..d780fc27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,17 +1,8 @@ import asyncio import dataclasses import uuid -from typing import ( - Any, - AsyncGenerator, - Callable, - Dict, - Generic, - List, - Optional, - Type, - Union, -) +from collections.abc import AsyncGenerator +from typing import Any, Callable, Generic, Optional, Union from unittest.mock import MagicMock import httpx @@ -66,7 +57,7 @@ class OAuthAccountModel(models.OAuthAccountProtocol[IDType]): @dataclasses.dataclass class UserOAuthModel(UserModel): - oauth_accounts: List[OAuthAccountModel] = dataclasses.field(default_factory=list) + oauth_accounts: list[OAuthAccountModel] = dataclasses.field(default_factory=list) class User(schemas.BaseUser[IDType]): @@ -344,11 +335,11 @@ async def get_by_email(self, email: str) -> Optional[UserModel]: return verified_superuser return None - async def create(self, create_dict: Dict[str, Any]) -> UserModel: + async def create(self, create_dict: dict[str, Any]) -> UserModel: return UserModel(**create_dict) async def update( - self, user: UserModel, update_dict: Dict[str, Any] + self, user: UserModel, update_dict: dict[str, Any] ) -> UserModel: for field, value in update_dict.items(): setattr(user, field, value) @@ -414,11 +405,11 @@ async def get_by_oauth_account( return inactive_user_oauth return None - async def create(self, create_dict: Dict[str, Any]) -> UserOAuthModel: + async def create(self, create_dict: dict[str, Any]) -> UserOAuthModel: return UserOAuthModel(**create_dict) async def update( - self, user: UserOAuthModel, update_dict: Dict[str, Any] + self, user: UserOAuthModel, update_dict: dict[str, Any] ) -> UserOAuthModel: for field, value in update_dict.items(): setattr(user, field, value) @@ -428,7 +419,7 @@ async def delete(self, user: UserOAuthModel) -> None: pass async def add_oauth_account( - self, user: UserOAuthModel, create_dict: Dict[str, Any] + self, user: UserOAuthModel, create_dict: dict[str, Any] ) -> UserOAuthModel: oauth_account = OAuthAccountModel(**create_dict) user.oauth_accounts.append(oauth_account) @@ -438,7 +429,7 @@ async def update_oauth_account( # type: ignore self, user: UserOAuthModel, oauth_account: OAuthAccountModel, - update_dict: Dict[str, Any], + update_dict: dict[str, Any], ) -> UserOAuthModel: for field, value in update_dict.items(): setattr(oauth_account, field, value) @@ -458,7 +449,7 @@ async def update_oauth_account( # type: ignore @pytest.fixture def make_user_manager(mocker: MockerFixture): - def _make_user_manager(user_manager_class: Type[BaseTestUserManager], mock_user_db): + def _make_user_manager(user_manager_class: type[BaseTestUserManager], mock_user_db): user_manager = user_manager_class(mock_user_db) mocker.spy(user_manager, "get_by_email") mocker.spy(user_manager, "request_verify") diff --git a/tests/test_authentication_authenticator.py b/tests/test_authentication_authenticator.py index baec8724..a7b04d22 100644 --- a/tests/test_authentication_authenticator.py +++ b/tests/test_authentication_authenticator.py @@ -1,4 +1,5 @@ -from typing import AsyncGenerator, Generic, List, Optional, Sequence +from collections.abc import AsyncGenerator, Sequence +from typing import Generic, Optional import httpx import pytest @@ -70,7 +71,7 @@ def _get_backend_user(name: str = "user"): @pytest_asyncio.fixture def get_test_auth_client(get_user_manager, get_test_client): async def _get_test_auth_client( - backends: List[AuthenticationBackend], + backends: list[AuthenticationBackend], get_enabled_backends: Optional[ DependencyCallable[Sequence[AuthenticationBackend]] ] = None, diff --git a/tests/test_authentication_backend.py b/tests/test_authentication_backend.py index a4cbc050..973b21c3 100644 --- a/tests/test_authentication_backend.py +++ b/tests/test_authentication_backend.py @@ -1,4 +1,4 @@ -from typing import Callable, Generic, Optional, Type, cast +from typing import Callable, Generic, Optional, cast import pytest from fastapi import Response @@ -34,13 +34,13 @@ async def destroy_token(self, token: str, user: models.UP) -> None: @pytest.fixture(params=[MockTransport, MockTransportLogoutNotSupported]) def transport(request) -> Transport: - transport_class: Type[BearerTransport] = request.param + transport_class: type[BearerTransport] = request.param return transport_class(tokenUrl="/login") @pytest.fixture(params=[MockStrategy, MockStrategyDestroyNotSupported]) def get_strategy(request) -> Callable[..., Strategy]: - strategy_class: Type[Strategy] = request.param + strategy_class: type[Strategy] = request.param return lambda: strategy_class() diff --git a/tests/test_authentication_strategy_db.py b/tests/test_authentication_strategy_db.py index c81a7626..998e0a35 100644 --- a/tests/test_authentication_strategy_db.py +++ b/tests/test_authentication_strategy_db.py @@ -1,7 +1,7 @@ import dataclasses import uuid from datetime import datetime, timezone -from typing import Any, Dict, Optional +from typing import Any, Optional import pytest @@ -24,7 +24,7 @@ class AccessTokenModel(AccessTokenProtocol[IDType]): class AccessTokenDatabaseMock(AccessTokenDatabase[AccessTokenModel]): - store: Dict[str, AccessTokenModel] + store: dict[str, AccessTokenModel] def __init__(self): self.store = {} @@ -41,13 +41,13 @@ async def get_by_token( else: return access_token - async def create(self, create_dict: Dict[str, Any]) -> AccessTokenModel: + async def create(self, create_dict: dict[str, Any]) -> AccessTokenModel: access_token = AccessTokenModel(**create_dict) self.store[access_token.token] = access_token return access_token async def update( - self, access_token: AccessTokenModel, update_dict: Dict[str, Any] + self, access_token: AccessTokenModel, update_dict: dict[str, Any] ) -> AccessTokenModel: for field, value in update_dict.items(): setattr(access_token, field, value) diff --git a/tests/test_authentication_strategy_redis.py b/tests/test_authentication_strategy_redis.py index f7c48635..16cd5fe6 100644 --- a/tests/test_authentication_strategy_redis.py +++ b/tests/test_authentication_strategy_redis.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Dict, Optional, Tuple +from typing import Optional import pytest @@ -8,7 +8,7 @@ class RedisMock: - store: Dict[str, Tuple[str, Optional[int]]] + store: dict[str, tuple[str, Optional[int]]] def __init__(self): self.store = {} diff --git a/tests/test_fastapi_users.py b/tests/test_fastapi_users.py index a42aa78a..32126525 100644 --- a/tests/test_fastapi_users.py +++ b/tests/test_fastapi_users.py @@ -1,4 +1,5 @@ -from typing import AsyncGenerator, Optional +from collections.abc import AsyncGenerator +from typing import Optional import httpx import pytest diff --git a/tests/test_router_auth.py b/tests/test_router_auth.py index 8e648b57..139d40e4 100644 --- a/tests/test_router_auth.py +++ b/tests/test_router_auth.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncGenerator, Dict, Tuple, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import pytest @@ -45,7 +46,7 @@ def _app_factory(requires_verification: bool) -> FastAPI: ) async def test_app_client( request, get_test_client, app_factory -) -> AsyncGenerator[Tuple[httpx.AsyncClient, bool], None]: +) -> AsyncGenerator[tuple[httpx.AsyncClient, bool], None]: requires_verification = request.param app = app_factory(requires_verification) @@ -60,7 +61,7 @@ class TestLogin: async def test_empty_body( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, ): client, _ = test_app_client @@ -71,7 +72,7 @@ async def test_empty_body( async def test_missing_username( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, ): client, _ = test_app_client @@ -83,7 +84,7 @@ async def test_missing_username( async def test_missing_password( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, ): client, _ = test_app_client @@ -95,28 +96,28 @@ async def test_missing_password( async def test_not_existing_user( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, ): client, _ = test_app_client data = {"username": "lancelot@camelot.bt", "password": "guinevere"} response = await client.post(path, data=data) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS assert user_manager.on_after_login.called is False async def test_wrong_password( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, ): client, _ = test_app_client data = {"username": "king.arthur@camelot.bt", "password": "percival"} response = await client.post(path, data=data) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS assert user_manager.on_after_login.called is False @@ -127,7 +128,7 @@ async def test_valid_credentials_unverified( self, path, email, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, user: UserModel, ): @@ -136,7 +137,7 @@ async def test_valid_credentials_unverified( response = await client.post(path, data=data) if requires_verification: assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.LOGIN_USER_NOT_VERIFIED assert user_manager.on_after_login.called is False else: @@ -152,7 +153,7 @@ async def test_valid_credentials_verified( self, path, email, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, verified_user: UserModel, ): @@ -172,14 +173,14 @@ async def test_valid_credentials_verified( async def test_inactive_user( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user_manager, ): client, _ = test_app_client data = {"username": "percival@camelot.bt", "password": "angharad"} response = await client.post(path, data=data) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS assert user_manager.on_after_login.called is False @@ -191,7 +192,7 @@ class TestLogout: async def test_missing_token( self, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], ): client, _ = test_app_client response = await client.post(path) @@ -201,7 +202,7 @@ async def test_valid_credentials_unverified( self, mocker, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -217,7 +218,7 @@ async def test_valid_credentials_verified( self, mocker, path, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client diff --git a/tests/test_router_oauth.py b/tests/test_router_oauth.py index 50ed3ad1..d67de146 100644 --- a/tests/test_router_oauth.py +++ b/tests/test_router_oauth.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, cast +from typing import Any, cast import httpx import pytest @@ -184,7 +184,7 @@ async def test_already_exists_error( assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.OAUTH_USER_ALREADY_EXISTS assert user_manager_oauth.on_after_login.called is False @@ -214,7 +214,7 @@ async def test_active_user( assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["access_token"] == str(user_oauth.id) assert user_manager_oauth.on_after_login.called is True @@ -278,7 +278,7 @@ async def test_redirect_url_router( "CODE", "http://www.tintagel.bt/callback", None ) - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["access_token"] == str(user_oauth.id) assert user_manager_oauth.on_after_login.called is True @@ -486,7 +486,7 @@ async def test_active_user( assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(user_oauth.id) async def test_redirect_url_router( @@ -521,7 +521,7 @@ async def test_redirect_url_router( "CODE", "http://www.tintagel.bt/callback", None ) - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(user_oauth.id) async def test_not_available_email( diff --git a/tests/test_router_register.py b/tests/test_router_register.py index 50f0f61b..b6bb1c4b 100644 --- a/tests/test_router_register.py +++ b/tests/test_router_register.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncGenerator, Dict, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import pytest @@ -52,7 +53,7 @@ async def test_invalid_password(self, test_app_client: httpx.AsyncClient): json = {"email": "king.arthur@camelot.bt", "password": "g"} response = await test_app_client.post("/register", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == { "code": ErrorCode.REGISTER_INVALID_PASSWORD, "reason": "Password should be at least 3 characters", @@ -65,7 +66,7 @@ async def test_existing_user(self, email, test_app_client: httpx.AsyncClient): json = {"email": email, "password": "guinevere"} response = await test_app_client.post("/register", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS @pytest.mark.parametrize("email", ["lancelot@camelot.bt", "Lancelot@camelot.bt"]) @@ -74,7 +75,7 @@ async def test_valid_body(self, email, test_app_client: httpx.AsyncClient): response = await test_app_client.post("/register", json=json) assert response.status_code == status.HTTP_201_CREATED - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert "hashed_password" not in data assert "password" not in data assert data["id"] is not None @@ -88,7 +89,7 @@ async def test_valid_body_is_superuser(self, test_app_client: httpx.AsyncClient) response = await test_app_client.post("/register", json=json) assert response.status_code == status.HTTP_201_CREATED - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_superuser"] is False async def test_valid_body_is_active(self, test_app_client: httpx.AsyncClient): @@ -100,7 +101,7 @@ async def test_valid_body_is_active(self, test_app_client: httpx.AsyncClient): response = await test_app_client.post("/register", json=json) assert response.status_code == status.HTTP_201_CREATED - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_active"] is True diff --git a/tests/test_router_reset.py b/tests/test_router_reset.py index 55c8b453..851ba1cb 100644 --- a/tests/test_router_reset.py +++ b/tests/test_router_reset.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncGenerator, Dict, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import pytest @@ -106,7 +107,7 @@ async def test_invalid_token( json = {"token": "foo", "password": "guinevere"} response = await test_app_client.post("/reset-password", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN async def test_inactive_user( @@ -118,7 +119,7 @@ async def test_inactive_user( json = {"token": "foo", "password": "guinevere"} response = await test_app_client.post("/reset-password", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN async def test_invalid_password( @@ -132,7 +133,7 @@ async def test_invalid_password( json = {"token": "foo", "password": "guinevere"} response = await test_app_client.post("/reset-password", json=json) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == { "code": ErrorCode.RESET_PASSWORD_INVALID_PASSWORD, "reason": "Invalid", diff --git a/tests/test_router_users.py b/tests/test_router_users.py index b6427c50..78ae6438 100644 --- a/tests/test_router_users.py +++ b/tests/test_router_users.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncGenerator, Dict, Tuple, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import pytest @@ -39,7 +40,7 @@ def _app_factory(requires_verification: bool) -> FastAPI: ) async def test_app_client( request, get_test_client, app_factory -) -> AsyncGenerator[Tuple[httpx.AsyncClient, bool], None]: +) -> AsyncGenerator[tuple[httpx.AsyncClient, bool], None]: requires_verification = request.param app = app_factory(requires_verification) @@ -50,14 +51,14 @@ async def test_app_client( @pytest.mark.router @pytest.mark.asyncio class TestMe: - async def test_missing_token(self, test_app_client: Tuple[httpx.AsyncClient, bool]): + async def test_missing_token(self, test_app_client: tuple[httpx.AsyncClient, bool]): client, _ = test_app_client response = await client.get("/me") assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_inactive_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], inactive_user: UserModel, ): client, _ = test_app_client @@ -68,7 +69,7 @@ async def test_inactive_user( async def test_active_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -79,13 +80,13 @@ async def test_active_user( assert response.status_code == status.HTTP_403_FORBIDDEN else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(user.id) assert data["email"] == user.email async def test_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -93,7 +94,7 @@ async def test_verified_user( "/me", headers={"Authorization": f"Bearer {verified_user.id}"} ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(verified_user.id) assert data["email"] == verified_user.email @@ -106,7 +107,7 @@ async def test_current_user_namespace(self, app_factory): class TestUpdateMe: async def test_missing_token( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], ): client, _ = test_app_client response = await client.patch("/me") @@ -114,7 +115,7 @@ async def test_missing_token( async def test_inactive_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], inactive_user: UserModel, ): client, _ = test_app_client @@ -125,7 +126,7 @@ async def test_inactive_user( async def test_existing_email( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_user: UserModel, ): @@ -139,12 +140,12 @@ async def test_existing_email( assert response.status_code == status.HTTP_403_FORBIDDEN else: assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS async def test_invalid_password( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -157,7 +158,7 @@ async def test_invalid_password( assert response.status_code == status.HTTP_403_FORBIDDEN else: assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == { "code": ErrorCode.UPDATE_USER_INVALID_PASSWORD, "reason": "Password should be at least 3 characters", @@ -165,7 +166,7 @@ async def test_invalid_password( async def test_empty_body( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -177,12 +178,12 @@ async def test_empty_body( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == user.email async def test_valid_body( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -195,12 +196,12 @@ async def test_valid_body( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == "king.arthur@tintagel.bt" async def test_unverified_after_email_change( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -210,12 +211,12 @@ async def test_unverified_after_email_change( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_verified"] is False async def test_valid_body_is_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -228,12 +229,12 @@ async def test_valid_body_is_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_superuser"] is False async def test_valid_body_is_active( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -246,12 +247,12 @@ async def test_valid_body_is_active( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_active"] is True async def test_valid_body_is_verified( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -264,14 +265,14 @@ async def test_valid_body_is_verified( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_verified"] is False async def test_valid_body_password( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -293,7 +294,7 @@ async def test_valid_body_password( async def test_empty_body_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -302,12 +303,12 @@ async def test_empty_body_verified_user( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == verified_user.email async def test_valid_body_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -317,12 +318,12 @@ async def test_valid_body_verified_user( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == "king.arthur@tintagel.bt" async def test_valid_body_is_superuser_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -332,12 +333,12 @@ async def test_valid_body_is_superuser_verified_user( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_superuser"] is False async def test_valid_body_is_active_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -347,12 +348,12 @@ async def test_valid_body_is_active_verified_user( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_active"] is True async def test_valid_body_is_verified_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -362,14 +363,14 @@ async def test_valid_body_is_verified_verified_user( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_verified"] is True async def test_valid_body_password_verified_user( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -390,14 +391,14 @@ async def test_valid_body_password_verified_user( @pytest.mark.router @pytest.mark.asyncio class TestGetUser: - async def test_missing_token(self, test_app_client: Tuple[httpx.AsyncClient, bool]): + async def test_missing_token(self, test_app_client: tuple[httpx.AsyncClient, bool]): client, _ = test_app_client response = await client.get("/d35d213e-f3d8-4f08-954a-7e0d1bea286f") assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_regular_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -410,7 +411,7 @@ async def test_regular_user( async def test_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -422,7 +423,7 @@ async def test_verified_user( async def test_not_existing_user_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], superuser: UserModel, ): client, requires_verification = test_app_client @@ -437,7 +438,7 @@ async def test_not_existing_user_unverified_superuser( async def test_not_existing_user_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_superuser: UserModel, ): client, _ = test_app_client @@ -449,7 +450,7 @@ async def test_not_existing_user_verified_superuser( async def test_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -462,13 +463,13 @@ async def test_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(user.id) assert "hashed_password" not in data async def test_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -478,7 +479,7 @@ async def test_verified_superuser( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(user.id) assert "hashed_password" not in data @@ -489,14 +490,14 @@ async def test_get_user_namespace(self, app_factory, user: UserModel): @pytest.mark.router @pytest.mark.asyncio class TestUpdateUser: - async def test_missing_token(self, test_app_client: Tuple[httpx.AsyncClient, bool]): + async def test_missing_token(self, test_app_client: tuple[httpx.AsyncClient, bool]): client, _ = test_app_client response = await client.patch("/d35d213e-f3d8-4f08-954a-7e0d1bea286f") assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_regular_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -509,7 +510,7 @@ async def test_regular_user( async def test_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -521,7 +522,7 @@ async def test_verified_user( async def test_not_existing_user_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], superuser: UserModel, ): client, requires_verification = test_app_client @@ -537,7 +538,7 @@ async def test_not_existing_user_unverified_superuser( async def test_not_existing_user_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_superuser: UserModel, ): client, _ = test_app_client @@ -550,7 +551,7 @@ async def test_not_existing_user_verified_superuser( async def test_empty_body_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -563,12 +564,12 @@ async def test_empty_body_unverified_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == user.email async def test_empty_body_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -580,12 +581,12 @@ async def test_empty_body_verified_superuser( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == user.email async def test_valid_body_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -601,12 +602,12 @@ async def test_valid_body_unverified_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == "king.arthur@tintagel.bt" async def test_existing_email_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_user: UserModel, verified_superuser: UserModel, @@ -618,12 +619,12 @@ async def test_existing_email_verified_superuser( headers={"Authorization": f"Bearer {verified_superuser.id}"}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.UPDATE_USER_EMAIL_ALREADY_EXISTS async def test_invalid_password_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -634,7 +635,7 @@ async def test_invalid_password_verified_superuser( headers={"Authorization": f"Bearer {verified_superuser.id}"}, ) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == { "code": ErrorCode.UPDATE_USER_INVALID_PASSWORD, "reason": "Password should be at least 3 characters", @@ -642,7 +643,7 @@ async def test_invalid_password_verified_superuser( async def test_valid_body_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -655,12 +656,12 @@ async def test_valid_body_verified_superuser( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["email"] == "king.arthur@tintagel.bt" async def test_valid_body_is_superuser_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -676,12 +677,12 @@ async def test_valid_body_is_superuser_unverified_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_superuser"] is True async def test_valid_body_is_superuser_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -694,12 +695,12 @@ async def test_valid_body_is_superuser_verified_superuser( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_superuser"] is True async def test_valid_body_is_active_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -715,12 +716,12 @@ async def test_valid_body_is_active_unverified_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_active"] is False async def test_valid_body_is_active_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -733,12 +734,12 @@ async def test_valid_body_is_active_verified_superuser( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_active"] is False async def test_valid_body_is_verified_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -754,12 +755,12 @@ async def test_valid_body_is_verified_unverified_superuser( else: assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_verified"] is True async def test_valid_body_is_verified_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -772,14 +773,14 @@ async def test_valid_body_is_verified_verified_superuser( ) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["is_verified"] is True async def test_valid_body_password_unverified_superuser( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -806,7 +807,7 @@ async def test_valid_body_password_verified_superuser( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): @@ -830,7 +831,7 @@ async def test_valid_body_password_unchanged_unverified_superuser( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -857,14 +858,14 @@ async def test_valid_body_password_unchanged_unverified_superuser( @pytest.mark.router @pytest.mark.asyncio class TestDeleteUser: - async def test_missing_token(self, test_app_client: Tuple[httpx.AsyncClient, bool]): + async def test_missing_token(self, test_app_client: tuple[httpx.AsyncClient, bool]): client, _ = test_app_client response = await client.delete("/d35d213e-f3d8-4f08-954a-7e0d1bea286f") assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_regular_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, ): client, requires_verification = test_app_client @@ -877,7 +878,7 @@ async def test_regular_user( async def test_verified_user( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_user: UserModel, ): client, _ = test_app_client @@ -889,7 +890,7 @@ async def test_verified_user( async def test_not_existing_user_unverified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], superuser: UserModel, ): client, requires_verification = test_app_client @@ -904,7 +905,7 @@ async def test_not_existing_user_unverified_superuser( async def test_not_existing_user_verified_superuser( self, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], verified_superuser: UserModel, ): client, _ = test_app_client @@ -918,7 +919,7 @@ async def test_unverified_superuser( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, superuser: UserModel, ): @@ -942,7 +943,7 @@ async def test_verified_superuser( self, mocker, mock_user_db, - test_app_client: Tuple[httpx.AsyncClient, bool], + test_app_client: tuple[httpx.AsyncClient, bool], user: UserModel, verified_superuser: UserModel, ): diff --git a/tests/test_router_verify.py b/tests/test_router_verify.py index e798e84a..ef8b8034 100644 --- a/tests/test_router_verify.py +++ b/tests/test_router_verify.py @@ -1,4 +1,5 @@ -from typing import Any, AsyncGenerator, Dict, cast +from collections.abc import AsyncGenerator +from typing import Any, cast import httpx import pytest @@ -136,7 +137,7 @@ async def test_invalid_verify_token( response = await test_app_client.post("/verify", json={"token": "foo"}) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN async def test_user_not_exists( @@ -148,7 +149,7 @@ async def test_user_not_exists( response = await test_app_client.post("/verify", json={"token": "foo"}) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_BAD_TOKEN async def test_user_already_verified( @@ -160,7 +161,7 @@ async def test_user_already_verified( response = await test_app_client.post("/verify", json={"token": "foo"}) assert response.status_code == status.HTTP_400_BAD_REQUEST - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["detail"] == ErrorCode.VERIFY_USER_ALREADY_VERIFIED async def test_success( @@ -174,7 +175,7 @@ async def test_success( response = await test_app_client.post("/verify", json={"token": "foo"}) assert response.status_code == status.HTTP_200_OK - data = cast(Dict[str, Any], response.json()) + data = cast(dict[str, Any], response.json()) assert data["id"] == str(user.id) async def test_verify_namespace( From 1e163804e261dc79bd69808e53763bb277ee55c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 12:52:25 +0000 Subject: [PATCH 09/25] Enable Python 3.13 support --- .github/workflows/build.yml | 4 ++-- pyproject.toml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af1e4999..e584dd42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python_version: [3.9, '3.10', '3.11', '3.12'] + python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v3 @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python_version: [3.9, '3.10', '3.11', '3.12'] + python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v3 diff --git a/pyproject.toml b/pyproject.toml index 55f5b7c8..6d5ec89b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Internet :: WWW/HTTP :: Session", ] From ad99e4d66aada7865b3d1573570f3d025a65956a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 12:56:54 +0000 Subject: [PATCH 10/25] Fix depreciation warnings --- pyproject.toml | 1 + tests/conftest.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6d5ec89b..03b3539d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ ignore_missing_imports = true [tool.pytest.ini_options] asyncio_mode = "auto" addopts = "--ignore=test_build.py" +asyncio_default_fixture_loop_scope = "session" markers = [ "authentication", "db", diff --git a/tests/conftest.py b/tests/conftest.py index d780fc27..d7e47ecf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -546,7 +546,7 @@ def get_test_client(): async def _get_test_client(app: FastAPI) -> AsyncGenerator[httpx.AsyncClient, None]: async with LifespanManager(app): async with httpx.AsyncClient( - app=app, base_url="http://app.io" + transport=httpx.ASGITransport(app), base_url="http://app.io" ) as test_client: yield test_client From 0b1d740f0087961435483176a9d980b10ec7d901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 13:01:33 +0000 Subject: [PATCH 11/25] Bump dependencies --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 03b3539d..afdf009a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,10 +144,10 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "fastapi >=0.65.2", - "pwdlib[argon2,bcrypt] ==0.2.0", + "pwdlib[argon2,bcrypt] ==0.2.1", "email-validator >=1.1.0,<2.3", - "pyjwt[crypto] ==2.8.0", - "python-multipart ==0.0.9", + "pyjwt[crypto] ==2.9.0", + "python-multipart ==0.0.16", "makefun >=1.11.2,<2.0.0", ] From 55adea47bc7366b4881320d05a3b110c852f642a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 13:04:09 +0000 Subject: [PATCH 12/25] Add Dependabot for GitHub Actions --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7e04fad1..b10baaf6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,7 @@ updates: open-pull-requests-limit: 10 reviewers: - frankie567 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 21a2804c73c2ef648f8c4e37ae803e0bae06a101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:04:50 +0000 Subject: [PATCH 13/25] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 6 +++--- .github/workflows/documentation.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e584dd42..ab5539e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Install dependencies @@ -33,7 +33,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Install dependencies @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 51f638d3..99f4e3f4 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 - name: Install dependencies From d9cbeeb43c0bf67b0e681f9416fd66502ea8458c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:04:54 +0000 Subject: [PATCH 14/25] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 6 +++--- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/documentation.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab5539e3..295b42e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: @@ -31,7 +31,7 @@ jobs: python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: @@ -60,7 +60,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0f71fea1..d07be692 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 99f4e3f4..58e83afd 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: From 55285d1e083e3c5e4231dac5a5480190bb7154ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:04:52 +0000 Subject: [PATCH 15/25] Bump github/codeql-action from 2 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v2...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d07be692..8e2bbfbd 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 9f4a1ea15b74c4abadf70fb450700eb14de98182 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:05:01 +0000 Subject: [PATCH 16/25] Bump python-multipart from 0.0.16 to 0.0.17 Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.16 to 0.0.17. - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.16...0.0.17) --- updated-dependencies: - dependency-name: python-multipart dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index afdf009a..31822c53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,7 @@ dependencies = [ "pwdlib[argon2,bcrypt] ==0.2.1", "email-validator >=1.1.0,<2.3", "pyjwt[crypto] ==2.9.0", - "python-multipart ==0.0.16", + "python-multipart ==0.0.17", "makefun >=1.11.2,<2.0.0", ] From 38fe6cd530405ba1e383f426962d1eb568546adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sun, 3 Nov 2024 13:16:05 +0000 Subject: [PATCH 17/25] =?UTF-8?q?Bump=20version=2013.0.0=20=E2=86=92=2014.?= =?UTF-8?q?0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Breaking changes ---------------- * Drop Python 3.8 support Improvements ------------ * Bump dependencies: * `python-multipart ==0.0.17` * `pwdlib[argon2,bcrypt] ==0.2.1` * `pyjwt[crypto] ==2.9.0` --- fastapi_users/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_users/__init__.py b/fastapi_users/__init__.py index c0aab03c..f603a179 100644 --- a/fastapi_users/__init__.py +++ b/fastapi_users/__init__.py @@ -1,6 +1,6 @@ """Ready-to-use and customizable users management for FastAPI.""" -__version__ = "13.0.0" +__version__ = "14.0.0" from fastapi_users import models, schemas # noqa: F401 from fastapi_users.exceptions import InvalidID, InvalidPasswordException From 514e5bab401f968baca988e09c4995e98da6c7da Mon Sep 17 00:00:00 2001 From: Nima Xin Date: Thu, 7 Nov 2024 16:49:31 +0330 Subject: [PATCH 18/25] Fix database URL examples in docs --- docs/configuration/databases/sqlalchemy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/databases/sqlalchemy.md b/docs/configuration/databases/sqlalchemy.md index 77cd0dad..14e4bc7c 100644 --- a/docs/configuration/databases/sqlalchemy.md +++ b/docs/configuration/databases/sqlalchemy.md @@ -11,8 +11,8 @@ To work with your DBMS, you'll need to install the corresponding asyncio driver. Examples of `DB_URL`s are: -* PostgreSQL: `engine = create_engine('postgresql+asyncpg://user:password@host:port/name')` -* SQLite: `engine = create_engine('sqlite+aiosqlite:///name.db')` +* PostgreSQL: `postgresql+asyncpg://user:password@host:port/name` +* SQLite: `sqlite+aiosqlite:///name.db` For the sake of this tutorial from now on, we'll use a simple SQLite database. From d82e35bcd5f3094096a79ae724225104c17bfde8 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:53:30 +0000 Subject: [PATCH 19/25] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 85bc3338..f1608701 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [![PyPI version](https://badge.fury.io/py/fastapi-users.svg)](https://badge.fury.io/py/fastapi-users) [![Downloads](https://pepy.tech/badge/fastapi-users)](https://pepy.tech/project/fastapi-users) -[![All Contributors](https://img.shields.io/badge/all_contributors-80-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-81-orange.svg?style=flat-square)](#contributors-)

@@ -183,6 +183,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d raindata5
raindata5

📖 Mark Donnelly
Mark Donnelly

📖 Alexander Zinov
Alexander Zinov

đŸ’ģ + nimaxin
nimaxin

📖 From a881996f927cb3d424a21d2eeef5f1361a21131d Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:53:31 +0000 Subject: [PATCH 20/25] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.all-contributorsrc b/.all-contributorsrc index 4066cf1a..920228b3 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -750,6 +750,15 @@ "contributions": [ "code" ] + }, + { + "login": "nimaxin", + "name": "nimaxin", + "avatar_url": "https://avatars.githubusercontent.com/u/97331299?v=4", + "profile": "https://github.com/nimaxin", + "contributions": [ + "doc" + ] } ], "contributorsPerLine": 7, From 9c24c684e6daa280ca74cf440b948416c8a94f8d Mon Sep 17 00:00:00 2001 From: cyberksh <125770189+cyberksh@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:34:53 +0530 Subject: [PATCH 21/25] config: update repo name in mkdocs.yml --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 56344532..9e61c6d4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,7 @@ theme: - search.highlight - content.code.annotate -repo_name: frankie567/fastapi-users +repo_name: fastapi-users/fastapi-users repo_url: https://github.com/fastapi-users/fastapi-users edit_uri: "" From 005dbefa20829b754d2ce4180a6a655f9c2ffdcd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:23:50 +0000 Subject: [PATCH 22/25] Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 295b42e8..b660cfd4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: - name: Test run: | hatch run test:test-cov-xml - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true From d1b52a2b866b461b100fa152c77834ff337f05c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 04:25:51 +0000 Subject: [PATCH 23/25] Bump python-multipart from 0.0.17 to 0.0.20 Bumps [python-multipart](https://github.com/Kludex/python-multipart) from 0.0.17 to 0.0.20. - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.17...0.0.20) --- updated-dependencies: - dependency-name: python-multipart dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 31822c53..48774318 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,7 +147,7 @@ dependencies = [ "pwdlib[argon2,bcrypt] ==0.2.1", "email-validator >=1.1.0,<2.3", "pyjwt[crypto] ==2.9.0", - "python-multipart ==0.0.17", + "python-multipart ==0.0.20", "makefun >=1.11.2,<2.0.0", ] From 8ea78fd49b8be36f42675c425bbb8e61dd2707e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sat, 4 Jan 2025 14:13:38 +0100 Subject: [PATCH 24/25] Bump dependencies --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48774318..d346869f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,17 +146,17 @@ dependencies = [ "fastapi >=0.65.2", "pwdlib[argon2,bcrypt] ==0.2.1", "email-validator >=1.1.0,<2.3", - "pyjwt[crypto] ==2.9.0", + "pyjwt[crypto] ==2.10.1", "python-multipart ==0.0.20", "makefun >=1.11.2,<2.0.0", ] [project.optional-dependencies] sqlalchemy = [ - "fastapi-users-db-sqlalchemy >=6.0.0", + "fastapi-users-db-sqlalchemy >=7.0.0", ] beanie = [ - "fastapi-users-db-beanie >=3.0.0", + "fastapi-users-db-beanie >=4.0.0", ] oauth = [ "httpx-oauth >=0.13" From 9d78b2a35dc7f35c2ffca67232c11f4d27a5db00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Sat, 4 Jan 2025 14:16:19 +0100 Subject: [PATCH 25/25] =?UTF-8?q?Bump=20version=2014.0.0=20=E2=86=92=2014.?= =?UTF-8?q?0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improvements ------------ * Bump dependencies * `pyjwt[crypto] ==2.10.1` * `python-multipart ==0.0.20` --- fastapi_users/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi_users/__init__.py b/fastapi_users/__init__.py index f603a179..0c213994 100644 --- a/fastapi_users/__init__.py +++ b/fastapi_users/__init__.py @@ -1,6 +1,6 @@ """Ready-to-use and customizable users management for FastAPI.""" -__version__ = "14.0.0" +__version__ = "14.0.1" from fastapi_users import models, schemas # noqa: F401 from fastapi_users.exceptions import InvalidID, InvalidPasswordException