diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c3544d5..78927fb 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -42,12 +42,12 @@ jobs: LATEST_PREV_TAG=$(git tag -l "v${PREV_MAJOR}.*" | sort -V | tail -1) if [ -n "$LATEST_PREV_TAG" ]; then - git checkout "$LATEST_PREV_TAG" -- docs/ src/ zensical.toml + git checkout "$LATEST_PREV_TAG" -- docs/ docs_src/ src/ zensical.toml if ! grep -q '\[project\.extra\.version\]' zensical.toml; then printf '\n[project.extra.version]\nprovider = "mike"\ndefault = "stable"\nalias = true\n' >> zensical.toml fi uv run mike deploy "v${PREV_MAJOR}" - git checkout HEAD -- docs/ src/ zensical.toml + git checkout HEAD -- docs/ docs_src/ src/ zensical.toml fi # Delete old feature versions diff --git a/docs/migration/v3.md b/docs/migration/v3.md index 56ea99c..02b6236 100644 --- a/docs/migration/v3.md +++ b/docs/migration/v3.md @@ -4,6 +4,93 @@ This page covers every breaking change introduced in **v3.0** and the steps requ --- +## CRUD + +### Facet keys now always use the full relationship chain + +In `v2`, relationship facet fields used only the terminal column key (e.g. `"name"` for `Role.name`) and only prepended the relationship name when two facet fields shared the same column key. In `v3`, facet keys **always** include the full relationship chain joined by `__`, regardless of collisions. + +=== "Before (`v2`)" + + ``` + User.status -> status + (User.role, Role.name) -> name + (User.role, Role.permission, Permission.name) -> name + ``` + +=== "Now (`v3`)" + + ``` + User.status -> status + (User.role, Role.name) -> role__name + (User.role, Role.permission, Permission.name) -> role__permission__name + ``` + +--- + +### `*_params` dependencies consolidated into per-paginate methods + +The six individual dependency methods (`offset_params`, `cursor_params`, `paginate_params`, `filter_params`, `search_params`, `order_params`) have been **removed** and replaced by three consolidated methods that bundle pagination, search, filter, and order into a single `Depends()` call. + +| Removed | Replacement | +|---|---| +| `offset_params()` + `filter_params()` + `search_params()` + `order_params()` | `offset_paginate_params()` | +| `cursor_params()` + `filter_params()` + `search_params()` + `order_params()` | `cursor_paginate_params()` | +| `paginate_params()` + `filter_params()` + `search_params()` + `order_params()` | `paginate_params()` | + +Each new method accepts `search`, `filter`, and `order` boolean toggles (all `True` by default) to disable features you don't need. + +=== "Before (`v2`)" + + ```python + from fastapi_toolsets.crud import OrderByClause + + @router.get("/offset") + async def list_articles_offset( + session: SessionDep, + params: Annotated[dict, Depends(ArticleCrud.offset_params(default_page_size=20))], + filter_by: Annotated[dict, Depends(ArticleCrud.filter_params())], + order_by: Annotated[OrderByClause | None, Depends(ArticleCrud.order_params(default_field=Article.created_at))], + search: str | None = None, + ) -> OffsetPaginatedResponse[ArticleRead]: + return await ArticleCrud.offset_paginate( + session=session, + **params, + search=search, + filter_by=filter_by or None, + order_by=order_by, + schema=ArticleRead, + ) + ``` + +=== "Now (`v3`)" + + ```python + @router.get("/offset") + async def list_articles_offset( + session: SessionDep, + params: Annotated[ + dict, + Depends( + ArticleCrud.offset_paginate_params( + default_page_size=20, + default_order_field=Article.created_at, + ) + ), + ], + ) -> OffsetPaginatedResponse[ArticleRead]: + return await ArticleCrud.offset_paginate(session=session, **params, schema=ArticleRead) + ``` + +The same pattern applies to `cursor_paginate_params()` and `paginate_params()`. To disable a feature, pass the toggle: + +```python +# No search or ordering, only pagination + filtering +ArticleCrud.offset_paginate_params(search=False, order=False) +``` + +--- + ## Models The lifecycle event system has been rewritten. Callbacks are now registered with a module-level [`listens_for`](../reference/models.md#fastapi_toolsets.models.listens_for) decorator and dispatched by [`EventSession`](../reference/models.md#fastapi_toolsets.models.EventSession), replacing the mixin-based approach from `v2`. diff --git a/docs/module/pytest.md b/docs/module/pytest.md index 52824b8..c52746f 100644 --- a/docs/module/pytest.md +++ b/docs/module/pytest.md @@ -79,9 +79,6 @@ The examples above are already compatible with parallel test execution with `pyt ## Cleaning up tables -!!! warning - Since `V2.1.0` `cleanup_tables` now live in `fastapi_toolsets.db`. For backward compatibility the function is still available in `fastapi_toolsets.pytest`, but this will be remove in `V3.0.0`. - If you want to manually clean up a database you can use [`cleanup_tables`](../reference/db.md#fastapi_toolsets.db.cleanup_tables), this will truncate all tables between tests for fast isolation: ```python diff --git a/pyproject.toml b/pyproject.toml index f05abea..4380fa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fastapi-toolsets" -version = "2.4.3" +version = "3.0.0" description = "Production-ready utilities for FastAPI applications" readme = "README.md" license = "MIT" diff --git a/src/fastapi_toolsets/__init__.py b/src/fastapi_toolsets/__init__.py index 1951558..8dcbf97 100644 --- a/src/fastapi_toolsets/__init__.py +++ b/src/fastapi_toolsets/__init__.py @@ -21,4 +21,4 @@ Example usage: return Response(data={"user": user.username}, message="Success") """ -__version__ = "2.4.3" +__version__ = "3.0.0" diff --git a/src/fastapi_toolsets/exceptions/handler.py b/src/fastapi_toolsets/exceptions/handler.py index d21e3c9..dd7b0a3 100644 --- a/src/fastapi_toolsets/exceptions/handler.py +++ b/src/fastapi_toolsets/exceptions/handler.py @@ -122,7 +122,7 @@ def _format_validation_error( ) return JSONResponse( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, content=error_response.model_dump(), ) diff --git a/src/fastapi_toolsets/metrics/handler.py b/src/fastapi_toolsets/metrics/handler.py index e2e7ea9..ff63b32 100644 --- a/src/fastapi_toolsets/metrics/handler.py +++ b/src/fastapi_toolsets/metrics/handler.py @@ -1,6 +1,6 @@ """Prometheus metrics endpoint for FastAPI applications.""" -import asyncio +import inspect import os from fastapi import FastAPI @@ -55,10 +55,10 @@ def init_metrics( # Partition collectors and cache env check at startup — both are stable for the app lifetime. async_collectors = [ - c for c in registry.get_collectors() if asyncio.iscoroutinefunction(c.func) + c for c in registry.get_collectors() if inspect.iscoroutinefunction(c.func) ] sync_collectors = [ - c for c in registry.get_collectors() if not asyncio.iscoroutinefunction(c.func) + c for c in registry.get_collectors() if not inspect.iscoroutinefunction(c.func) ] multiprocess_mode = _is_multiprocess() diff --git a/src/fastapi_toolsets/pytest/utils.py b/src/fastapi_toolsets/pytest/utils.py index f97316c..5242063 100644 --- a/src/fastapi_toolsets/pytest/utils.py +++ b/src/fastapi_toolsets/pytest/utils.py @@ -1,7 +1,6 @@ """Pytest helper utilities for FastAPI testing.""" import os -import warnings from collections.abc import AsyncGenerator, Callable from contextlib import asynccontextmanager from typing import Any @@ -16,31 +15,10 @@ from sqlalchemy.ext.asyncio import ( ) from sqlalchemy.orm import DeclarativeBase -from ..db import cleanup_tables as _cleanup_tables -from ..db import create_database +from ..db import cleanup_tables, create_database from ..models.watched import EventSession -async def cleanup_tables( - session: AsyncSession, - base: type[DeclarativeBase], -) -> None: - """Truncate all tables for fast between-test cleanup. - - .. deprecated:: - Import ``cleanup_tables`` from ``fastapi_toolsets.db`` instead. - This re-export will be removed in v3.0.0. - """ - warnings.warn( - "Importing cleanup_tables from fastapi_toolsets.pytest is deprecated " - "and will be removed in v3.0.0. " - "Use 'from fastapi_toolsets.db import cleanup_tables' instead.", - DeprecationWarning, - stacklevel=2, - ) - await _cleanup_tables(session=session, base=base) - - def _get_xdist_worker(default_test_db: str) -> str: """Return the pytest-xdist worker name, or *default_test_db* when not running under xdist. @@ -273,7 +251,7 @@ async def create_db_session( yield session if cleanup: - await _cleanup_tables(session=session, base=base) + await cleanup_tables(session=session, base=base) if drop_tables: async with engine.begin() as conn: diff --git a/tests/test_pytest.py b/tests/test_pytest.py index b0ffa67..b277251 100644 --- a/tests/test_pytest.py +++ b/tests/test_pytest.py @@ -374,19 +374,6 @@ class TestCreateDbSession: pass -class TestDeprecatedCleanupTables: - """Tests for the deprecated cleanup_tables re-export in fastapi_toolsets.pytest.""" - - @pytest.mark.anyio - async def test_emits_deprecation_warning(self): - """cleanup_tables imported from fastapi_toolsets.pytest emits DeprecationWarning.""" - from fastapi_toolsets.pytest.utils import cleanup_tables - - async with create_db_session(DATABASE_URL, Base, drop_tables=True) as session: - with pytest.warns(DeprecationWarning, match="fastapi_toolsets.db"): - await cleanup_tables(session, Base) - - class TestGetXdistWorker: """Tests for _get_xdist_worker helper.""" diff --git a/uv.lock b/uv.lock index 0c556e2..240244d 100644 --- a/uv.lock +++ b/uv.lock @@ -251,7 +251,7 @@ wheels = [ [[package]] name = "fastapi-toolsets" -version = "2.4.3" +version = "3.0.0" source = { editable = "." } dependencies = [ { name = "asyncpg" },