Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update codebase to support fastapi>=0.100.0 and pydantic>=2.0.0 #447

Merged
merged 23 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6a09192
update codebase to support fastapi>=0.100.0 and pydantic>=2.0.0
johnybx Aug 21, 2023
f234e0d
update comment
johnybx Aug 21, 2023
43268a7
update examples
johnybx Aug 21, 2023
742accd
fix unsupported typing, fix compatibility with older python versions
johnybx Aug 22, 2023
5bc801b
fix breaking change of `with_prefix` -> keep same behaviour as before…
johnybx Sep 4, 2023
d5618bd
Merge branch 'main' into main
arthurio Sep 5, 2023
c457b97
Merge remote-tracking branch 'origin/main' into johnybx/main
arthurio Sep 5, 2023
9449e25
Merge remote-tracking branch 'origin/main' into johnybx/main
arthurio Sep 5, 2023
4af4450
Merge remote-tracking branch 'origin/main' into johnybx/main
arthurio Sep 5, 2023
30f4c3b
fix coverage
johnybx Sep 6, 2023
3174131
update required dependencies in readme
johnybx Sep 6, 2023
bf7e5c2
Merge branch 'main' into main
johnybx Sep 6, 2023
b1f8c0d
fix linting
johnybx Sep 6, 2023
73baabe
fix supported pydantic versions
johnybx Sep 6, 2023
5551a2a
Merge remote-tracking branch 'origin/main' into johnybx/main
arthurio Sep 12, 2023
e76051a
Update examples/fastapi_filter_mongoengine.py
johnybx Sep 13, 2023
94c45f2
Update examples/fastapi_filter_sqlalchemy.py
johnybx Sep 13, 2023
eadbac0
Update fastapi_filter/base/filter.py
johnybx Sep 13, 2023
cf2473b
resolve conflicts, update poetry.lock
johnybx Sep 13, 2023
35ff5ae
Merge branch 'main' into main
johnybx Sep 13, 2023
d8406fa
Merge branch 'main' into main
johnybx Sep 13, 2023
0f1c161
fix linting
johnybx Sep 13, 2023
a267eea
add note and docs section about limitations of union types in filter
johnybx Sep 20, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ fastapi_filter.sqlite
poetry.toml
.pytest_cache/
.ruff_cache/
__pycache__
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
**Required:**

- Python: >=3.8, <4.0
- Fastapi: >=0.78, <1.0
- Pydantic: >=1.10.0, <2.0.0
- Fastapi: >=0.100, <1.0
- Pydantic: >=2.0.0, <3.0.0

**Optional**

Expand Down
34 changes: 15 additions & 19 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Add querystring filters to your api endpoints and show them in the swagger UI.
The supported backends are [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) and
[MongoEngine](https://github.com/MongoEngine/mongoengine).


## Example

![Swagger UI](./swagger-ui.png)
Expand All @@ -25,16 +24,16 @@ as well as the type of operator, then tie your filter to a specific model.

By default, **fastapi_filter** supports the following operators:

- `neq`
- `gt`
- `gte`
- `in`
- `isnull`
- `lt`
- `lte`
- `not`/`ne`
- `not_in`/`nin`
- `like`/`ilike`
- `neq`
- `gt`
- `gte`
- `in`
- `isnull`
- `lt`
- `lte`
- `not`/`ne`
- `not_in`/`nin`
- `like`/`ilike`

_**Note:** Mysql excludes `None` values when using `in` filter_

Expand Down Expand Up @@ -89,7 +88,6 @@ Wherever you would use a `Depends`, replace it with `FilterDepends` if you are p
that `FilterDepends` converts the `list` filter fields to `str` so that they can be displayed and used in swagger.
It also handles turning `ValidationError` into `HTTPException(status_code=422)`.


### with_prefix

[link](https://github.com/arthurio/fastapi-filter/blob/main/fastapi_filter/base/filter.py#L21)
Expand Down Expand Up @@ -133,12 +131,11 @@ There is a specific field on the filter class that can be used for ordering. The
takes a list of string. From an API call perspective, just like the `__in` filters, you simply pass a comma separated
list of strings.

You can change the **direction** of the sorting (*asc* or *desc*) by prefixing with `-` or `+` (Optional, it's the
You can change the **direction** of the sorting (_asc_ or _desc_) by prefixing with `-` or `+` (Optional, it's the
default behavior if omitted).

If you don't want to allow ordering on your filter, just don't add `order_by` (or custom `ordering_field_name`) as a field and you are all set.


## Search

There is a specific field on the filter class that can be used for searching. The default name is `search` and it takes
Expand All @@ -148,7 +145,6 @@ You have to define what fields/columns to search in with the `search_model_field

If you don't want to allow searching on your filter, just don't add `search` (or custom `search_field_name`) as a field and you are all set.


### Example - Basic

```python
Expand Down Expand Up @@ -215,17 +211,17 @@ curl /users?custom_order_by=+id

### Restrict the `order_by` values

Add the following validator to your filter class:
Add the following field_validator to your filter class:

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

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

@validator("order_by")
@field_validator("order_by")
def restrict_sortable_fields(cls, value):
if value is None:
return None
Expand All @@ -241,7 +237,7 @@ class MyFilter(Filter):
```

1. If you want to restrict only on specific directions, like `-created_at` and `name` for example, you can remove this
line. Your `allowed_field_names` would be something like `["age", "-age", "-created_at"]`.
line. Your `allowed_field_names` would be something like `["age", "-age", "-created_at"]`.

### Example - Search

Expand Down
56 changes: 30 additions & 26 deletions examples/fastapi_filter_mongoengine.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging
from typing import Any, Dict, Generator, List, Optional
from typing import Any, List, Optional

import click
import uvicorn
from bson.objectid import ObjectId
from faker import Faker
from fastapi import FastAPI, Query
from mongoengine import Document, connect, fields
from pydantic import BaseModel, EmailStr, Field
from pydantic import BaseModel, ConfigDict, EmailStr, Field, GetCoreSchemaHandler
from pydantic_core import CoreSchema, core_schema

from fastapi_filter import FilterDepends, with_prefix
from fastapi_filter.contrib.mongoengine import Filter
Expand All @@ -19,18 +20,22 @@

class PydanticObjectId(ObjectId):
@classmethod
def __get_validators__(cls) -> Generator:
yield cls.validate

@classmethod
def validate(cls, v: ObjectId) -> str:
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
return core_schema.no_info_after_validator_function(
cls.validate,
core_schema.is_instance_schema(cls=ObjectId),
serialization=core_schema.plain_serializer_function_ser_schema(
str,
info_arg=False,
return_schema=core_schema.str_schema(),
),
)
johnybx marked this conversation as resolved.
Show resolved Hide resolved

@staticmethod
def validate(v: ObjectId) -> ObjectId:
if not ObjectId.is_valid(v):
raise ValueError("Invalid objectid")
return str(v)

@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type="string")
return v


class Address(Document):
Expand Down Expand Up @@ -63,23 +68,22 @@ class UserIn(BaseModel):


class UserOut(UserIn):
model_config = ConfigDict(from_attributes=True)

id: PydanticObjectId = Field(..., alias="_id")
name: str
email: EmailStr
age: int
address: Optional[AddressOut]

class Config:
orm_mode = True
address: Optional[AddressOut] = None


class AddressFilter(Filter):
street: Optional[str]
country: Optional[str]
city: Optional[str]
city__in: Optional[List[str]]
custom_order_by: Optional[List[str]]
custom_search: Optional[str]
street: Optional[str] = None
country: Optional[str] = None
city: Optional[str] = None
city__in: Optional[List[str]] = None
custom_order_by: Optional[List[str]] = None
custom_search: Optional[str] = None

class Constants(Filter.Constants):
model = Address
Expand All @@ -89,16 +93,16 @@ class Constants(Filter.Constants):


class UserFilter(Filter):
name: Optional[str]
name: Optional[str] = None
address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))
age__lt: Optional[int]
age__lt: Optional[int] = None
age__gte: int = Field(Query(description="this is a nice description"))
"""Required field with a custom description.

See: https://github.com/tiangolo/fastapi/issues/4700 for why we need to wrap `Query` in `Field`.
"""
order_by: List[str] = ["age"]
search: Optional[str]
search: Optional[str] = None

class Constants(Filter.Constants):
model = User
Expand Down Expand Up @@ -138,7 +142,7 @@ async def get_users(user_filter: UserFilter = FilterDepends(UserFilter)) -> Any:

@app.get("/addresses", response_model=List[AddressOut])
async def get_addresses(
address_filter: AddressFilter = FilterDepends(with_prefix("my_prefix", AddressFilter), by_alias=True),
address_filter: AddressFilter = FilterDepends(with_prefix("address", AddressFilter), by_alias=True),
johnybx marked this conversation as resolved.
Show resolved Hide resolved
) -> Any:
query = address_filter.filter(Address.objects())
query = address_filter.sort(query)
Expand Down
40 changes: 19 additions & 21 deletions examples/fastapi_filter_sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import uvicorn
from faker import Faker
from fastapi import Depends, FastAPI, Query
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import Column, ForeignKey, Integer, String, event, select
from sqlalchemy.engine import Engine
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
Expand Down Expand Up @@ -53,14 +53,13 @@ class User(Base):


class AddressOut(BaseModel):
model_config = ConfigDict(from_attributes=True)

id: int
street: str
city: str
country: str

class Config:
orm_mode = True


class UserIn(BaseModel):
name: str
Expand All @@ -69,20 +68,19 @@ class UserIn(BaseModel):


class UserOut(UserIn):
id: int
address: Optional[AddressOut]
model_config = ConfigDict(from_attributes=True)

class Config:
orm_mode = True
id: int
address: Optional[AddressOut] = None


class AddressFilter(Filter):
street: Optional[str]
country: Optional[str]
city: Optional[str]
city__in: Optional[List[str]]
custom_order_by: Optional[List[str]]
custom_search: Optional[str]
street: Optional[str] = None
country: Optional[str] = None
city: Optional[str] = None
city__in: Optional[List[str]] = None
custom_order_by: Optional[List[str]] = None
custom_search: Optional[str] = None

class Constants(Filter.Constants):
model = Address
Expand All @@ -92,19 +90,19 @@ class Constants(Filter.Constants):


class UserFilter(Filter):
name: Optional[str]
name__ilike: Optional[str]
name__like: Optional[str]
name__neq: Optional[str]
name: Optional[str] = None
name__ilike: Optional[str] = None
name__like: Optional[str] = None
name__neq: Optional[str] = None
address: Optional[AddressFilter] = FilterDepends(with_prefix("address", AddressFilter))
age__lt: Optional[int]
age__lt: Optional[int] = None
age__gte: int = Field(Query(description="this is a nice description"))
"""Required field with a custom description.

See: https://github.com/tiangolo/fastapi/issues/4700 for why we need to wrap `Query` in `Field`.
"""
order_by: List[str] = ["age"]
search: Optional[str]
search: Optional[str] = None

class Constants(Filter.Constants):
model = User
Expand Down Expand Up @@ -158,7 +156,7 @@ async def get_users(

@app.get("/addresses", response_model=List[AddressOut])
async def get_addresses(
address_filter: AddressFilter = FilterDepends(with_prefix("my_prefix", AddressFilter), by_alias=True),
address_filter: AddressFilter = FilterDepends(with_prefix("address", AddressFilter), by_alias=True),
johnybx marked this conversation as resolved.
Show resolved Hide resolved
db: AsyncSession = Depends(get_db),
) -> Any:
query = select(Address)
Expand Down
Loading