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
20 changes: 16 additions & 4 deletions .github/workflows/publish-manual.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@ jobs:
with:
python-version: '3.11'

- name: Update version in setup.py
if: inputs.version != ''
- name: Verify requested version matches pyproject.toml
env:
REQUESTED_VERSION: ${{ inputs.version }}
run: |
sed -i "s/version='[^']*'/version='${{ inputs.version }}'/" setup.py
python - <<'PY'
import os
import tomllib
from pathlib import Path

requested = os.environ["REQUESTED_VERSION"].removeprefix("v")
actual = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
if requested != actual:
raise SystemExit(
f"Requested version {requested!r} does not match pyproject.toml ({actual}). "
"Update pyproject.toml before publishing."
)
PY

- name: Install build dependencies
run: |
Expand All @@ -47,4 +60,3 @@ jobs:
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: |
twine upload dist/*

20 changes: 16 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,23 @@ jobs:
with:
python-version: '3.11'

- name: Update version in setup.py (manual trigger)
if: github.event_name == 'workflow_dispatch' && inputs.version != ''
- name: Verify requested version matches pyproject.toml
env:
REQUESTED_VERSION: ${{ inputs.version || github.event.release.tag_name }}
run: |
sed -i "s/version='[^']*'/version='${{ inputs.version }}'/" setup.py
python - <<'PY'
import os
import tomllib
from pathlib import Path

requested = os.environ["REQUESTED_VERSION"].removeprefix("v")
actual = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
if requested != actual:
raise SystemExit(
f"Requested version {requested!r} does not match pyproject.toml ({actual}). "
"Update pyproject.toml before publishing."
)
PY

- name: Install build dependencies
run: |
Expand Down Expand Up @@ -65,4 +78,3 @@ jobs:
fi
echo "✅ PyPI token found. Uploading package to PyPI..."
twine upload dist/*

2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
python -m venv /tmp/smoke
/tmp/smoke/bin/pip install --upgrade pip
/tmp/smoke/bin/pip install dist/*.whl
/tmp/smoke/bin/python -c "import fastapi_viewsets, sys; print('imported', fastapi_viewsets.__name__)"
/tmp/smoke/bin/python -I -c "from fastapi_viewsets import BaseViewset, AsyncBaseViewset; print('imported installed wheel')"

- name: Upload distributions
uses: actions/upload-artifact@v4
Expand Down
33 changes: 32 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }}
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-

Expand Down Expand Up @@ -83,3 +83,34 @@ jobs:
run: |
pytest tests/ -v --cov=fastapi_viewsets --cov-report=term-missing --cov-fail-under=70

package:
name: Build and test installed wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Build sdist and wheel
run: |
python -m pip install build twine
python -m build
python -m twine check --strict dist/*

- name: Install wheel with the minimum SQLAlchemy version
run: |
python -m venv .venv
.venv/bin/python -m pip install dist/*.whl pytest pytest-asyncio httpx "SQLAlchemy==2.0.0"
.venv/bin/python -m pip check
.venv/bin/python -I -c "from fastapi_viewsets import BaseViewset, AsyncBaseViewset"

- name: Exercise sync quickstarts without an async driver
run: |
.venv/bin/python -m pytest --noconftest -o addopts= tests/test_quickstarts.py -k "not async"

- name: Exercise all quickstarts with SQLite async support
run: |
.venv/bin/python -m pip install aiosqlite
.venv/bin/python -m pytest --noconftest -o addopts= tests/test_quickstarts.py
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
site/
.venv/
build/
dist/
*.egg-info/
__pycache__/
*.py[cod]
.pytest_cache/
.coverage*
coverage.xml
htmlcov/
3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include pytest.ini mkdocs.yml
recursive-include tests *.py
recursive-include docs *.md *.css
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoint
pip install fastapi-viewsets
```

Optional extras (see `setup.py`):
SQLAlchemy 2.0 or newer is installed automatically. For a local SQLite app,
start with the [sync quickstart](#quickstart-sqlalchemy-sync); no separate database driver is needed.

Optional extras (see `pyproject.toml`):

```bash
pip install "fastapi-viewsets[sqlalchemy]"
Expand Down Expand Up @@ -377,6 +380,9 @@ an async-capable URL and use the lazy helpers from `db_conf`. The
package auto-converts `sqlite://` to `sqlite+aiosqlite://`,
`postgresql://` to `postgresql+asyncpg://`, etc.

Save the following as `main.py` in an empty folder, then run
`uvicorn main:app --reload`. Open `http://127.0.0.1:8000/docs` to try the API.

```python
from contextlib import asynccontextmanager

Expand Down Expand Up @@ -691,9 +697,9 @@ class ItemsWithStats(BaseViewset):
- Internal `register()` deduplicated between sync and async viewsets via a shared mixin.
- PEP 621 `pyproject.toml`, `python_requires>=3.9`, FastAPI `>=0.110`, ruff/black/mypy preconfigured.

Previous release: [v1.1.0](RELEASE_1.1.0.md) introduced multi-ORM support via adapters (SQLAlchemy default, optional Tortoise and Peewee), `ORMFactory` and environment-driven `ORM_TYPE` configuration.
Previous release: [v1.1.0](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.1.0.md) introduced multi-ORM support via adapters (SQLAlchemy default, optional Tortoise and Peewee), `ORMFactory` and environment-driven `ORM_TYPE` configuration.

Details: [RELEASE_NOTES.md](RELEASE_NOTES.md), [RELEASE_1.2.0.md](RELEASE_1.2.0.md), [RELEASE_1.1.0.md](RELEASE_1.1.0.md).
Details: [RELEASE_NOTES.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_NOTES.md), [RELEASE_1.2.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.2.0.md), [RELEASE_1.1.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.1.0.md).

## Roadmap (planned)

Expand All @@ -718,6 +724,7 @@ Released: server-side `search` (v1.5.0), declarative ordering and advanced filte
From the repository root (see `pytest.ini`):

```bash
python -m pip install -e ".[test]"
pytest
```

Expand All @@ -729,7 +736,7 @@ See [open issues](https://github.com/svalench/fastapi_viewsets/issues) to propos

## License

Distributed under the MIT License. See [LICENSE](LICENSE).
Distributed under the MIT License. See [LICENSE](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE).

## Author

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
| Python | >= 3.9 |
| FastAPI | >= 0.110 |
| Pydantic | >= 2.5, < 3 |
| SQLAlchemy | >= 1.4.36 |
| SQLAlchemy | >= 2.0.0 |
| python-dotenv | >= 0.19 |

## Install from PyPI
Expand Down
3 changes: 3 additions & 0 deletions docs/quickstart-async.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ SQLALCHEMY_DATABASE_URL=sqlite:///./test.db

## Full example

Save as `main.py` in an empty folder and run `uvicorn main:app --reload`.
Open `http://127.0.0.1:8000/docs` to try the CRUD endpoints.

```python
from contextlib import asynccontextmanager

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ classifiers = [
dependencies = [
"fastapi>=0.110.0",
"uvicorn>=0.17.6",
"SQLAlchemy>=1.4.36",
"SQLAlchemy>=2.0.0",
"pydantic>=2.5,<3",
"python-dotenv>=0.19.0",
]

[project.optional-dependencies]
sqlalchemy = ["SQLAlchemy>=1.4.36"]
sqlalchemy = ["SQLAlchemy>=2.0.0"]
tortoise = ["tortoise-orm>=0.20.0,<1.0", "asyncpg>=0.28.0"]
peewee = ["peewee>=3.17.0,<4"]
test = [
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[metadata]
description-file = README.md
# Project metadata, including the README, is declared in pyproject.toml.
98 changes: 98 additions & 0 deletions tests/test_quickstarts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Exercise the published examples without substituting test-only app code."""

import os
import re
import subprocess
import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
EXAMPLES = [
("README.md", "Quickstart (SQLAlchemy, sync)"),
("README.md", "Async quickstart (SQLAlchemy 2.x + Pydantic v2)"),
("docs/quickstart-sync.md", "Full example"),
("docs/quickstart-async.md", "Full example"),
("docs/index.md", "30-second example"),
]

CHECK_ENDPOINTS = """
from fastapi.testclient import TestClient

with TestClient(app) as client:
response = client.get("/items")
assert response.status_code == 200, response.text
assert response.json() == []

response = client.post("/items", json={"name": "apple"})
assert response.status_code == 200, response.text
item = response.json()
assert isinstance(item["id"], int)
assert item["name"] == "apple"
url = f'/items/{item["id"]}'

response = client.get(url)
assert response.status_code == 200, response.text
assert response.json() == item

response = client.get("/items?limit=10&offset=0")
assert response.status_code == 200, response.text
assert response.json() == [item]

response = client.patch(url, json={"name": "banana"})
assert response.status_code == 200, response.text
assert response.json() == {"id": item["id"], "name": "banana"}

response = client.delete(url)
assert response.status_code == 200, response.text
assert response.json() == {"status": True, "text": "successfully deleted"}
assert client.get(url).status_code == 404
assert client.get("/items").json() == []

response = client.get("/openapi.json")
assert response.status_code == 200, response.text
paths = response.json()["paths"]
assert {"get", "post"} <= paths["/items"].keys()
assert {"get", "patch", "delete"} <= paths["/items/{id}"].keys()
"""


@pytest.mark.parametrize(
"filename,heading",
EXAMPLES,
ids=["readme-sync", "readme-async", "docs-sync", "docs-async", "docs-index"],
)
def test_quickstart(filename, heading, tmp_path):
text = (ROOT / filename).read_text(encoding="utf-8")
section = text.split(f"## {heading}\n", 1)[1].split("\n## ", 1)[0]
snippet = re.search(r"^```python\n(.*?)^```", section, flags=re.M | re.S)
assert snippet is not None, f"No Python example in {filename}: {heading}"

# A fresh process and directory prevent db_conf/ORMFactory caches, an
# existing database, or a developer's .env from masking startup failures.
# -I also prevents the source checkout from hiding a broken installed wheel.
env = os.environ.copy()
for key in (
"ORM_TYPE",
"DATABASE_URL",
"SQLALCHEMY_DATABASE_URL",
"SQLALCHEMY_ASYNC_DATABASE_URL",
):
env.pop(key, None)
# Execute checks in the same namespace, without running the example's
# __main__ block (which would start a long-lived Uvicorn server).
program = (
"namespace = {'__name__': 'quickstart'}\n"
f"exec(compile({snippet.group(1)!r}, {filename!r}, 'exec'), namespace)\n"
f"exec({CHECK_ENDPOINTS!r}, namespace)\n"
)
result = subprocess.run(
[sys.executable, "-I", "-c", program],
cwd=tmp_path,
env=env,
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
Loading