diff --git a/RELEASE_1.2.1.md b/RELEASE_1.2.1.md new file mode 100644 index 0000000..3922249 --- /dev/null +++ b/RELEASE_1.2.1.md @@ -0,0 +1,40 @@ +# Release 1.2.1 — async driver fallback hotfix + +Bugfix release for v1.2.0. + +## What changed + +### Fixed +- **Crash at adapter init when async DB driver is missing.** Since v1.2.0 the + default `SQLAlchemyAdapter` was always created with an explicit + `async_database_url`, which caused `create_async_engine()` to raise + `ModuleNotFoundError: No module named 'aiosqlite'` (or `asyncpg` / + `aiomysql`) at construction time, breaking purely-synchronous setups. + + Now the adapter degrades gracefully: if the async DB-API driver is not + installed, `async_engine` and `AsyncSessionLocal` fall back to `None` and + `get_async_session()` raises a helpful `RuntimeError` explaining which + package to install. Sync usage continues to work without any extra + dependency. + +### Internal +- Switched packaging in `pyproject.toml` from a hard-coded package list to + `[tool.setuptools.packages.find]`, so any future subpackages are picked up + automatically by the wheel/sdist build. +- Added regression tests covering both the explicit-URL and auto-converted-URL + paths when `aiosqlite` is missing. + +## Upgrade + +```bash +pip install -U fastapi-viewsets==1.2.1 +``` + +No code changes are required. If you want full async support, install the +matching driver: + +```bash +pip install aiosqlite # SQLite +pip install asyncpg # PostgreSQL +pip install aiomysql # MySQL +``` diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ce8c20c..10d9658 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,21 @@ # Release Notes +## Version 1.2.1 + +### 🐛 Bugfix + +- **`SQLAlchemyAdapter` no longer crashes at construction** when the + async DB-API driver (`aiosqlite` / `asyncpg` / `aiomysql`) is missing. + Sync-only setups keep working out of the box; `get_async_session()` + raises a helpful `RuntimeError` lazily if you try to use async without + the driver. Regression introduced in 1.2.0 by always passing an + explicit `async_database_url` from `ORMFactory.get_adapter_from_env`. + +### 📦 Packaging + +- Switched to `[tool.setuptools.packages.find]` so future subpackages are + picked up automatically by wheel/sdist builds. + ## Version 1.2.0 ### ✨ Highlights diff --git a/fastapi_viewsets/orm/sqlalchemy_adapter.py b/fastapi_viewsets/orm/sqlalchemy_adapter.py index efe8e6b..f211e18 100644 --- a/fastapi_viewsets/orm/sqlalchemy_adapter.py +++ b/fastapi_viewsets/orm/sqlalchemy_adapter.py @@ -67,19 +67,25 @@ def __init__( self.Base.query = self.db_session.query_property() - # Setup async engine and session + # Setup async engine and session. + # + # The async engine is best-effort: if the matching async DB-API + # driver (aiosqlite, asyncpg, aiomysql, ...) is not installed we + # fall back to ``async_engine = None`` so that purely synchronous + # usage still works. The error is re-raised lazily from + # :meth:`get_async_session` with installation hints. if async_engine: self.async_engine = async_engine - elif async_database_url: - self.async_engine = create_async_engine(async_database_url, echo=False) else: - # Auto-convert sync URL to async - async_url = self._get_async_database_url() + async_url = async_database_url or self._get_async_database_url() try: self.async_engine = create_async_engine(async_url, echo=False) except Exception: + # Missing async DB-API driver (aiosqlite/asyncpg/aiomysql), + # or no async dialect for this URL — degrade to sync-only mode. + # The error is re-raised lazily from ``get_async_session``. self.async_engine = None - + if self.async_engine: self.AsyncSessionLocal = async_sessionmaker( self.async_engine, diff --git a/pyproject.toml b/pyproject.toml index a327941..82d45e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "fastapi_viewsets" -version = "1.2.0" +version = "1.2.1" description = "DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support." readme = "README.md" license = { text = "MIT" } @@ -51,8 +51,10 @@ Homepage = "https://github.com/svalench/fastapi_viewsets" Issues = "https://github.com/svalench/fastapi_viewsets/issues" Changelog = "https://github.com/svalench/fastapi_viewsets/blob/main/RELEASE_NOTES.md" -[tool.setuptools] -packages = ["fastapi_viewsets", "fastapi_viewsets.orm"] +[tool.setuptools.packages.find] +where = ["."] +include = ["fastapi_viewsets*"] +exclude = ["tests*"] [tool.black] line-length = 100 diff --git a/tests/test_async_driver_missing.py b/tests/test_async_driver_missing.py new file mode 100644 index 0000000..d64eb99 --- /dev/null +++ b/tests/test_async_driver_missing.py @@ -0,0 +1,57 @@ +"""Regression test for v1.2.1. + +When ``async_database_url`` is provided but the corresponding async DB-API +driver (e.g. ``aiosqlite``) is not installed, ``SQLAlchemyAdapter.__init__`` +must degrade gracefully to sync-only mode instead of raising +``ModuleNotFoundError`` at construction time. + +Regression introduced in v1.2.0 where ``ORMFactory.get_adapter_from_env`` +started always passing an explicit ``async_database_url`` to the adapter, +bypassing the existing best-effort ``try/except`` on the URL-conversion path. +""" + +from unittest.mock import patch + +import pytest + +from fastapi_viewsets.orm.sqlalchemy_adapter import SQLAlchemyAdapter + + +def _import_error(*_args, **_kwargs): + raise ModuleNotFoundError("No module named 'aiosqlite'") + + +def test_explicit_async_url_without_driver_falls_back_to_sync(): + """Adapter must not raise when async driver is missing.""" + with patch( + "fastapi_viewsets.orm.sqlalchemy_adapter.create_async_engine", + side_effect=_import_error, + ): + adapter = SQLAlchemyAdapter( + database_url="sqlite:///:memory:", + async_database_url="sqlite+aiosqlite:///:memory:", + ) + + assert adapter.async_engine is None + assert adapter.AsyncSessionLocal is None + + # Sync session must keep working. + session = adapter.get_session() + assert session is not None + session.close() + + # Async session must raise a helpful runtime error (not at __init__). + with pytest.raises(RuntimeError, match="async database driver"): + adapter.get_async_session() + + +def test_auto_converted_async_url_without_driver_falls_back_to_sync(): + """Same fallback when ``async_database_url`` is auto-derived.""" + with patch( + "fastapi_viewsets.orm.sqlalchemy_adapter.create_async_engine", + side_effect=_import_error, + ): + adapter = SQLAlchemyAdapter(database_url="sqlite:///:memory:") + + assert adapter.async_engine is None + assert adapter.AsyncSessionLocal is None