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

Improve serializer style #66

Merged
merged 9 commits into from
Jan 29, 2024
3 changes: 3 additions & 0 deletions docs/docs/release_notes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
### 3.9.0
- Change the style of `ModelSerializer` usage

### 3.8.2
- Add `content-type = application/json` header in raise response of `__call__`

Expand Down
150 changes: 98 additions & 52 deletions docs/docs/serializer.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,114 @@ You can write your `serializer` in 2 style:
Write a normal `pydantic` class and use it as serializer:

```python
from pydantic import BaseModel
from pydantic import Field

from panther.app import API
from panther.request import Request
from panther.response import Response


class UserSerializer(BaseModel):
username: str
password: str
first_name: str = Field(default='', min_length=2)
last_name: str = Field(default='', min_length=4)


@API(input_model=UserSerializer)
async def serializer_example(request: Request):
return Response(data=request.validated_data)
from pydantic import BaseModel
from pydantic import Field
from panther.app import API
from panther.request import Request
from panther.response import Response
class UserSerializer(BaseModel):
username: str
password: str
first_name: str = Field(default='', min_length=2)
last_name: str = Field(default='', min_length=4)
@API(input_model=UserSerializer)
async def serializer_example(request: Request):
return Response(data=request.validated_data)
```

## Style 2 (Model Serializer)
Use panther `ModelSerializer` to write your serializer which will use your `model` fields as its fields, and you can say which fields are `required`
### Simple Usage

Use panther `ModelSerializer` to write your serializer which will use your `model` to create fields.

```python
from pydantic import Field

from panther import status
from panther.app import API
from panther.db import Model
from panther.request import Request
from panther.response import Response
from panther.serializer import ModelSerializer


class User(Model):
username: str
password: str
first_name: str = Field(default='', min_length=2)
last_name: str = Field(default='', min_length=4)


class UserModelSerializer(metaclass=ModelSerializer, model=User):
fields = ['username', 'first_name', 'last_name']
required_fields = ['first_name']


@API(input_model=UserModelSerializer)
async def model_serializer_example(request: Request):
return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED)
from pydantic import Field

from panther import status
from panther.app import API
from panther.db import Model
from panther.request import Request
from panther.response import Response
from panther.serializer import ModelSerializer


class User(Model):
username: str
password: str
first_name: str = Field(default='', min_length=2)
last_name: str = Field(default='', min_length=4)


class UserModelSerializer(ModelSerializer):
class Config:
model = User
fields = ['username', 'first_name', 'last_name']
required_fields = ['first_name']


@API(input_model=UserModelSerializer)
async def model_serializer_example(request: Request):
return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED)
```

## Notes
1. In the example above `UserModelSerializer` only accepts the values of `fields` attribute
### Notes
1. In the example above, `ModelSerializer` will look up for the value of `Config.fields` in the `User.fields` and use their `type` and `value` for the `validation`.
2. `Config.model` and `Config.fields` are `required` when you are using `ModelSerializer`.
3. If you want to use `Config.required_fields`, you have to put its value in `Config.fields` too.



### Complex Usage

You can use `pydantic.BaseModel` features in `ModelSerializer` too.

```python
from pydantic import Field, field_validator, ConfigDict

2. In default the `UserModelSerializer.fields` are same as `User.fields` but you can change their default and make them required with `required_fields` attribute
from panther import status
from panther.app import API
from panther.db import Model
from panther.request import Request
from panther.response import Response
from panther.serializer import ModelSerializer


class User(Model):
username: str
password: str
first_name: str = Field(default='', min_length=2)
last_name: str = Field(default='', min_length=4)

3. If you want to use `required_fields` you have to put them in `fields` too.
class UserModelSerializer(ModelSerializer):
model_config = ConfigDict(str_to_upper=True)
age: int = Field(default=20)
is_male: bool
username: str

class Config:
model = User
fields = ['username', 'first_name', 'last_name']
required_fields = ['first_name']

4. `fields` attribute is `required` when you are using `ModelSerializer` as `metaclass`
@field_validator('username')
def validate_username(cls, username):
print(f'{username=}')
return username

5. `model=` is required when you are using `ModelSerializer` as `metaclass`

@API(input_model=UserModelSerializer)
async def model_serializer_example(request: Request):
return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED)
```

6. You have to use `ModelSerializer` as `metaclass` (not as a parent)
### Notes
1. You can add custom `fields` in `pydantic style`
2. You can add `model_config` as `attribute` and also in the `Config`
3. You can use `@field_validator` and other `validators` of `pydantic`.


7. Panther is going to create a `pydantic` model as your `UserModelSerializer` in the startup
39 changes: 24 additions & 15 deletions example/model_serializer_example.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from pydantic import Field
from pydantic import Field, field_validator, ConfigDict

from panther import Panther, status
from panther.app import API
from panther.db import Model
from panther.request import Request
from panther.response import Response
from panther.serializer import ModelSerializer


Expand All @@ -15,18 +11,31 @@ class User(Model):
last_name: str = Field(default='', min_length=4)


class UserSerializer(metaclass=ModelSerializer, model=User):
fields = ['username', 'first_name', 'last_name']
# required_fields = ['first_name']
class UserSerializer(ModelSerializer):
"""
Hello this is doc
"""
model_config = ConfigDict(str_to_upper=False) # Has more priority
age: int = Field(default=20)
is_male: bool
username: str

class Config:
str_to_upper = True
model = User
fields = ['username', 'first_name', 'last_name']
required_fields = ['first_name']

@API(input_model=UserSerializer)
async def model_serializer_example(request: Request):
return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED)
@field_validator('username', mode='before')
def validate_username(cls, username):
print(f'{username=}')
return username

def create(self) -> type[Model]:
print('UserSerializer.create()')
return super().create()

url_routing = {
'': model_serializer_example,
}

app = Panther(__name__, configs=__name__, urls=url_routing)
serialized = UserSerializer(username='alirn', first_name='Ali', last_name='RnRn', is_male=1)
print(serialized)
# serialized.create()
2 changes: 1 addition & 1 deletion panther/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from panther.main import Panther # noqa: F401

__version__ = '3.8.2'
__version__ = '3.9.0'


def version():
Expand Down
3 changes: 3 additions & 0 deletions panther/cli/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ async def info_api(request: Request):

TEMPLATE = {
'app': {
'__init__.py': '',
'apis.py': apis_py,
'models.py': models_py,
'serializers.py': serializers_py,
'throttling.py': throttling_py,
'urls.py': app_urls_py,
},
'core': {
'__init__.py': '',
'configs.py': configs_py,
'urls.py': urls_py,
},
Expand Down Expand Up @@ -161,6 +163,7 @@ async def info_api(request: Request):
"""

SINGLE_FILE_TEMPLATE = {
'__init__.py': '',
'main.py': single_main_py,
'.env': env,
'.gitignore': git_ignore,
Expand Down
Loading