Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
642a9b3
feat: migrate toolchain from Poetry to uv, Python 3.12+, SQLAlchemy <3.0
mcode-bot Jun 18, 2026
f52e3c4
feat: refactor FilterWrapper to callable class, modernize type annota…
mcode-bot Jun 18, 2026
c2ec4ba
feat: add Annotated[Filter, Query()] tests and examples, fix deprecat…
mcode-bot Jun 18, 2026
7528ad9
fix: remove poetry.lock, update .python-version to 3.12, use FlatUser…
mcode-bot Jun 18, 2026
99ee824
fix: run pytest directly in nox session venv to actually test paramet…
mcode-bot Jun 18, 2026
790d35c
fix: parametrize nox SQLAlchemy as 2.0.51 only; 2.1.0 final not yet o…
mcode-bot Jun 18, 2026
8851d35
fix: add pydantic[email] to nox session for beanie EmailStr dependency
mcode-bot Jun 18, 2026
6eea150
fix: replace broken #beanie anchor with external beanie example URL i…
mcode-bot Jun 18, 2026
e5306c1
chore: add functional test suite for SQLAlchemy backend (.mcode)
mcode-bot Jun 18, 2026
687f638
fix: correct docstring style and imports in .mcode functional test (r…
mcode-bot Jun 18, 2026
e0e4600
fix: apply ruff format to .mcode functional test file
mcode-bot Jun 18, 2026
6a467d5
fix: add type: ignore for create_model overload, add beanie to mypy i…
mcode-bot Jun 18, 2026
80b7f7d
Fix review issue #1: Use session.run('uv', 'run', 'pytest') as spec r…
mcode-bot Jun 18, 2026
89c1208
Fix review issue #5: Replace hardcoded sandbox paths with portable al…
mcode-bot Jun 18, 2026
4379d78
Fix review issue #6: Route each native-pattern test to non-overlappin…
mcode-bot Jun 18, 2026
9a7970f
Fix review issue #7: Extract assert_pytest_passed helper, replace 19 …
mcode-bot Jun 18, 2026
eea4ee8
fix: apply ruff format to functional test after review fixes
mcode-bot Jun 18, 2026
1f0ef64
fix: add [tool.coverage.run] omit for beanie/mongoengine to hit 100% …
mcode-bot Jun 18, 2026
04b2bd6
Add nosec suppressions for bandit B404/B603 in functional test
mcode-bot Jun 19, 2026
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
17 changes: 6 additions & 11 deletions .github/workflows/publish.yml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,12 @@ jobs:
with:
python-version: "3.12"

- uses: snok/install-poetry@v1
with:
version: 1.7.1
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Publish package
- name: Build and publish package
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
poetry publish \
--build \
--username "__token__" \
--password "${{ env.PYPI_API_TOKEN }}"
uv build
uv publish
52 changes: 16 additions & 36 deletions .github/workflows/test.yml
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v5
Expand All @@ -27,34 +27,25 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: cache poetry install
uses: actions/cache@v4
with:
path: ~/.local
key: poetry-${{ steps.setup-python.outputs.python-version }}-1.8.5-0

- uses: snok/install-poetry@v1
with:
version: 1.8.5
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Load cached venv
id: cached-poetry-dependencies
id: cached-uv-dependencies
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-0
key: venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/uv.lock') }}-0

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --extras all
if: steps.cached-uv-dependencies.outputs.cache-hit != 'true'
run: uv sync --all-extras

- name: Set up MongoDB
uses: supercharge/mongodb-github-action@v1.10.0

- name: Run unit tests
run: poetry run -- nox -- tests --cov-report=xml
run: uv run nox -- tests --cov-report=xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
Expand All @@ -69,7 +60,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v5
Expand All @@ -80,30 +71,19 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: cache poetry install
uses: actions/cache@v4
with:
path: ~/.local
key: poetry-${{ steps.setup-python.outputs.python-version }}-1.8.5-0

- uses: snok/install-poetry@v1
with:
version: 1.8.5
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install uv
uses: astral-sh/setup-uv@v6

- name: Load cached venv
id: cached-poetry-dependencies
id: cached-uv-dependencies
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}-0
key: venv-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/uv.lock') }}-0

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --extras all
if: steps.cached-uv-dependencies.outputs.cache-hit != 'true'
run: uv sync --all-extras

- name: Run pre-commit
run: |
source .venv/bin/activate
pre-commit run --all-files
run: uv run pre-commit run --all-files
197 changes: 197 additions & 0 deletions .mcode/functional-tests/1/test_sqlalchemy_suite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"""Functional tests for the fastapi-filter library's SQLAlchemy backend.

These tests wrap the library's own pytest suite and verify:
1. FilterDepends (existing pattern) still works - regression test for _FilterWrapper refactor
2. Annotated[Filter, Query()] native pattern works - new feature in the modernization
3. split_str validator handles comma-separated strings in both FilterDepends and native paths
4. Order-by functionality works correctly

This is a CLI-style functional test: each test class invokes the library's pytest suite
with a specific subset of tests via subprocess and checks the exit code + output.
"""

import os
import shutil
import subprocess # nosec B404

_uv = shutil.which("uv")
if _uv is None:
raise RuntimeError("uv binary not found on PATH; install uv before running functional tests")
UV_BIN: str = _uv
REPO_DIR: str = os.path.join(os.environ.get("WORKSPACE_DIR", "/l2l/workspace"), "fastapi-filter")
ENV = {**os.environ}


def run_pytest(test_selector: str, extra_args: list[str] | None = None) -> subprocess.CompletedProcess:
"""Run a specific subset of the SQLAlchemy test suite via uv run pytest."""
cmd = [
UV_BIN,
"run",
"pytest",
f"tests/test_sqlalchemy/{test_selector}",
"-v",
"--no-header",
"--tb=short",
"--no-cov", # disable coverage for functional test runs
]
if extra_args:
cmd.extend(extra_args)
return subprocess.run( # nosec B603
cmd,
cwd=REPO_DIR,
capture_output=True,
text=True,
timeout=120,
env=ENV,
)


def assert_pytest_passed(result: subprocess.CompletedProcess, label: str) -> None:
"""Assert that a subprocess pytest run succeeded and reported passing tests."""
assert result.returncode == 0, f"{label} failed.\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
assert "passed" in result.stdout


class TestFilterDependsRegression:
"""Regression tests for the FilterDepends pattern after _FilterWrapper refactor.

These tests verify the existing FilterDepends functionality is preserved.
test_filter.py::test_filter covers all the filter operators via direct instantiation.
"""

def test_filter_direct_instantiation(self):
"""FilterDepends pattern: direct filter instantiation with all operators."""
result = run_pytest("test_filter.py::test_filter")
assert_pytest_passed(result, "test_filter")

def test_filter_api_filterdepends(self):
"""FilterDepends pattern: API endpoint filters via /users and /users-by-alias."""
result = run_pytest("test_filter.py::test_api")
assert_pytest_passed(result, "test_api (FilterDepends path)")

def test_required_filter_validation(self):
"""FilterDepends pattern: required filter fields trigger 422 when missing."""
result = run_pytest("test_filter.py::test_required_filter")
assert_pytest_passed(result, "test_required_filter")

def test_deprecation_like_ilike(self):
"""FilterDepends pattern: like/ilike without % triggers DeprecationWarning."""
result = run_pytest("test_filter.py::test_filter_deprecation_like_and_ilike")
assert_pytest_passed(result, "test_filter_deprecation_like_and_ilike")


class TestNativeAnnotatedPattern:
"""Tests for the new Annotated[Filter, Query()] native pattern.

This is a new feature added in the modernization (target_only).
The /users-native endpoint uses Annotated[FlatUserFilter, Query()]
and must support comma-separated string handling via split_str.
"""

def test_native_pattern_basic_filters(self):
"""Native Annotated[Filter, Query()] pattern: basic name and age filters."""
result = run_pytest(
"test_filter.py::test_api_native_pattern",
extra_args=["-k", "filter_0 or filter_2 or filter_3 or filter_5 or filter_6"],
)
assert_pytest_passed(result, "test_api_native_pattern (basic filters)")

def test_native_pattern_split_str_comma_handling(self):
"""Native pattern: split_str handles single-element list wrapping from FastAPI.

When FastAPI receives ?name__in=Mr+Praline,Mr+Creosote via native Annotated[Filter, Query()],
it wraps the value in a list: ["Mr Praline,Mr Creosote"].
The split_str validator must detect this and split on commas.
This verifies the split_str fix that handles the single-element list wrapping
from FastAPI's native Annotated[Filter, Query()] path.

The test_api_native_pattern parametrize includes cases like:
filter_1: {"name__in": "Mr Praline,Mr Creosote,Gumbys,Knight"} -> 3 results
filter_4: {"name__not_in": "Mr Praline,Mr Creosote,Gumbys,Knight"} -> 2 results
filter_7: {"age__in": "1"} -> 1 result
filter_8: {"age__in": "21,33"} -> 3 results
"""
result = run_pytest(
"test_filter.py::test_api_native_pattern",
extra_args=["-k", "filter_1 or filter_4 or filter_7 or filter_8"],
)
assert_pytest_passed(result, "split_str comma handling in native pattern")
# Verify parametrized cases with comma-separated string values pass
# These are filter_1 (name__in), filter_4 (name__not_in), filter_7/8 (age__in)
assert "filter_1" in result.stdout
assert "filter_4" in result.stdout
assert "filter_7" in result.stdout
assert "filter_8" in result.stdout


class TestOrderByFunctionality:
"""Tests for the order_by sorting functionality.

These cover the full order_by test suite including direction (+/-),
custom ordering fields, restricted ordering, and duplicate detection.
"""

def test_order_by_direct(self):
"""Direct order_by instantiation with various sort directions."""
result = run_pytest("test_order_by.py::test_order_by")
assert_pytest_passed(result, "test_order_by")

def test_order_by_with_default(self):
"""Order-by with a default value is applied when no order_by param is given."""
result = run_pytest("test_order_by.py::test_order_by_with_default")
assert_pytest_passed(result, "test_order_by_with_default")

def test_order_by_api_with_default(self):
"""API endpoint: order_by with default applied correctly via HTTP."""
result = run_pytest("test_order_by.py::test_api_order_by_with_default")
assert_pytest_passed(result, "test_api_order_by_with_default")

def test_invalid_order_by(self):
"""Invalid order_by field raises ValidationError."""
result = run_pytest("test_order_by.py::test_invalid_order_by")
assert_pytest_passed(result, "test_invalid_order_by")

def test_missing_order_by_field(self):
"""Using sort() on a filter without order_by defined raises AttributeError."""
result = run_pytest("test_order_by.py::test_missing_order_by_field")
assert_pytest_passed(result, "test_missing_order_by_field")

def test_custom_order_by(self):
"""Custom ordering_field_name (not default 'order_by') works correctly."""
result = run_pytest("test_order_by.py::test_custom_order_by")
assert_pytest_passed(result, "test_custom_order_by")

def test_restricted_order_by_failure(self):
"""Order_by with non-allowed field raises ValidationError."""
result = run_pytest("test_order_by.py::test_restricted_order_by_failure")
assert_pytest_passed(result, "test_restricted_order_by_failure")

def test_restricted_order_by_success(self):
"""Order_by with allowed fields succeeds."""
result = run_pytest("test_order_by.py::test_restricted_order_by_success")
assert_pytest_passed(result, "test_restricted_order_by_success")

def test_api_order_by(self):
"""API endpoint: order_by via HTTP query params works correctly."""
result = run_pytest("test_order_by.py::test_api_order_by")
assert_pytest_passed(result, "test_api_order_by")

def test_api_order_by_invalid_field(self):
"""API endpoint: invalid order_by field returns 422."""
result = run_pytest("test_order_by.py::test_api_order_by_invalid_field")
assert_pytest_passed(result, "test_api_order_by_invalid_field")

def test_api_restricted_order_by(self):
"""API endpoint: restricted order_by fields enforce allowlist."""
result = run_pytest("test_order_by.py::test_api_restricted_order_by")
assert_pytest_passed(result, "test_api_restricted_order_by")

def test_api_custom_order_by(self):
"""API endpoint: custom ordering_field_name used via HTTP."""
result = run_pytest("test_order_by.py::test_api_custom_order_by")
assert_pytest_passed(result, "test_api_custom_order_by")

def test_order_by_duplicates_fail(self):
"""Duplicate fields in order_by raise ValidationError with descriptive message."""
result = run_pytest("test_order_by.py::test_order_by_with_duplicates_fail")
assert_pytest_passed(result, "test_order_by_with_duplicates_fail")
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.13
3.12
20 changes: 9 additions & 11 deletions README.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@

**Required:**

- Python: >=3.9, <4.0
- Fastapi: >=0.100, <1.0
- Pydantic: >=2.0.0, <3.0.0
- Python: >=3.12
- Fastapi: >=0.115.0, <1.0
- Pydantic: >=2.12.0, <3.0.0

**Optional**

- MongoEngine: >=0.24.1, <0.28.0
- SQLAlchemy: >=1.4.36, <2.1.0
- MongoEngine: >=0.28.0, <0.30.0
- SQLAlchemy: >=2.0.0, <3.0.0

## Installation

Expand All @@ -43,9 +43,8 @@ Please visit: [https://fastapi-filter.netlify.app/](https://fastapi-filter.netli
You can play with examples:

```bash
pip install poetry
poetry install
python examples/fastapi_filter_sqlalchemy.py
uv sync --all-extras
uv run python examples/fastapi_filter_sqlalchemy.py
```

### Filter
Expand All @@ -61,9 +60,8 @@ https://user-images.githubusercontent.com/950449/176747056-ea82d6b9-cb3b-43eb-ae
You can run tests with `pytest`.

```bash
pip install poetry
poetry install --extras all
pytest
uv sync --all-extras
uv run pytest
```

<img width="884" alt="arthur_Arthurs-MacBook-Pro-2___code_fastapi-filter" src="https://user-images.githubusercontent.com/950449/176737623-a77f15d6-4e60-4c06-bdb7-b3d77f346a54.png">
7 changes: 7 additions & 0 deletions docs/index.md
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ as well as the type of operator, then tie your filter to a specific model.

[MongoEngine](https://github.com/arthurio/fastapi-filter/blob/main/examples/fastapi_filter_mongoengine.py)

> **Warning: Synchronous driver.** The MongoEngine backend uses PyMongo, a
> **synchronous** driver. Calling filter or sort operations from `async def`
> FastAPI route handlers will **block the asyncio event loop**, degrading
> throughput under load. For async FastAPI applications, use the
> [Beanie backend](https://github.com/arthurio/fastapi-filter/blob/main/examples/fastapi_filter_beanie.py)
> instead.

[beanie](https://github.com/arthurio/fastapi-filter/blob/main/examples/fastapi_filter_beanie.py)

### Operators
Expand Down
Loading
Loading