Skip to content

Commit

Permalink
refactor: Use Python 3.9 type annotations (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelemi authored Sep 21, 2022
1 parent 9811ba4 commit d422011
Show file tree
Hide file tree
Showing 14 changed files with 156 additions and 147 deletions.
22 changes: 11 additions & 11 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Upload Python Package to pypi
name: Upload Python Package to PyPI

on:
release:
Expand All @@ -10,23 +10,23 @@ permissions:
jobs:
publish:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10" ]

steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
python-version: "3.10"

- name: Install dependencies
run: |
pip install -U pip poetry
- name: Install Poetry
run: pip install -U pip poetry==1.2.0

- name: Publish package
env:
PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: poetry publish --build -u "__token__" -p "${{ env.PYPI_API_TOKEN }}"
run: |
poetry publish \
--build \
--username "__token__" \
--password "${{ env.PYPI_API_TOKEN }}"
14 changes: 8 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,32 @@ on:
push:
branches:
- "main"
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [ "3.10" ]
python-version: [ "3.9", "3.10" ]

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
run: pip install -U pip poetry==1.2.0

- name: Set up MongoDB
uses: supercharge/[email protected]

- name: Install dependencies
run: |
pip install -U poetry
poetry install -E all
run: poetry install --extras all

- name: Unit tests
run: poetry run pytest tests --cov-report=xml
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
## Compatibilty

**Required:**
* Python: 3.10+
* Python: 3.9+
* Fastapi: 0.78+
* Pydantic: 1.9+

Expand Down
60 changes: 33 additions & 27 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,21 @@ and even re-use existing filters for their models.

As you can see in the examples, all it takes is something along the lines of:

```python hl_lines="19"
```python hl_lines="21"
from typing import Optional

class AddressFilter(Filter):
street: str | None
country: str | None
city__in: list[str] | None
street: Optional[str]
country: Optional[str]
city__in: Optional[list[str]]

class Constants(Filter.Constants):
model = Address


class UserFilter(Filter):
name: str | None
address: AddressFilter | None = FilterDepends(with_prefix("address", AddressFilter))
name: Optional[str]
address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))

class Constants(Filter.Constants):
model = User
Expand Down Expand Up @@ -96,17 +98,19 @@ used for filtering related fields.
The following would be equivalent:

```python
from typing import Optional

class AddressFilter(Filter):
street: str | None
country: str | None
street: Optional[str]
country: Optional[str]

class Constants(Filter.Constants):
model = Address


class UserFilter(Filter):
name: str | None
address: AddressFilter | None = FilterDepends(with_prefix("address", AddressFilter))
name: Optional[str]
address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))

class Constants(Filter.Constants):
model = User
Expand All @@ -116,9 +120,9 @@ AND

```python
class UserFilter(Filter):
name: str | None
address__street: str | None
address__country: str | None
name: Optional[str]
address__street: Optional[str]
address__country: Optional[str]
```

## Order by
Expand All @@ -135,12 +139,12 @@ If you don't want to allow ordering on your filter, just don't add `order_by` as

### Example - Basic


```python
from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter

class UserFilter(Filter):
order_by: list[str] | None
order_by: Optional[list[str]]

@app.get("/users", response_model=list[UserOut])
async def get_users(
Expand All @@ -167,14 +171,15 @@ Valid urls:
If for some reason you can't or don't want to use `order_by` as the field name for ordering, you can override it:

```python
from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter

class UserFilter(Filter):
class Constants(Filter.Constants):
model = User
ordering_field_name = "custom_order_by"

custom_order_by: list[str] | None
custom_order_by: Optional[list[str]]

@app.get("/users", response_model=list[UserOut])
async def get_users(
Expand All @@ -201,25 +206,26 @@ curl /users?custom_order_by=+id
Add the following validator to your filter class:

```python
from typing import Optional
from fastapi_filter.contrib.sqlalchemy import Filter
from pydantic import validator

class MyFilter(Filter):
order_by: list[str] | None
order_by: Optional[list[str]]

@validator("order_by")
def restrict_sortable_fields(cls, value):
if value is None:
return None
@validator("order_by")
def restrict_sortable_fields(cls, value):
if value is None:
return None

allowed_field_names = ["age", "id"]
allowed_field_names = ["age", "id"]

for field_name in value:
field_name = field_name.replace("+", "").replace("-", "") # (1)
if field_name not in allowed_field_names:
raise ValueError(f"You may only sort by: {', '.join(allowed_field_names)}")
for field_name in value:
field_name = field_name.replace("+", "").replace("-", "") # (1)
if field_name not in allowed_field_names:
raise ValueError(f"You may only sort by: {', '.join(allowed_field_names)}")

return value
return value
```

1. If you want to restrict only on specific directions, like `-created_at` and `name` for example, you can remove this
Expand Down
20 changes: 10 additions & 10 deletions examples/fastapi_filter_mongoengine.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Generator
from typing import Any, Generator, Optional

import uvicorn
from bson.objectid import ObjectId
Expand Down Expand Up @@ -63,28 +63,28 @@ class UserOut(UserIn):
name: str
email: EmailStr
age: int
address: AddressOut | None
address: Optional[AddressOut]

class Config:
orm_mode = True


class AddressFilter(Filter):
street: str | None
country: str | None
city: str | None
city__in: list[str] | None
custom_order_by: list[str] | None
street: Optional[str]
country: Optional[str]
city: Optional[str]
city__in: Optional[list[str]]
custom_order_by: Optional[list[str]]

class Constants(Filter.Constants):
model = Address
ordering_field_name = "custom_order_by"


class UserFilter(Filter):
name: str | None
address: AddressFilter | None = FilterDepends(with_prefix("address", AddressFilter))
age__lt: int | None
name: Optional[str]
address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))
age__lt: Optional[int]
age__gte: int = 10 # <-- NOTE(arthurio): This filter required
order_by: list[str] = ["age"]

Expand Down
20 changes: 10 additions & 10 deletions examples/fastapi_filter_sqlalchemy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, AsyncIterator
from typing import Any, AsyncIterator, Optional

import uvicorn
from faker import Faker
Expand Down Expand Up @@ -58,28 +58,28 @@ class UserIn(BaseModel):

class UserOut(UserIn):
id: int
address: AddressOut | None
address: Optional[AddressOut]

class Config:
orm_mode = True


class AddressFilter(Filter):
street: str | None
country: str | None
city: str | None
city__in: list[str] | None
custom_order_by: list[str] | None
street: Optional[str]
country: Optional[str]
city: Optional[str]
city__in: Optional[list[str]]
custom_order_by: Optional[list[str]]

class Constants(Filter.Constants):
model = Address
ordering_field_name = "custom_order_by"


class UserFilter(Filter):
name: str | None
address: AddressFilter | None = FilterDepends(with_prefix("address", AddressFilter))
age__lt: int | None
name: Optional[str]
address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))
age__lt: Optional[int]
age__gte: int = 10 # <-- NOTE(arthurio): This filter required
order_by: list[str] = ["age"]

Expand Down
18 changes: 9 additions & 9 deletions fastapi_filter/base/filter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from collections import defaultdict
from collections.abc import Iterable
from copy import deepcopy
from typing import Any, Type
from typing import Any, Optional, Type, Union

from fastapi import Depends, HTTPException, status
from pydantic import BaseModel, Extra, ValidationError, create_model, fields, validator
Expand Down Expand Up @@ -130,11 +130,11 @@ def with_prefix(prefix: str, Filter: Type[BaseFilterModel]):
from fastapi_filter.filter import FilterDepends
class NumberFilter(BaseModel):
count: int | None
count: Optional[int]
class MainFilter(BaseModel):
name: str
number_filter: Filter | None = FilterDepends(with_prefix("number_filter", Filter))
number_filter: Optional[Filter] = FilterDepends(with_prefix("number_filter", Filter))
```
As a result, you'll get the following filters:
Expand All @@ -151,11 +151,11 @@ class MainFilter(BaseModel):
from pydantic import BaseModel
class NumberFilter(BaseModel):
count: int | None = Query(default=10, alias=counter)
count: Optional[int] = Query(default=10, alias=counter)
class MainFilter(BaseModel):
name: str
number_filter: Filter | None = FilterDepends(with_prefix("number_filter", Filter))
number_filter: Optional[Filter] = FilterDepends(with_prefix("number_filter", Filter))
```
As a result, you'll get the following filters:
Expand All @@ -177,24 +177,24 @@ def alias_generator(cls, string: str) -> str:


def _list_to_str_fields(Filter: Type[BaseFilterModel]):
ret: dict[str, tuple[object | Type, FieldInfo | None]] = {}
ret: dict[str, tuple[Union[object, Type], Optional[FieldInfo]]] = {}
for f in Filter.__fields__.values():
field_info = deepcopy(f.field_info)
if f.shape == fields.SHAPE_LIST:
if isinstance(field_info.default, Iterable):
field_info.default = ",".join(field_info.default)
ret[f.name] = (str | None, field_info)
ret[f.name] = (Optional[str], field_info)
else:
field_type = Filter.__annotations__.get(f.name, f.outer_type_)
ret[f.name] = (field_type | None, field_info)
ret[f.name] = (Optional[field_type], field_info)

return ret


def FilterDepends(Filter: Type[BaseFilterModel], *, by_alias: bool = False, use_cache: bool = True) -> Any:
"""This is a hack to support lists in filters.
Fastapi doesn't support it yet: https://github.com/tiangolo/fastapi/issues/50
FastAPI doesn't support it yet: https://github.com/tiangolo/fastapi/issues/50
What we do is loop through the fields of a filter and change any `list` field to a `str` one so that it won't be
excluded from the possible query parameters.
Expand Down
Loading

0 comments on commit d422011

Please sign in to comment.