diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 91d300c..d819c8f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: [3.7, 3.8, 3.9, '3.10', '3.11'] + python_version: [3.9, '3.10', '3.11', '3.12', '3.13'] database_url: [ "sqlite+aiosqlite:///./test-fastapiusers.db", @@ -43,9 +43,9 @@ jobs: ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} - name: Install dependencies @@ -61,7 +61,7 @@ jobs: DATABASE_URL: ${{ matrix.database_url }} run: | hatch run test-cov-xml - - uses: codecov/codecov-action@v3 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -78,11 +78,11 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.gitignore b/.gitignore index b949f48..434348e 100644 --- a/.gitignore +++ b/.gitignore @@ -104,9 +104,6 @@ ENV/ # mypy .mypy_cache/ -# .vscode -.vscode/ - # OS files .DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5d8d955 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "python.analysis.typeCheckingMode": "basic", + "python.analysis.autoImportCompletions": true, + "python.terminal.activateEnvironment": true, + "python.terminal.activateEnvInCurrentTerminal": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "editor.rulers": [88], + "python.defaultInterpreterPath": "${workspaceFolder}/.hatch/fastapi-users-db-sqlalchemy/bin/python", + "python.testing.pytestPath": "${workspaceFolder}/.hatch/fastapi-users-db-sqlalchemy/bin/pytest", + "python.testing.cwd": "${workspaceFolder}", + "python.testing.pytestArgs": ["--no-cov"], + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + } + } diff --git a/README.md b/README.md index d9756d4..2f8498c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ [](https://badge.fury.io/py/fastapi-users-db-sqlalchemy) [](https://pepy.tech/project/fastapi-users-db-sqlalchemy)
--- diff --git a/fastapi_users_db_sqlalchemy/__init__.py b/fastapi_users_db_sqlalchemy/__init__.py index 6d4ee82..467a2bf 100644 --- a/fastapi_users_db_sqlalchemy/__init__.py +++ b/fastapi_users_db_sqlalchemy/__init__.py @@ -1,6 +1,7 @@ """FastAPI Users database adapter for SQLAlchemy.""" + import uuid -from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type +from typing import TYPE_CHECKING, Any, Generic, Optional from fastapi_users.db.base import BaseUserDatabase from fastapi_users.models import ID, OAP, UP @@ -11,7 +12,7 @@ from fastapi_users_db_sqlalchemy.generics import GUID -__version__ = "5.0.0" +__version__ = "7.0.0" UUID_ID = uuid.UUID @@ -103,14 +104,14 @@ class SQLAlchemyUserDatabase(Generic[UP, ID], BaseUserDatabase[UP, ID]): """ session: AsyncSession - user_table: Type[UP] - oauth_account_table: Optional[Type[SQLAlchemyBaseOAuthAccountTable]] + user_table: type[UP] + oauth_account_table: Optional[type[SQLAlchemyBaseOAuthAccountTable]] def __init__( self, session: AsyncSession, - user_table: Type[UP], - oauth_account_table: Optional[Type[SQLAlchemyBaseOAuthAccountTable]] = None, + user_table: type[UP], + oauth_account_table: Optional[type[SQLAlchemyBaseOAuthAccountTable]] = None, ): self.session = session self.user_table = user_table @@ -138,24 +139,26 @@ async def get_by_oauth_account(self, oauth: str, account_id: str) -> Optional[UP ) return await self._get_user(statement) - async def create(self, create_dict: Dict[str, Any]) -> UP: + async def create(self, create_dict: dict[str, Any]) -> UP: user = self.user_table(**create_dict) self.session.add(user) await self.session.commit() + await self.session.refresh(user) return user - async def update(self, user: UP, update_dict: Dict[str, Any]) -> UP: + async def update(self, user: UP, update_dict: dict[str, Any]) -> UP: for key, value in update_dict.items(): setattr(user, key, value) self.session.add(user) await self.session.commit() + await self.session.refresh(user) return user async def delete(self, user: UP) -> None: await self.session.delete(user) await self.session.commit() - async def add_oauth_account(self, user: UP, create_dict: Dict[str, Any]) -> UP: + async def add_oauth_account(self, user: UP, create_dict: dict[str, Any]) -> UP: if self.oauth_account_table is None: raise NotImplementedError() @@ -170,7 +173,7 @@ async def add_oauth_account(self, user: UP, create_dict: Dict[str, Any]) -> UP: return user async def update_oauth_account( - self, user: UP, oauth_account: OAP, update_dict: Dict[str, Any] + self, user: UP, oauth_account: OAP, update_dict: dict[str, Any] ) -> UP: if self.oauth_account_table is None: raise NotImplementedError() diff --git a/fastapi_users_db_sqlalchemy/access_token.py b/fastapi_users_db_sqlalchemy/access_token.py index 5878818..9f68af6 100644 --- a/fastapi_users_db_sqlalchemy/access_token.py +++ b/fastapi_users_db_sqlalchemy/access_token.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime -from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type +from typing import TYPE_CHECKING, Any, Generic, Optional from fastapi_users.authentication.strategy.db import AP, AccessTokenDatabase from fastapi_users.models import ID @@ -50,7 +50,7 @@ class SQLAlchemyAccessTokenDatabase(Generic[AP], AccessTokenDatabase[AP]): def __init__( self, session: AsyncSession, - access_token_table: Type[AP], + access_token_table: type[AP], ): self.session = session self.access_token_table = access_token_table @@ -69,17 +69,19 @@ async def get_by_token( results = await self.session.execute(statement) return results.scalar_one_or_none() - async def create(self, create_dict: Dict[str, Any]) -> AP: + async def create(self, create_dict: dict[str, Any]) -> AP: access_token = self.access_token_table(**create_dict) self.session.add(access_token) await self.session.commit() + await self.session.refresh(access_token) return access_token - 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: for key, value in update_dict.items(): setattr(access_token, key, value) self.session.add(access_token) await self.session.commit() + await self.session.refresh(access_token) return access_token async def delete(self, access_token: AP) -> None: diff --git a/pyproject.toml b/pyproject.toml index baeb6dd..ef32d8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,14 @@ plugins = "sqlalchemy.ext.mypy.plugin" [tool.pytest.ini_options] -asyncio_mode = "auto" +asyncio_mode = "strict" +asyncio_default_fixture_loop_scope = "function" addopts = "--ignore=test_build.py" [tool.ruff] -extend-select = ["I"] + +[tool.ruff.lint] +extend-select = ["I", "UP"] [tool.hatch] @@ -19,6 +22,7 @@ commit_extra_args = ["-e"] path = "fastapi_users_db_sqlalchemy/__init__.py" [tool.hatch.envs.default] +installer = "uv" dependencies = [ "aiosqlite", "asyncpg", @@ -40,13 +44,13 @@ dependencies = [ test = "pytest --cov=fastapi_users_db_sqlalchemy/ --cov-report=term-missing --cov-fail-under=100" test-cov-xml = "pytest --cov=fastapi_users_db_sqlalchemy/ --cov-report=xml --cov-fail-under=100" lint = [ - "black . ", - "ruff --fix .", + "ruff format . ", + "ruff check --fix .", "mypy fastapi_users_db_sqlalchemy/", ] lint-check = [ - "black --check .", - "ruff .", + "ruff format --check .", + "ruff check .", "mypy fastapi_users_db_sqlalchemy/", ] @@ -71,15 +75,15 @@ classifiers = [ "Framework :: FastAPI", "Framework :: AsyncIO", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "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", ] -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = [ "fastapi-users >= 10.0.0", "sqlalchemy[asyncio] >=2.0.0,<2.1.0", diff --git a/tests/conftest.py b/tests/conftest.py index 17d3c79..b7661c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,5 @@ -import asyncio import os -from typing import Any, Dict, Optional +from typing import Any, Optional import pytest from fastapi_users import schemas @@ -26,16 +25,8 @@ class UserOAuth(User, schemas.BaseOAuthAccountMixin): pass -@pytest.fixture(scope="session") -def event_loop(): - """Force the pytest-asyncio loop to be the main one.""" - loop = asyncio.new_event_loop() - yield loop - loop.close() - - @pytest.fixture -def oauth_account1() -> Dict[str, Any]: +def oauth_account1() -> dict[str, Any]: return { "oauth_name": "service1", "access_token": "TOKEN", @@ -46,7 +37,7 @@ def oauth_account1() -> Dict[str, Any]: @pytest.fixture -def oauth_account2() -> Dict[str, Any]: +def oauth_account2() -> dict[str, Any]: return { "oauth_name": "service2", "access_token": "TOKEN", diff --git a/tests/test_access_token.py b/tests/test_access_token.py index df149ff..f0e5fb9 100644 --- a/tests/test_access_token.py +++ b/tests/test_access_token.py @@ -1,8 +1,9 @@ import uuid +from collections.abc import AsyncGenerator from datetime import datetime, timedelta, timezone -from typing import AsyncGenerator import pytest +import pytest_asyncio from pydantic import UUID4 from sqlalchemy import exc from sqlalchemy.ext.asyncio import ( @@ -42,7 +43,7 @@ def user_id() -> UUID4: return uuid.uuid4() -@pytest.fixture +@pytest_asyncio.fixture async def sqlalchemy_access_token_db( user_id: UUID4, ) -> AsyncGenerator[SQLAlchemyAccessTokenDatabase[AccessToken], None]: diff --git a/tests/test_users.py b/tests/test_users.py index 141a93f..4a83e39 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -1,6 +1,8 @@ -from typing import Any, AsyncGenerator, Dict, List +from collections.abc import AsyncGenerator +from typing import Any import pytest +import pytest_asyncio from sqlalchemy import String, exc from sqlalchemy.ext.asyncio import ( AsyncEngine, @@ -45,12 +47,12 @@ class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, OAuthBase): class UserOAuth(SQLAlchemyBaseUserTableUUID, OAuthBase): first_name: Mapped[str] = mapped_column(String(255), nullable=True) - oauth_accounts: Mapped[List[OAuthAccount]] = relationship( + oauth_accounts: Mapped[list[OAuthAccount]] = relationship( "OAuthAccount", lazy="joined" ) -@pytest.fixture +@pytest_asyncio.fixture async def sqlalchemy_user_db() -> AsyncGenerator[SQLAlchemyUserDatabase, None]: engine = create_async_engine(DATABASE_URL) sessionmaker = create_async_session_maker(engine) @@ -65,7 +67,7 @@ async def sqlalchemy_user_db() -> AsyncGenerator[SQLAlchemyUserDatabase, None]: await connection.run_sync(Base.metadata.drop_all) -@pytest.fixture +@pytest_asyncio.fixture async def sqlalchemy_user_db_oauth() -> AsyncGenerator[SQLAlchemyUserDatabase, None]: engine = create_async_engine(DATABASE_URL) sessionmaker = create_async_session_maker(engine) @@ -168,8 +170,8 @@ async def test_queries_custom_fields( @pytest.mark.asyncio async def test_queries_oauth( sqlalchemy_user_db_oauth: SQLAlchemyUserDatabase[UserOAuth, UUID_ID], - oauth_account1: Dict[str, Any], - oauth_account2: Dict[str, Any], + oauth_account1: dict[str, Any], + oauth_account2: dict[str, Any], ): user_create = { "email": "lancelot@camelot.bt",