panther 3.8.1__tar.gz → 3.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {panther-3.8.1 → panther-3.9.0}/PKG-INFO +1 -1
  2. {panther-3.8.1 → panther-3.9.0}/panther/__init__.py +1 -1
  3. {panther-3.8.1 → panther-3.9.0}/panther/cli/template.py +2 -0
  4. {panther-3.8.1 → panther-3.9.0}/panther/main.py +1 -0
  5. panther-3.9.0/panther/serializer.py +122 -0
  6. {panther-3.8.1 → panther-3.9.0}/panther.egg-info/PKG-INFO +1 -1
  7. {panther-3.8.1 → panther-3.9.0}/panther.egg-info/SOURCES.txt +1 -1
  8. panther-3.9.0/tests/test_serializer.py +317 -0
  9. panther-3.8.1/panther/serializer.py +0 -47
  10. panther-3.8.1/tests/test_model_serializer.py +0 -170
  11. {panther-3.8.1 → panther-3.9.0}/LICENSE +0 -0
  12. {panther-3.8.1 → panther-3.9.0}/README.md +0 -0
  13. {panther-3.8.1 → panther-3.9.0}/panther/_load_configs.py +0 -0
  14. {panther-3.8.1 → panther-3.9.0}/panther/_utils.py +0 -0
  15. {panther-3.8.1 → panther-3.9.0}/panther/app.py +0 -0
  16. {panther-3.8.1 → panther-3.9.0}/panther/authentications.py +0 -0
  17. {panther-3.8.1 → panther-3.9.0}/panther/background_tasks.py +0 -0
  18. {panther-3.8.1 → panther-3.9.0}/panther/base_request.py +0 -0
  19. {panther-3.8.1 → panther-3.9.0}/panther/base_websocket.py +0 -0
  20. {panther-3.8.1 → panther-3.9.0}/panther/caching.py +0 -0
  21. {panther-3.8.1 → panther-3.9.0}/panther/cli/__init__.py +0 -0
  22. {panther-3.8.1 → panther-3.9.0}/panther/cli/create_command.py +0 -0
  23. {panther-3.8.1 → panther-3.9.0}/panther/cli/main.py +0 -0
  24. {panther-3.8.1 → panther-3.9.0}/panther/cli/monitor_command.py +0 -0
  25. {panther-3.8.1 → panther-3.9.0}/panther/cli/run_command.py +0 -0
  26. {panther-3.8.1 → panther-3.9.0}/panther/cli/utils.py +0 -0
  27. {panther-3.8.1 → panther-3.9.0}/panther/configs.py +0 -0
  28. {panther-3.8.1 → panther-3.9.0}/panther/db/__init__.py +0 -0
  29. {panther-3.8.1 → panther-3.9.0}/panther/db/connection.py +0 -0
  30. {panther-3.8.1 → panther-3.9.0}/panther/db/models.py +0 -0
  31. {panther-3.8.1 → panther-3.9.0}/panther/db/queries/__init__.py +0 -0
  32. {panther-3.8.1 → panther-3.9.0}/panther/db/queries/mongodb_queries.py +0 -0
  33. {panther-3.8.1 → panther-3.9.0}/panther/db/queries/pantherdb_queries.py +0 -0
  34. {panther-3.8.1 → panther-3.9.0}/panther/db/queries/queries.py +0 -0
  35. {panther-3.8.1 → panther-3.9.0}/panther/db/utils.py +0 -0
  36. {panther-3.8.1 → panther-3.9.0}/panther/exceptions.py +0 -0
  37. {panther-3.8.1 → panther-3.9.0}/panther/file_handler.py +0 -0
  38. {panther-3.8.1 → panther-3.9.0}/panther/logging.py +0 -0
  39. {panther-3.8.1 → panther-3.9.0}/panther/middlewares/__init__.py +0 -0
  40. {panther-3.8.1 → panther-3.9.0}/panther/middlewares/base.py +0 -0
  41. {panther-3.8.1 → panther-3.9.0}/panther/middlewares/db.py +0 -0
  42. {panther-3.8.1 → panther-3.9.0}/panther/middlewares/redis.py +0 -0
  43. {panther-3.8.1 → panther-3.9.0}/panther/monitoring.py +0 -0
  44. {panther-3.8.1 → panther-3.9.0}/panther/panel/__init__.py +0 -0
  45. {panther-3.8.1 → panther-3.9.0}/panther/panel/apis.py +0 -0
  46. {panther-3.8.1 → panther-3.9.0}/panther/panel/urls.py +0 -0
  47. {panther-3.8.1 → panther-3.9.0}/panther/panel/utils.py +0 -0
  48. {panther-3.8.1 → panther-3.9.0}/panther/permissions.py +0 -0
  49. {panther-3.8.1 → panther-3.9.0}/panther/request.py +0 -0
  50. {panther-3.8.1 → panther-3.9.0}/panther/response.py +0 -0
  51. {panther-3.8.1 → panther-3.9.0}/panther/routings.py +0 -0
  52. {panther-3.8.1 → panther-3.9.0}/panther/status.py +0 -0
  53. {panther-3.8.1 → panther-3.9.0}/panther/test.py +0 -0
  54. {panther-3.8.1 → panther-3.9.0}/panther/throttling.py +0 -0
  55. {panther-3.8.1 → panther-3.9.0}/panther/utils.py +0 -0
  56. {panther-3.8.1 → panther-3.9.0}/panther/websocket.py +0 -0
  57. {panther-3.8.1 → panther-3.9.0}/panther.egg-info/dependency_links.txt +0 -0
  58. {panther-3.8.1 → panther-3.9.0}/panther.egg-info/entry_points.txt +0 -0
  59. {panther-3.8.1 → panther-3.9.0}/panther.egg-info/requires.txt +0 -0
  60. {panther-3.8.1 → panther-3.9.0}/panther.egg-info/top_level.txt +0 -0
  61. {panther-3.8.1 → panther-3.9.0}/pyproject.toml +0 -0
  62. {panther-3.8.1 → panther-3.9.0}/setup.cfg +0 -0
  63. {panther-3.8.1 → panther-3.9.0}/setup.py +0 -0
  64. {panther-3.8.1 → panther-3.9.0}/tests/test_authentication.py +0 -0
  65. {panther-3.8.1 → panther-3.9.0}/tests/test_background_tasks.py +0 -0
  66. {panther-3.8.1 → panther-3.9.0}/tests/test_caching.py +0 -0
  67. {panther-3.8.1 → panther-3.9.0}/tests/test_cli.py +0 -0
  68. {panther-3.8.1 → panther-3.9.0}/tests/test_database.py +0 -0
  69. {panther-3.8.1 → panther-3.9.0}/tests/test_mongodb.py +0 -0
  70. {panther-3.8.1 → panther-3.9.0}/tests/test_multipart.py +0 -0
  71. {panther-3.8.1 → panther-3.9.0}/tests/test_panel_apis.py +0 -0
  72. {panther-3.8.1 → panther-3.9.0}/tests/test_request.py +0 -0
  73. {panther-3.8.1 → panther-3.9.0}/tests/test_routing.py +0 -0
  74. {panther-3.8.1 → panther-3.9.0}/tests/test_run.py +0 -0
  75. {panther-3.8.1 → panther-3.9.0}/tests/test_simple_responses.py +0 -0
  76. {panther-3.8.1 → panther-3.9.0}/tests/test_status.py +0 -0
  77. {panther-3.8.1 → panther-3.9.0}/tests/test_utils.py +0 -0
  78. {panther-3.8.1 → panther-3.9.0}/tests/test_websockets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 3.8.1
3
+ Version: 3.9.0
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '3.8.1'
3
+ __version__ = '3.9.0'
4
4
 
5
5
 
6
6
  def version():
@@ -99,6 +99,7 @@ requirements = """panther==%s
99
99
 
100
100
  TEMPLATE = {
101
101
  'app': {
102
+ '__init__.py': '',
102
103
  'apis.py': apis_py,
103
104
  'models.py': models_py,
104
105
  'serializers.py': serializers_py,
@@ -106,6 +107,7 @@ TEMPLATE = {
106
107
  'urls.py': app_urls_py,
107
108
  },
108
109
  'core': {
110
+ '__init__.py': '',
109
111
  'configs.py': configs_py,
110
112
  'urls.py': urls_py,
111
113
  },
@@ -300,6 +300,7 @@ class Panther:
300
300
  async def _raise(self, send, *, status_code: int):
301
301
  await http_response(
302
302
  send,
303
+ headers={'content-type': 'application/json'},
303
304
  status_code=status_code,
304
305
  monitoring=self.monitoring,
305
306
  exception=True,
@@ -0,0 +1,122 @@
1
+ import typing
2
+
3
+ from pydantic import create_model, BaseModel
4
+ from pydantic.fields import FieldInfo
5
+ from pydantic_core._pydantic_core import PydanticUndefined
6
+
7
+ from panther.db import Model
8
+
9
+
10
+ class MetaModelSerializer:
11
+ KNOWN_CONFIGS = ['model', 'fields', 'required_fields']
12
+
13
+ def __new__(
14
+ cls,
15
+ cls_name: str,
16
+ bases: tuple[type[typing.Any], ...],
17
+ namespace: dict[str, typing.Any],
18
+ **kwargs
19
+ ):
20
+ if cls_name == 'ModelSerializer':
21
+ cls.model_serializer = type(cls_name, (), namespace)
22
+ return super().__new__(cls)
23
+
24
+ # 1. Initial Check
25
+ cls.check_config(cls_name=cls_name, namespace=namespace)
26
+ config = namespace.pop('Config')
27
+
28
+ # 2. Collect `Fields`
29
+ field_definitions = cls.collect_fields(config=config, namespace=namespace)
30
+
31
+ # 3. Collect `pydantic.model_config`
32
+ model_config = cls.collect_model_config(config=config, namespace=namespace)
33
+ namespace |= {'model_config': model_config}
34
+
35
+ # 4. Create a serializer
36
+ return create_model(
37
+ __model_name=cls_name,
38
+ __module__=namespace['__module__'],
39
+ __validators__=namespace,
40
+ __base__=(cls.model_serializer, BaseModel),
41
+ __doc__=namespace.get('__doc__'),
42
+ model=(typing.ClassVar, config.model),
43
+ **field_definitions
44
+ )
45
+
46
+ @classmethod
47
+ def check_config(cls, cls_name: str, namespace: dict) -> None:
48
+ module = namespace['__module__']
49
+ address = f'{module}.{cls_name}'
50
+
51
+ # Check `Config`
52
+ if (config := namespace.get('Config')) is None:
53
+ msg = f'`class Config` is required in {address}.'
54
+ raise AttributeError(msg) from None
55
+
56
+ # Check `model`
57
+ if (model := getattr(config, 'model', None)) is None:
58
+ msg = f'`{cls_name}.Config.model` is required.'
59
+ raise AttributeError(msg) from None
60
+
61
+ # Check `model` type
62
+ try:
63
+ if not issubclass(model, Model):
64
+ msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model`.'
65
+ raise AttributeError(msg) from None
66
+ except TypeError:
67
+ msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model`.'
68
+ raise AttributeError(msg) from None
69
+
70
+ # Check `fields`
71
+ if (fields := getattr(config, 'fields', None)) is None:
72
+ msg = f'`{cls_name}.Config.fields` is required.'
73
+ raise AttributeError(msg) from None
74
+
75
+ for field_name in fields:
76
+ if field_name not in model.model_fields:
77
+ msg = f'`{cls_name}.Config.fields.{field_name}` is not valid.'
78
+ raise AttributeError(msg) from None
79
+
80
+ # Check `required_fields`
81
+ if not hasattr(config, 'required_fields'):
82
+ config.required_fields = []
83
+
84
+ for required in config.required_fields:
85
+ if required not in config.fields:
86
+ msg = f'`{cls_name}.Config.required_fields.{required}` should be in `Config.fields` too.'
87
+ raise AttributeError(msg) from None
88
+
89
+ @classmethod
90
+ def collect_fields(cls, config: typing.Callable, namespace: dict) -> dict:
91
+ field_definitions = {}
92
+
93
+ # Define `fields`
94
+ for field_name in config.fields:
95
+ field_definitions[field_name] = (
96
+ config.model.model_fields[field_name].annotation,
97
+ config.model.model_fields[field_name]
98
+ )
99
+
100
+ # Apply `required_fields`
101
+ for required in config.required_fields:
102
+ field_definitions[required][1].default = PydanticUndefined
103
+
104
+ # Collect and Override `Class Fields`
105
+ for key, value in namespace.pop('__annotations__', {}).items():
106
+ field_info = namespace.pop(key, FieldInfo(required=True))
107
+ field_info.annotation = value
108
+ field_definitions[key] = (value, field_info)
109
+
110
+ return field_definitions
111
+
112
+ @classmethod
113
+ def collect_model_config(cls, config: typing.Callable, namespace: dict) -> dict:
114
+ return {
115
+ attr: getattr(config, attr) for attr in dir(config)
116
+ if not attr.startswith('__') and attr not in cls.KNOWN_CONFIGS
117
+ } | namespace.pop('model_config', {})
118
+
119
+
120
+ class ModelSerializer(metaclass=MetaModelSerializer):
121
+ def create(self) -> type[Model]:
122
+ return self.model.insert_one(self.model_dump())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 3.8.1
3
+ Version: 3.9.0
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -61,13 +61,13 @@ tests/test_background_tasks.py
61
61
  tests/test_caching.py
62
62
  tests/test_cli.py
63
63
  tests/test_database.py
64
- tests/test_model_serializer.py
65
64
  tests/test_mongodb.py
66
65
  tests/test_multipart.py
67
66
  tests/test_panel_apis.py
68
67
  tests/test_request.py
69
68
  tests/test_routing.py
70
69
  tests/test_run.py
70
+ tests/test_serializer.py
71
71
  tests/test_simple_responses.py
72
72
  tests/test_status.py
73
73
  tests/test_utils.py
@@ -0,0 +1,317 @@
1
+ from pathlib import Path
2
+ from unittest import TestCase
3
+
4
+ from pydantic import Field, ConfigDict
5
+ from pydantic import field_validator
6
+
7
+ from panther import Panther
8
+ from panther.app import API
9
+ from panther.db import Model
10
+ from panther.request import Request
11
+ from panther.serializer import ModelSerializer
12
+ from panther.test import APIClient
13
+
14
+
15
+ class Book(Model):
16
+ name: str
17
+ author: str = Field('default_author')
18
+ pages_count: int = Field(0)
19
+
20
+
21
+ class NotRequiredFieldsSerializer(ModelSerializer):
22
+ class Config:
23
+ model = Book
24
+ fields = ['author', 'pages_count']
25
+
26
+
27
+ class RequiredFieldsSerializer(ModelSerializer):
28
+ class Config:
29
+ model = Book
30
+ fields = ['name', 'author', 'pages_count']
31
+
32
+
33
+ class OnlyRequiredFieldsSerializer(ModelSerializer):
34
+ class Config:
35
+ model = Book
36
+ fields = ['name', 'author', 'pages_count']
37
+ required_fields = ['author', 'pages_count']
38
+
39
+
40
+ class WithValidatorsSerializer(ModelSerializer):
41
+ class Config:
42
+ model = Book
43
+ fields = ['name', 'author', 'pages_count']
44
+ required_fields = ['author', 'pages_count']
45
+
46
+ @field_validator('name', 'author', 'pages_count')
47
+ def validate(cls, field):
48
+ return 'validated'
49
+
50
+
51
+ class WithClassFieldsSerializer(ModelSerializer):
52
+ age: int = Field(10)
53
+
54
+ class Config:
55
+ model = Book
56
+ fields = ['name', 'author', 'pages_count']
57
+ required_fields = ['author', 'pages_count']
58
+
59
+
60
+ @API(input_model=NotRequiredFieldsSerializer)
61
+ async def not_required(request: Request):
62
+ return request.validated_data
63
+
64
+
65
+ @API(input_model=RequiredFieldsSerializer)
66
+ async def required(request: Request):
67
+ return request.validated_data
68
+
69
+
70
+ @API(input_model=OnlyRequiredFieldsSerializer)
71
+ async def only_required(request: Request):
72
+ return request.validated_data
73
+
74
+
75
+ @API(input_model=WithValidatorsSerializer)
76
+ async def with_validators(request: Request):
77
+ return request.validated_data
78
+
79
+
80
+ @API(input_model=WithClassFieldsSerializer)
81
+ async def with_class_fields(request: Request):
82
+ return request.validated_data
83
+
84
+
85
+ urls = {
86
+ 'not-required': not_required,
87
+ 'required': required,
88
+ 'only-required': only_required,
89
+ 'with-validators': with_validators,
90
+ 'class-fields': with_class_fields,
91
+ }
92
+
93
+
94
+ class TestModelSerializer(TestCase):
95
+ DB_PATH = 'test.pdb'
96
+
97
+ @classmethod
98
+ def setUpClass(cls) -> None:
99
+ global MIDDLEWARES
100
+ MIDDLEWARES = [
101
+ ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{cls.DB_PATH}'}),
102
+ ]
103
+ app = Panther(__name__, configs=__name__, urls=urls)
104
+ cls.client = APIClient(app=app)
105
+
106
+ def tearDown(self) -> None:
107
+ Path(self.DB_PATH).unlink(missing_ok=True)
108
+
109
+ # # # Class Usage
110
+
111
+ def test_not_required_fields_empty_response(self):
112
+ payload = {}
113
+ res = self.client.post('not-required', payload=payload)
114
+ assert res.status_code == 200
115
+ assert res.data == {'author': 'default_author', 'pages_count': 0}
116
+
117
+ def test_not_required_fields_full_response(self):
118
+ payload = {
119
+ 'author': 'ali',
120
+ 'pages_count': '12'
121
+ }
122
+ res = self.client.post('not-required', payload=payload)
123
+ assert res.status_code == 200
124
+ assert res.data == {'author': 'ali', 'pages_count': 12}
125
+
126
+ def test_required_fields_error(self):
127
+ payload = {}
128
+ res = self.client.post('required', payload=payload)
129
+ assert res.status_code == 400
130
+ assert res.data == {'name': 'Field required'}
131
+
132
+ def test_required_fields_success(self):
133
+ payload = {
134
+ 'name': 'how to code',
135
+ 'author': 'ali',
136
+ 'pages_count': '12'
137
+ }
138
+ res = self.client.post('required', payload=payload)
139
+ assert res.status_code == 200
140
+ assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}
141
+
142
+ def test_only_required_fields_error(self):
143
+ payload = {}
144
+ res = self.client.post('only-required', payload=payload)
145
+ assert res.status_code == 400
146
+ assert res.data == {'name': 'Field required', 'author': 'Field required', 'pages_count': 'Field required'}
147
+
148
+ def test_only_required_fields_success(self):
149
+ payload = {
150
+ 'name': 'how to code',
151
+ 'author': 'ali',
152
+ 'pages_count': '12'
153
+ }
154
+ res = self.client.post('only-required', payload=payload)
155
+ assert res.status_code == 200
156
+ assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}
157
+
158
+ def test_with_validators(self):
159
+ payload = {
160
+ 'name': 'how to code',
161
+ 'author': 'ali',
162
+ 'pages_count': '12'
163
+ }
164
+ res = self.client.post('with-validators', payload=payload)
165
+ assert res.status_code == 200
166
+ assert res.data == {'name': 'validated', 'author': 'validated', 'pages_count': 'validated'}
167
+
168
+ def test_with_class_fields_success(self):
169
+ # Test Default Value
170
+ payload1 = {
171
+ 'name': 'how to code',
172
+ 'author': 'ali',
173
+ 'pages_count': '12'
174
+ }
175
+ res = self.client.post('class-fields', payload=payload1)
176
+ assert res.status_code == 200
177
+ assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12, 'age': 10}
178
+
179
+ # Test Validation
180
+ payload2 = {
181
+ 'name': 'how to code',
182
+ 'author': 'ali',
183
+ 'pages_count': '12',
184
+ 'age': 30
185
+ }
186
+ res = self.client.post('class-fields', payload=payload2)
187
+ assert res.status_code == 200
188
+ assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12, 'age': 30}
189
+
190
+ # # # Class Definition
191
+
192
+ def test_define_class_without_meta(self):
193
+ try:
194
+ class Serializer0(ModelSerializer):
195
+ pass
196
+ except Exception as e:
197
+ assert isinstance(e, AttributeError)
198
+ assert e.args[0] == '`class Config` is required in tests.test_serializer.Serializer0.'
199
+ else:
200
+ assert False
201
+
202
+ def test_define_class_without_model(self):
203
+ try:
204
+ class Serializer1(ModelSerializer):
205
+ class Config:
206
+ pass
207
+ except Exception as e:
208
+ assert isinstance(e, AttributeError)
209
+ assert e.args[0] == '`Serializer1.Config.model` is required.'
210
+ else:
211
+ assert False
212
+
213
+ def test_define_class_without_fields(self):
214
+ try:
215
+ class Serializer2(ModelSerializer):
216
+ class Config:
217
+ model = Book
218
+ except Exception as e:
219
+ assert isinstance(e, AttributeError)
220
+ assert e.args[0] == '`Serializer2.Config.fields` is required.'
221
+ else:
222
+ assert False
223
+
224
+ def test_define_class_with_invalid_fields(self):
225
+ try:
226
+ class Serializer3(ModelSerializer):
227
+ class Config:
228
+ model = Book
229
+ fields = ['ok', 'no']
230
+ except Exception as e:
231
+ assert isinstance(e, AttributeError)
232
+ assert e.args[0] == '`Serializer3.Config.fields.ok` is not valid.'
233
+ else:
234
+ assert False
235
+
236
+ def test_define_class_with_invalid_required_fields(self):
237
+ try:
238
+ class Serializer4(ModelSerializer):
239
+ class Config:
240
+ model = Book
241
+ fields = ['name', 'author']
242
+ required_fields = ['pages_count']
243
+ except Exception as e:
244
+ assert isinstance(e, AttributeError)
245
+ assert e.args[0] == '`Serializer4.Config.required_fields.pages_count` should be in `Config.fields` too.'
246
+ else:
247
+ assert False
248
+
249
+ def test_define_class_with_invalid_model(self):
250
+ try:
251
+ class Serializer5(ModelSerializer):
252
+ class Config:
253
+ model = ModelSerializer
254
+ fields = ['name', 'author', 'pages_count']
255
+ except Exception as e:
256
+ assert isinstance(e, AttributeError)
257
+ assert e.args[0] == '`Serializer5.Config.model` is not subclass of `panther.db.Model`.'
258
+ else:
259
+ assert False
260
+
261
+ # # # Serializer Usage
262
+ def test_with_simple_model_config(self):
263
+ class Serializer(ModelSerializer):
264
+ model_config = ConfigDict(str_to_upper=True)
265
+
266
+ class Config:
267
+ model = Book
268
+ fields = ['name', 'author', 'pages_count']
269
+
270
+ serialized = Serializer(name='book', author='AliRn', pages_count='12')
271
+ assert serialized.name == 'BOOK'
272
+ assert serialized.author == 'ALIRN'
273
+ assert serialized.pages_count == 12
274
+
275
+ def test_with_inner_model_config(self):
276
+ class Serializer(ModelSerializer):
277
+ class Config:
278
+ str_to_upper = True
279
+ model = Book
280
+ fields = ['name', 'author', 'pages_count']
281
+
282
+ serialized = Serializer(name='book', author='AliRn', pages_count='12')
283
+ assert serialized.name == 'BOOK'
284
+ assert serialized.author == 'ALIRN'
285
+ assert serialized.pages_count == 12
286
+
287
+ def test_with_dual_model_config(self):
288
+ class Serializer(ModelSerializer):
289
+ model_config = ConfigDict(str_to_upper=False)
290
+
291
+ class Config:
292
+ str_to_upper = True
293
+ model = Book
294
+ fields = ['name', 'author', 'pages_count']
295
+
296
+ serialized = Serializer(name='book', author='AliRn', pages_count='12')
297
+ assert serialized.name == 'book'
298
+ assert serialized.author == 'AliRn'
299
+ assert serialized.pages_count == 12
300
+
301
+ def test_serializer_doc(self):
302
+ class Serializer1(ModelSerializer):
303
+ """Hello I'm Doc"""
304
+ class Config:
305
+ model = Book
306
+ fields = ['name', 'author', 'pages_count']
307
+
308
+ serialized = Serializer1(name='book', author='AliRn', pages_count='12')
309
+ assert serialized.__doc__ == 'Hello I\'m Doc'
310
+
311
+ class Serializer2(ModelSerializer):
312
+ class Config:
313
+ model = Book
314
+ fields = ['name', 'author', 'pages_count']
315
+
316
+ serialized = Serializer2(name='book', author='AliRn', pages_count='12')
317
+ assert serialized.__doc__ is None
@@ -1,47 +0,0 @@
1
- from pydantic import create_model
2
- from pydantic_core._pydantic_core import PydanticUndefined
3
-
4
-
5
- class ModelSerializer:
6
- def __new__(cls, *args, model=None, **kwargs):
7
- # Check `metaclass`
8
- if len(args) == 0:
9
- address = f'{cls.__module__}.{cls.__name__}'
10
- msg = f"you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> {address}"
11
- raise TypeError(msg)
12
-
13
- model_name = args[0]
14
- data = args[2]
15
- address = f'{data["__module__"]}.{model_name}'
16
-
17
- # Check `model`
18
- if model is None:
19
- msg = f"'model' required while using 'ModelSerializer' metaclass -> {address}"
20
- raise AttributeError(msg)
21
- # Check `fields`
22
- if 'fields' not in data:
23
- msg = f"'fields' required while using 'ModelSerializer' metaclass. -> {address}"
24
- raise AttributeError(msg) from None
25
-
26
- model_fields = model.model_fields
27
- field_definitions = {}
28
-
29
- # Collect `fields`
30
- for field_name in data['fields']:
31
- if field_name not in model_fields:
32
- msg = f"'{field_name}' is not in '{model.__name__}' -> {address}"
33
- raise AttributeError(msg) from None
34
- field_definitions[field_name] = (model_fields[field_name].annotation, model_fields[field_name])
35
-
36
- # Change `required_fields
37
- for required in data.get('required_fields', []):
38
- if required not in field_definitions:
39
- msg = f"'{required}' is in 'required_fields' but not in 'fields' -> {address}"
40
- raise AttributeError(msg) from None
41
- field_definitions[required][1].default = PydanticUndefined
42
-
43
- # Create Model
44
- return create_model(
45
- __model_name=model_name,
46
- **field_definitions
47
- )
@@ -1,170 +0,0 @@
1
- from pathlib import Path
2
- from unittest import TestCase
3
-
4
- from pydantic import Field
5
-
6
- from panther import Panther
7
- from panther.app import API
8
- from panther.db import Model
9
- from panther.request import Request
10
- from panther.serializer import ModelSerializer
11
- from panther.test import APIClient
12
-
13
-
14
- class Book(Model):
15
- name: str
16
- author: str = Field('default_author')
17
- pages_count: int = Field(0)
18
-
19
-
20
- class NotRequiredFieldsSerializer(metaclass=ModelSerializer, model=Book):
21
- fields = ['author', 'pages_count']
22
-
23
-
24
- class RequiredFieldsSerializer(metaclass=ModelSerializer, model=Book):
25
- fields = ['name', 'author', 'pages_count']
26
-
27
-
28
- class OnlyRequiredFieldsSerializer(metaclass=ModelSerializer, model=Book):
29
- fields = ['name', 'author', 'pages_count']
30
- required_fields = ['author', 'pages_count']
31
-
32
-
33
- @API(input_model=NotRequiredFieldsSerializer)
34
- async def not_required(request: Request):
35
- return request.validated_data
36
-
37
-
38
- @API(input_model=RequiredFieldsSerializer)
39
- async def required(request: Request):
40
- return request.validated_data
41
-
42
-
43
- @API(input_model=OnlyRequiredFieldsSerializer)
44
- async def only_required(request: Request):
45
- return request.validated_data
46
-
47
-
48
- urls = {
49
- 'not-required': not_required,
50
- 'required': required,
51
- 'only-required': only_required,
52
- }
53
-
54
-
55
- class TestModelSerializer(TestCase):
56
- DB_PATH = 'test.pdb'
57
-
58
- @classmethod
59
- def setUpClass(cls) -> None:
60
- global MIDDLEWARES
61
- MIDDLEWARES = [
62
- ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{cls.DB_PATH}'}),
63
- ]
64
- app = Panther(__name__, configs=__name__, urls=urls)
65
- cls.client = APIClient(app=app)
66
-
67
- def tearDown(self) -> None:
68
- Path(self.DB_PATH).unlink(missing_ok=True)
69
-
70
- def test_not_required_fields_empty_response(self):
71
- payload = {}
72
- res = self.client.post('not-required', payload=payload)
73
- assert res.status_code == 200
74
- assert res.data == {'author': 'default_author', 'pages_count': 0}
75
-
76
- def test_not_required_fields_full_response(self):
77
- payload = {
78
- 'author': 'ali',
79
- 'pages_count': '12'
80
- }
81
- res = self.client.post('not-required', payload=payload)
82
- assert res.status_code == 200
83
- assert res.data == {'author': 'ali', 'pages_count': 12}
84
-
85
- def test_required_fields_error(self):
86
- payload = {}
87
- res = self.client.post('required', payload=payload)
88
- assert res.status_code == 400
89
- assert res.data == {'name': 'Field required'}
90
-
91
- def test_required_fields_success(self):
92
- payload = {
93
- 'name': 'how to code',
94
- 'author': 'ali',
95
- 'pages_count': '12'
96
- }
97
- res = self.client.post('required', payload=payload)
98
- assert res.status_code == 200
99
- assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}
100
-
101
- def test_only_required_fields_error(self):
102
- payload = {}
103
- res = self.client.post('only-required', payload=payload)
104
- assert res.status_code == 400
105
- assert res.data == {'name': 'Field required', 'author': 'Field required', 'pages_count': 'Field required'}
106
-
107
- def test_only_required_fields_success(self):
108
- payload = {
109
- 'name': 'how to code',
110
- 'author': 'ali',
111
- 'pages_count': '12'
112
- }
113
- res = self.client.post('only-required', payload=payload)
114
- assert res.status_code == 200
115
- assert res.data == {'name': 'how to code', 'author': 'ali', 'pages_count': 12}
116
-
117
- def test_define_class_without_fields(self):
118
- try:
119
- class Serializer1(metaclass=ModelSerializer, model=Book):
120
- pass
121
- except Exception as e:
122
- assert isinstance(e, AttributeError)
123
- assert e.args[0] == "'fields' required while using 'ModelSerializer' metaclass. -> tests.test_model_serializer.Serializer1"
124
- else:
125
- assert False
126
-
127
- def test_define_class_with_invalid_fields(self):
128
- try:
129
- class Serializer2(metaclass=ModelSerializer, model=Book):
130
- fields = ['ok', 'no']
131
- except Exception as e:
132
- assert isinstance(e, AttributeError)
133
- assert e.args[0] == "'ok' is not in 'Book' -> tests.test_model_serializer.Serializer2"
134
- else:
135
- assert False
136
-
137
- def test_define_class_with_invalid_required_fields(self):
138
- try:
139
- class Serializer3(metaclass=ModelSerializer, model=Book):
140
- fields = ['name', 'author']
141
- required_fields = ['pages_count']
142
- except Exception as e:
143
- assert isinstance(e, AttributeError)
144
- assert e.args[0] == "'pages_count' is in 'required_fields' but not in 'fields' -> tests.test_model_serializer.Serializer3"
145
- else:
146
- assert False
147
-
148
- def test_define_class_without_model(self):
149
- try:
150
- class Serializer4(metaclass=ModelSerializer):
151
- fields = ['name', 'author']
152
- required_fields = ['pages_count']
153
- except Exception as e:
154
- assert isinstance(e, AttributeError)
155
- assert e.args[0] == "'model' required while using 'ModelSerializer' metaclass -> tests.test_model_serializer.Serializer4"
156
- else:
157
- assert False
158
-
159
- def test_define_class_without_metaclass(self):
160
- class Serializer5(ModelSerializer):
161
- fields = ['name', 'author']
162
- required_fields = ['pages_count']
163
-
164
- try:
165
- Serializer5(name='alice', author='bob')
166
- except Exception as e:
167
- assert isinstance(e, TypeError)
168
- assert e.args[0] == "you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> tests.test_model_serializer.Serializer5"
169
- else:
170
- assert False
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes