-
-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
321 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
You can write your `serializer` in 2 style: | ||
|
||
|
||
## Style 1 (Pydantic) | ||
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) | ||
``` | ||
|
||
## 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` | ||
|
||
```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) | ||
``` | ||
|
||
### Notes: | ||
1. In the example above `UserModelSerializer` only accepts the values of `fields` attribute | ||
|
||
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 | ||
|
||
3. If you want uses `required_fields` you have to put them in `fields` too. | ||
|
||
4. `fields` attribute is `required` when you are using `ModelSerializer` as `metaclass` | ||
|
||
5. `model=` is required when you are using `ModelSerializer` as `metaclass` | ||
|
||
6. You have to use `ModelSerializer` as `metaclass` (not as a parent) | ||
|
||
7. Panther is going to create a `pydantic` model as your `UserModelSerializer` in the startup |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from pydantic import Field | ||
|
||
from panther import status, Panther | ||
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 UserSerializer(metaclass=ModelSerializer, 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) | ||
|
||
|
||
url_routing = { | ||
'': model_serializer_example, | ||
} | ||
|
||
app = Panther(__name__, configs=__name__, urls=url_routing) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
from panther.main import Panther # noqa: F401 | ||
|
||
__version__ = '3.6.0' | ||
__version__ = '3.7.0' | ||
|
||
|
||
def version(): | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from pydantic import create_model | ||
from pydantic_core._pydantic_core import PydanticUndefined | ||
|
||
|
||
class ModelSerializer: | ||
def __new__(cls, *args, **kwargs): | ||
if len(args) == 0: | ||
msg = f"you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> {cls.__name__}" | ||
raise TypeError(msg) | ||
model_name = args[0] | ||
if 'model' not in kwargs: | ||
msg = f"'model' required while using 'ModelSerializer' metaclass -> {model_name}" | ||
raise AttributeError(msg) | ||
|
||
model_fields = kwargs['model'].model_fields | ||
field_definitions = {} | ||
if 'fields' not in args[2]: | ||
msg = f"'fields' required while using 'ModelSerializer' metaclass. -> {model_name}" | ||
raise AttributeError(msg) from None | ||
for field_name in args[2]['fields']: | ||
if field_name not in model_fields: | ||
msg = f"'{field_name}' is not in '{kwargs['model'].__name__}' -> {model_name}" | ||
raise AttributeError(msg) from None | ||
|
||
field_definitions[field_name] = (model_fields[field_name].annotation, model_fields[field_name]) | ||
for required in args[2].get('required_fields', []): | ||
if required not in field_definitions: | ||
msg = f"'{required}' is in 'required_fields' but not in 'fields' -> {model_name}" | ||
raise AttributeError(msg) from None | ||
field_definitions[required][1].default = PydanticUndefined | ||
return create_model( | ||
__model_name=model_name, | ||
**field_definitions | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import asyncio | ||
from pathlib import Path | ||
from unittest import TestCase | ||
|
||
from pydantic import Field | ||
|
||
from panther import Panther | ||
from panther.app import API | ||
from panther.configs import config | ||
from panther.db import Model | ||
from panther.request import Request | ||
from panther.serializer import ModelSerializer | ||
from panther.test import APIClient | ||
|
||
|
||
class Book(Model): | ||
name: str | ||
author: str = Field('default_author') | ||
pages_count: int = Field(0) | ||
|
||
|
||
class NotRequiredFieldsSerializer(metaclass=ModelSerializer, model=Book): | ||
fields = ['author', 'pages_count'] | ||
|
||
|
||
class RequiredFieldsSerializer(metaclass=ModelSerializer, model=Book): | ||
fields = ['name', 'author', 'pages_count'] | ||
|
||
|
||
class OnlyRequiredFieldsSerializer(metaclass=ModelSerializer, model=Book): | ||
fields = ['name', 'author', 'pages_count'] | ||
required_fields = ['author', 'pages_count'] | ||
|
||
|
||
@API(input_model=NotRequiredFieldsSerializer) | ||
async def not_required(request: Request): | ||
return request.validated_data | ||
|
||
|
||
@API(input_model=RequiredFieldsSerializer) | ||
async def required(request: Request): | ||
return request.validated_data | ||
|
||
|
||
@API(input_model=OnlyRequiredFieldsSerializer) | ||
async def only_required(request: Request): | ||
return request.validated_data | ||
|
||
|
||
urls = { | ||
'not-required': not_required, | ||
'required': required, | ||
'only-required': only_required, | ||
} | ||
|
||
|
||
class TestModelSerializer(TestCase): | ||
DB_PATH = 'test.pdb' | ||
|
||
@classmethod | ||
def setUpClass(cls) -> None: | ||
global MIDDLEWARES | ||
MIDDLEWARES = [ | ||
('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{cls.DB_PATH}'}), | ||
] | ||
app = Panther(__name__, configs=__name__, urls=urls) | ||
cls.client = APIClient(app=app) | ||
|
||
def tearDown(self) -> None: | ||
Path(self.DB_PATH).unlink(missing_ok=True) | ||
|
||
def test_not_required_fields_empty_response(self): | ||
payload = {} | ||
res = self.client.post('not-required', payload=payload) | ||
assert res.status_code == 200 | ||
assert res.data == {'author': 'default_author', 'pages_count': 0} | ||
|
||
def test_not_required_fields_full_response(self): | ||
payload = { | ||
'author': 'ali', | ||
'pages_count': '12' | ||
} | ||
res = self.client.post('not-required', payload=payload) | ||
assert res.status_code == 200 | ||
assert res.data == {'author': 'ali', 'pages_count': 12} | ||
|
||
def test_required_fields_error(self): | ||
payload = {} | ||
res = self.client.post('required', payload=payload) | ||
assert res.status_code == 400 | ||
assert res.data == {'name': 'Field required'} | ||
|
||
def test_required_fields_success(self): | ||
payload = { | ||
'name': 'how to code', | ||
'author': 'ali', | ||
'pages_count': '12' | ||
} | ||
res = self.client.post('required', payload=payload) | ||
assert res.status_code == 200 | ||
assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12} | ||
|
||
def test_only_required_fields_error(self): | ||
payload = {} | ||
res = self.client.post('only-required', payload=payload) | ||
assert res.status_code == 400 | ||
assert res.data == {'name': 'Field required', 'author': 'Field required', 'pages_count': 'Field required'} | ||
|
||
def test_only_required_fields_success(self): | ||
payload = { | ||
'name': 'how to code', | ||
'author': 'ali', | ||
'pages_count': '12' | ||
} | ||
res = self.client.post('only-required', payload=payload) | ||
assert res.status_code == 200 | ||
assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12} | ||
|
||
def test_define_class_without_fields(self): | ||
try: | ||
class Serializer1(metaclass=ModelSerializer, model=Book): | ||
pass | ||
except Exception as e: | ||
assert isinstance(e, AttributeError) | ||
assert e.args[0] == "'fields' required while using 'ModelSerializer' metaclass. -> Serializer1" | ||
else: | ||
assert False | ||
|
||
def test_define_class_with_invalid_fields(self): | ||
try: | ||
class Serializer2(metaclass=ModelSerializer, model=Book): | ||
fields = ['ok', 'no'] | ||
except Exception as e: | ||
assert isinstance(e, AttributeError) | ||
assert e.args[0] == "'ok' is not in 'Book' -> Serializer2" | ||
else: | ||
assert False | ||
|
||
def test_define_class_with_invalid_required_fields(self): | ||
try: | ||
class Serializer3(metaclass=ModelSerializer, model=Book): | ||
fields = ['name', 'author'] | ||
required_fields = ['pages_count'] | ||
except Exception as e: | ||
assert isinstance(e, AttributeError) | ||
assert e.args[0] == "'pages_count' is in 'required_fields' but not in 'fields' -> Serializer3" | ||
else: | ||
assert False | ||
|
||
def test_define_class_without_model(self): | ||
try: | ||
class Serializer4(metaclass=ModelSerializer): | ||
fields = ['name', 'author'] | ||
required_fields = ['pages_count'] | ||
except Exception as e: | ||
assert isinstance(e, AttributeError) | ||
assert e.args[0] == "'model' required while using 'ModelSerializer' metaclass -> Serializer4" | ||
else: | ||
assert False | ||
|
||
def test_define_class_without_metaclass(self): | ||
class Serializer5(ModelSerializer): | ||
fields = ['name', 'author'] | ||
required_fields = ['pages_count'] | ||
|
||
try: | ||
Serializer5(name='alice', author='bob') | ||
except Exception as e: | ||
assert isinstance(e, TypeError) | ||
assert e.args[0] == ("you should not inherit the 'ModelSerializer', " | ||
"you should use it as 'metaclass' -> Serializer5") | ||
else: | ||
assert False |