Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -61,3 +61,25 @@ jobs:
path: htmlcov/
retention-days: 30

# Exercise the ORM adapters (Tortoise/Peewee) on the newest supported
# Python. The main matrix only installs the [test] extra, so adapter
# tests are skipped there — this job makes sure they actually run.
test-extras:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[test,tortoise,peewee]"

- name: Run tests
run: |
pytest tests/ -v --cov=fastapi_viewsets --cov-report=term-missing --cov-fail-under=70

49 changes: 49 additions & 0 deletions RELEASE_1.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Release v1.4.0

## ✨ What's New

Official support for **Python 3.14**, plus dependency bounds that keep the
optional ORM adapters on known-compatible major versions.

## 🐍 Python 3.14 Support

- `pyproject.toml` now declares `Programming Language :: Python :: 3.14`, so
PyPI and the version badge reflect the new interpreter.
- The CI test matrix runs on Python 3.9 – 3.14.
- `black` `target-version` extended with `py314`.
- The full test suite (263 tests) passes on Python 3.14.3 with current
dependency versions: FastAPI 0.141, Pydantic 2.12, SQLAlchemy 2.0.52.

## 📦 Dependency Bounds

- `tortoise` extra: `tortoise-orm>=0.20.0,<1.0` — Tortoise ORM 1.x introduces
breaking API changes (`TortoiseContext`, removal of implicit global state)
that are not compatible with this adapter yet.
- `peewee` extra: `peewee>=3.17.0,<4` — Peewee 4.x changes exception
behaviour of `ModelSelect.get()` (raw `IndexError` instead of
`DoesNotExist`), which breaks adapters and tests.
- Core dependencies are unchanged (`fastapi>=0.110`, `pydantic>=2.5,<3`,
`SQLAlchemy>=1.4.36`) — current releases of all of them already support
Python 3.14.

## 🧪 Testing

- Fixed previously broken Tortoise adapter tests: models are now properly
registered through `models=["tests.tortoise_models"]` instead of relying
on unregistered locally-defined models, and schemas are generated with
`Tortoise.generate_schemas(safe=True)`.
- Fixed the Peewee `delete_element` test to expect `DoesNotExist` after
deletion instead of a `None` return from `get_by_id()`.
- New CI job `test-extras` runs the suite with the `tortoise` and `peewee`
extras installed on Python 3.14, so adapter tests are no longer skipped
in CI.

## 📦 Installation

```bash
pip install fastapi-viewsets
```

---

**Full Changelog**: https://github.com/svalench/fastapi_viewsets/compare/v1.3.0...v1.4.0
13 changes: 13 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Release Notes

## Version 1.4.0

### 🐍 Python 3.14

- Official Python 3.14 support: new PyPI classifier, CI matrix extended to
3.9 – 3.14, and the full test suite verified on 3.14.3.
- Dependency bounds for optional adapters: `tortoise-orm>=0.20,<1.0` and
`peewee>=3.17,<4` (Tortoise 1.x and Peewee 4.x contain breaking changes).
- Fixed Tortoise/Peewee adapter tests that were silently skipped or broken
in CI, and added a `test-extras` CI job that exercises them on 3.14.

Details: [RELEASE_1.4.0.md](RELEASE_1.4.0.md).

## Version 1.3.0

### ✨ Highlights
Expand Down
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "fastapi_viewsets"
version = "1.3.0"
version = "1.4.0"
description = "DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support."
readme = "README.md"
license = { text = "MIT" }
Expand All @@ -18,6 +18,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Framework :: FastAPI",
Expand All @@ -34,8 +35,8 @@ dependencies = [

[project.optional-dependencies]
sqlalchemy = ["SQLAlchemy>=1.4.36"]
tortoise = ["tortoise-orm>=0.20.0", "asyncpg>=0.28.0"]
peewee = ["peewee>=3.17.0"]
tortoise = ["tortoise-orm>=0.20.0,<1.0", "asyncpg>=0.28.0"]
peewee = ["peewee>=3.17.0,<4"]
test = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
Expand All @@ -60,7 +61,7 @@ exclude = ["tests*"]

[tool.black]
line-length = 100
target-version = ["py39", "py310", "py311", "py312", "py313"]
target-version = ["py39", "py310", "py311", "py312", "py313", "py314"]

[tool.ruff]
line-length = 100
Expand Down
53 changes: 20 additions & 33 deletions tests/test_adapter_methods_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,21 +186,18 @@ async def test_tortoise_adapter_get_list_queryset_with_offset(self):
"""Test Tortoise adapter get_list_queryset_async with offset."""
try:
from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
from tortoise.models import Model
from tortoise import fields

from tests.tortoise_models import SimpleTortoiseModel as TestModel

adapter = TortoiseAdapter(
database_url="sqlite:///test_tortoise_offset.db",
models=[],
database_url="sqlite://:memory:",
models=["tests.tortoise_models"],
app_label="test"
)

class TestModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50)

# Initialize

# Initialize and create the table
from tortoise import Tortoise
await adapter._ensure_initialized()
await Tortoise.generate_schemas(safe=True)

# Create test data
await TestModel.create(name="test1")
Expand Down Expand Up @@ -228,20 +225,14 @@ async def test_tortoise_adapter_create_element_validation(self):
"""Test Tortoise adapter create_element_async validation."""
try:
from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
from tortoise.models import Model
from tortoise import fields

from tests.tortoise_models import SimpleTortoiseModel as TestModel

adapter = TortoiseAdapter(
database_url="sqlite:///test_tortoise_validation.db",
models=[],
database_url="sqlite://:memory:",
models=["tests.tortoise_models"],
app_label="test"
)

class TestModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50, required=True)
value = fields.IntField(null=True)


await adapter._ensure_initialized()

# Test missing required field
Expand All @@ -266,21 +257,17 @@ async def test_tortoise_adapter_update_element_put(self):
"""Test Tortoise adapter update_element_async with PUT."""
try:
from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
from tortoise.models import Model
from tortoise import fields

from tests.tortoise_models import SimpleTortoiseModel as TestModel

adapter = TortoiseAdapter(
database_url="sqlite:///test_tortoise_put.db",
models=[],
database_url="sqlite://:memory:",
models=["tests.tortoise_models"],
app_label="test"
)

class TestModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50)
value = fields.IntField(null=True)


from tortoise import Tortoise
await adapter._ensure_initialized()
await Tortoise.generate_schemas(safe=True)

# Create test data
obj = await TestModel.create(name="old", value=10)
Expand Down
38 changes: 13 additions & 25 deletions tests/test_exception_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,21 +280,17 @@ async def test_tortoise_adapter_create_element_integrity_error(self):
"""Test Tortoise adapter create_element_async handles IntegrityError."""
try:
from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
from tortoise.models import Model
from tortoise import fields
from tortoise.exceptions import IntegrityError as TortoiseIntegrityError
from tests.tortoise_models import UniqueNameTortoiseModel as TestModel

adapter = TortoiseAdapter(
database_url="sqlite:///test_tortoise_integrity.db",
models=[],
database_url="sqlite://:memory:",
models=["tests.tortoise_models"],
app_label="test"
)

class TestModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50, unique=True)

from tortoise import Tortoise
await adapter._ensure_initialized()
await Tortoise.generate_schemas(safe=True)

# Create first record
await TestModel.create(name="test1")
Expand Down Expand Up @@ -322,19 +318,14 @@ async def test_tortoise_adapter_create_element_generic_error(self):
"""Test Tortoise adapter create_element_async handles generic Exception."""
try:
from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
from tortoise.models import Model
from tortoise import fields
from tests.tortoise_models import SimpleTortoiseModel as TestModel

adapter = TortoiseAdapter(
database_url="sqlite:///test_tortoise_generic.db",
models=[],
database_url="sqlite://:memory:",
models=["tests.tortoise_models"],
app_label="test"
)

class TestModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50)

await adapter._ensure_initialized()

# Mock create to raise generic exception
Expand All @@ -361,20 +352,17 @@ async def test_tortoise_adapter_update_element_empty_data(self):
"""Test Tortoise adapter update_element_async with empty data."""
try:
from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
from tortoise.models import Model
from tortoise import fields
from tests.tortoise_models import SimpleTortoiseModel as TestModel

adapter = TortoiseAdapter(
database_url="sqlite:///test_tortoise_empty.db",
models=[],
database_url="sqlite://:memory:",
models=["tests.tortoise_models"],
app_label="test"
)

class TestModel(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50)

from tortoise import Tortoise
await adapter._ensure_initialized()
await Tortoise.generate_schemas(safe=True)

# Create test data
obj = await TestModel.create(name="test")
Expand Down
3 changes: 2 additions & 1 deletion tests/test_missing_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ class TestModel(Base):
assert result is True

# Verify it's deleted
assert TestModel.get_by_id(obj_id) is None
with pytest.raises(TestModel.DoesNotExist):
TestModel.get_by_id(obj_id)

# Test with non-existent id
with pytest.raises(HTTPException) as exc_info:
Expand Down
28 changes: 28 additions & 0 deletions tests/tortoise_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Shared Tortoise ORM models for tests.

These models live at module level (as required by Tortoise) and are
referenced by tests via ``models=["tests.tortoise_models"]`` so that
``Tortoise.init()`` can discover and register them properly.

The module is only imported from tests that already know tortoise-orm
is installed; importing it without tortoise-orm raises ``ImportError``,
which those tests translate into a skip.
"""

from tortoise import fields
from tortoise.models import Model


class SimpleTortoiseModel(Model):
"""Model with a required ``name`` and an optional ``value``."""

id = fields.IntField(pk=True)
name = fields.CharField(max_length=50)
value = fields.IntField(null=True)


class UniqueNameTortoiseModel(Model):
"""Model with a unique constraint on ``name`` (for IntegrityError tests)."""

id = fields.IntField(pk=True)
name = fields.CharField(max_length=50, unique=True)
Loading