panther 5.0.0b3__py3-none-any.whl → 5.0.0b5__py3-none-any.whl

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 (57) hide show
  1. panther/__init__.py +1 -1
  2. panther/_load_configs.py +46 -37
  3. panther/_utils.py +49 -34
  4. panther/app.py +96 -97
  5. panther/authentications.py +97 -50
  6. panther/background_tasks.py +98 -124
  7. panther/base_request.py +16 -10
  8. panther/base_websocket.py +8 -8
  9. panther/caching.py +16 -80
  10. panther/cli/create_command.py +17 -16
  11. panther/cli/main.py +1 -1
  12. panther/cli/monitor_command.py +11 -6
  13. panther/cli/run_command.py +5 -71
  14. panther/cli/template.py +7 -7
  15. panther/cli/utils.py +58 -69
  16. panther/configs.py +70 -72
  17. panther/db/connections.py +30 -24
  18. panther/db/cursor.py +3 -1
  19. panther/db/models.py +26 -10
  20. panther/db/queries/base_queries.py +4 -5
  21. panther/db/queries/mongodb_queries.py +21 -21
  22. panther/db/queries/pantherdb_queries.py +1 -1
  23. panther/db/queries/queries.py +26 -8
  24. panther/db/utils.py +1 -1
  25. panther/events.py +25 -14
  26. panther/exceptions.py +2 -7
  27. panther/file_handler.py +1 -1
  28. panther/generics.py +74 -100
  29. panther/logging.py +2 -1
  30. panther/main.py +12 -13
  31. panther/middlewares/cors.py +67 -0
  32. panther/middlewares/monitoring.py +5 -3
  33. panther/openapi/urls.py +2 -2
  34. panther/openapi/utils.py +3 -3
  35. panther/openapi/views.py +20 -37
  36. panther/pagination.py +4 -2
  37. panther/panel/apis.py +2 -7
  38. panther/panel/urls.py +2 -6
  39. panther/panel/utils.py +9 -5
  40. panther/panel/views.py +13 -22
  41. panther/permissions.py +2 -1
  42. panther/request.py +2 -1
  43. panther/response.py +101 -94
  44. panther/routings.py +12 -12
  45. panther/serializer.py +20 -43
  46. panther/test.py +73 -58
  47. panther/throttling.py +68 -3
  48. panther/utils.py +5 -11
  49. panther-5.0.0b5.dist-info/METADATA +188 -0
  50. panther-5.0.0b5.dist-info/RECORD +75 -0
  51. panther/monitoring.py +0 -34
  52. panther-5.0.0b3.dist-info/METADATA +0 -223
  53. panther-5.0.0b3.dist-info/RECORD +0 -75
  54. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
  55. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
  56. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
  57. {panther-5.0.0b3.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
panther/panel/views.py CHANGED
@@ -4,22 +4,24 @@ from panther import status
4
4
  from panther.app import API, GenericAPI
5
5
  from panther.configs import config
6
6
  from panther.db.models import BaseUser
7
- from panther.exceptions import RedirectAPIError, AuthenticationAPIError
7
+ from panther.exceptions import AuthenticationAPIError, RedirectAPIError
8
8
  from panther.panel.middlewares import RedirectToSlashMiddleware
9
- from panther.panel.utils import get_models, clean_model_schema
9
+ from panther.panel.utils import clean_model_schema, get_models
10
10
  from panther.permissions import BasePermission
11
11
  from panther.request import Request
12
- from panther.response import TemplateResponse, Response, Cookie, RedirectResponse
12
+ from panther.response import Cookie, RedirectResponse, Response, TemplateResponse
13
13
 
14
14
  logger = logging.getLogger('panther')
15
15
 
16
16
 
17
17
  class AdminPanelPermission(BasePermission):
18
+ """We didn't want to change AUTHENTICATION class of user, so we use permission class for this purpose."""
19
+
18
20
  @classmethod
19
21
  async def authorization(cls, request: Request) -> bool:
20
22
  from panther.authentications import CookieJWTAuthentication
21
23
 
22
- try: # We don't want to set AUTHENTICATION class, so we have to use permission classes
24
+ try:
23
25
  await CookieJWTAuthentication.authentication(request=request)
24
26
  return True
25
27
  except AuthenticationAPIError:
@@ -50,22 +52,14 @@ class LoginView(GenericAPI):
50
52
  status_code=status.HTTP_400_BAD_REQUEST,
51
53
  context={'error': 'Authentication Error'},
52
54
  )
53
- tokens = JWTAuthentication.login(user.id)
55
+ tokens = await JWTAuthentication.login(user=user)
54
56
  return RedirectResponse(
55
57
  url=request.query_params.get('redirect_to', '..'),
56
58
  status_code=status.HTTP_302_FOUND,
57
59
  set_cookies=[
58
- Cookie(
59
- key='access_token',
60
- value=tokens['access_token'],
61
- max_age=config.JWT_CONFIG.life_time
62
- ),
63
- Cookie(
64
- key='refresh_token',
65
- value=tokens['refresh_token'],
66
- max_age=config.JWT_CONFIG.refresh_life_time
67
- )
68
- ]
60
+ Cookie(key='access_token', value=tokens['access_token'], max_age=config.JWT_CONFIG.life_time),
61
+ Cookie(key='refresh_token', value=tokens['refresh_token'], max_age=config.JWT_CONFIG.refresh_life_time),
62
+ ],
69
63
  )
70
64
 
71
65
 
@@ -93,7 +87,7 @@ class TableView(GenericAPI):
93
87
  'fields': clean_model_schema(model.schema()),
94
88
  'tables': get_models(),
95
89
  'records': Response.prepare_data(data),
96
- }
90
+ },
97
91
  )
98
92
 
99
93
 
@@ -108,7 +102,7 @@ class CreateView(GenericAPI):
108
102
  context={
109
103
  'fields': clean_model_schema(model.schema()),
110
104
  'tables': get_models(),
111
- }
105
+ },
112
106
  )
113
107
 
114
108
  async def post(self, request: Request, index: int):
@@ -129,10 +123,7 @@ class DetailView(GenericAPI):
129
123
  obj = await model.find_one_or_raise(id=document_id)
130
124
  return TemplateResponse(
131
125
  name='detail.html',
132
- context={
133
- 'fields': clean_model_schema(model.schema()),
134
- 'data': obj.model_dump()
135
- }
126
+ context={'fields': clean_model_schema(model.schema()), 'data': obj.model_dump()},
136
127
  )
137
128
 
138
129
  async def put(self, request: Request, index: int, document_id: str):
panther/permissions.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from panther.request import Request
2
+ from panther.websocket import Websocket
2
3
 
3
4
 
4
5
  class BasePermission:
5
6
  @classmethod
6
- async def authorization(cls, request: Request) -> bool:
7
+ async def authorization(cls, request: Request | Websocket) -> bool:
7
8
  return True
8
9
 
9
10
 
panther/request.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import logging
2
- from typing import Literal, Callable
2
+ from collections.abc import Callable
3
+ from typing import Literal
3
4
  from urllib.parse import parse_qsl
4
5
 
5
6
  import orjson as json
panther/response.py CHANGED
@@ -1,9 +1,15 @@
1
1
  import asyncio
2
+ import logging
3
+ from collections.abc import AsyncGenerator, Generator
2
4
  from dataclasses import dataclass
3
5
  from http import cookies
4
6
  from sys import version_info
5
7
  from types import NoneType
6
- from typing import Generator, AsyncGenerator, Any, Type, Literal
8
+ from typing import Any, Literal
9
+
10
+ import jinja2
11
+
12
+ from panther.exceptions import APIError
7
13
 
8
14
  if version_info >= (3, 11):
9
15
  from typing import LiteralString
@@ -13,19 +19,23 @@ else:
13
19
  LiteralString = TypeVar('LiteralString')
14
20
 
15
21
  import orjson as json
22
+ from pantherdb import Cursor as PantherDBCursor
16
23
  from pydantic import BaseModel
17
24
 
18
25
  from panther import status
19
- from panther.configs import config
20
26
  from panther._utils import to_async_generator
27
+ from panther.configs import config
21
28
  from panther.db.cursor import Cursor
22
- from pantherdb import Cursor as PantherDBCursor
23
29
  from panther.pagination import Pagination
24
30
 
25
- ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | Type[BaseModel]
31
+ ResponseDataTypes = (
32
+ list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | type[BaseModel]
33
+ )
26
34
  IterableDataTypes = list | tuple | set | Cursor | PantherDBCursor
27
35
  StreamingDataTypes = Generator | AsyncGenerator
28
36
 
37
+ logger = logging.getLogger('panther')
38
+
29
39
 
30
40
  @dataclass(slots=True)
31
41
  class Cookie:
@@ -44,6 +54,7 @@ class Cookie:
44
54
  `lax` is the default behavior if not specified.
45
55
  expires: [Deprecated] In HTTP version 1.1, `expires` was deprecated and replaced with the easier-to-use `max-age`
46
56
  """
57
+
47
58
  key: str
48
59
  value: str
49
60
  domain: str = None
@@ -63,7 +74,7 @@ class Response:
63
74
  status_code: int = status.HTTP_200_OK,
64
75
  headers: dict | None = None,
65
76
  pagination: Pagination | None = None,
66
- set_cookies: list[Cookie] | None = None
77
+ set_cookies: Cookie | list[Cookie] | None = None,
67
78
  ):
68
79
  """
69
80
  :param data: should be an instance of ResponseDataTypes
@@ -71,19 +82,20 @@ class Response:
71
82
  :param headers: should be dict of headers
72
83
  :param pagination: an instance of Pagination or None
73
84
  The `pagination.template()` method will be used
74
- :param set_cookies: list of cookies you want to set on the client
75
- Set the `max_age` to `0` if you want to delete a cookie
85
+ :param set_cookies: single cookie or list of cookies you want to set on the client.
86
+ Set the `max-age` to `0` if you want to delete a cookie.
76
87
  """
77
- headers = headers or {}
78
- self.pagination: Pagination | None = pagination
79
- if isinstance(data, Cursor):
88
+ if isinstance(data, (Cursor, PantherDBCursor)):
80
89
  data = list(data)
81
- self.initial_data = data
82
- self.data = self.prepare_data(data=data)
83
- self.status_code = self.check_status_code(status_code=status_code)
90
+ self.data = data
91
+ self.status_code = status_code
92
+ self.headers = {'Content-Type': self.content_type} | (headers or {})
93
+ self.pagination: Pagination | None = pagination
84
94
  self.cookies = None
85
95
  if set_cookies:
86
96
  c = cookies.SimpleCookie()
97
+ if not isinstance(set_cookies, list):
98
+ set_cookies = [set_cookies]
87
99
  for cookie in set_cookies:
88
100
  c[cookie.key] = cookie.value
89
101
  c[cookie.key]['path'] = cookie.path
@@ -95,78 +107,63 @@ class Response:
95
107
  if cookie.max_age is not None:
96
108
  c[cookie.key]['max-age'] = cookie.max_age
97
109
  self.cookies = [(b'Set-Cookie', cookie.OutputString().encode()) for cookie in c.values()]
98
- self.headers = headers
110
+
111
+ def __str__(self):
112
+ if len(data := str(self.data)) > 30:
113
+ data = f'{data:.27}...'
114
+ return f'Response(status_code={self.status_code}, data={data})'
115
+
116
+ __repr__ = __str__
99
117
 
100
118
  @property
101
119
  def body(self) -> bytes:
120
+ def default(obj: Any):
121
+ if isinstance(obj, BaseModel):
122
+ return obj.model_dump()
123
+ raise TypeError(f'Type {type(obj)} not serializable')
124
+
102
125
  if isinstance(self.data, bytes):
103
126
  return self.data
104
-
105
127
  if self.data is None:
106
128
  return b''
107
- return json.dumps(self.data)
108
-
109
- @property
110
- def headers(self) -> dict:
111
- return {
112
- 'Content-Type': self.content_type,
113
- 'Content-Length': len(self.body),
114
- 'Access-Control-Allow-Origin': '*',
115
- } | self._headers
129
+ return json.dumps(self.data, default=default)
116
130
 
117
131
  @property
118
- def bytes_headers(self) -> list[tuple[bytes]]:
119
- result = [(k.encode(), str(v).encode()) for k, v in (self.headers or {}).items()]
132
+ def bytes_headers(self) -> list[tuple[bytes, bytes]]:
133
+ headers = {'Content-Length': len(self.body)} | self.headers
134
+ result = [(k.encode(), str(v).encode()) for k, v in headers.items()]
120
135
  if self.cookies:
121
- result.extend(self.cookies)
136
+ result += self.cookies
122
137
  return result
123
138
 
124
- @headers.setter
125
- def headers(self, headers: dict):
126
- self._headers = headers
127
-
128
- @classmethod
129
- def prepare_data(cls, data: Any):
130
- """Make sure the response data is only ResponseDataTypes or Iterable of ResponseDataTypes"""
131
- if isinstance(data, (int | float | str | bool | bytes | NoneType)):
132
- return data
133
-
134
- elif isinstance(data, dict):
135
- return {key: cls.prepare_data(value) for key, value in data.items()}
136
-
137
- elif issubclass(type(data), BaseModel):
138
- return data.model_dump()
139
-
140
- elif isinstance(data, IterableDataTypes):
141
- return [cls.prepare_data(d) for d in data]
142
-
143
- else:
144
- msg = f'Invalid Response Type: {type(data)}'
145
- raise TypeError(msg)
146
-
147
- @classmethod
148
- def check_status_code(cls, status_code: Any):
149
- if not isinstance(status_code, int):
150
- error = f'Response `status_code` Should Be `int`. (`{status_code}` is {type(status_code)})'
151
- raise TypeError(error)
152
- return status_code
153
-
154
- async def send_headers(self, send, /):
139
+ async def send(self, send, receive):
155
140
  await send({'type': 'http.response.start', 'status': self.status_code, 'headers': self.bytes_headers})
156
-
157
- async def send_body(self, send, receive, /):
158
141
  await send({'type': 'http.response.body', 'body': self.body, 'more_body': False})
159
142
 
160
- async def send(self, send, receive, /):
161
- await self.send_headers(send)
162
- await self.send_body(send, receive)
163
-
164
- def __str__(self):
165
- if len(data := str(self.data)) > 30:
166
- data = f'{data:.27}...'
167
- return f'Response(status_code={self.status_code}, data={data})'
168
-
169
- __repr__ = __str__
143
+ async def serialize_output(self, output_model: type[BaseModel]):
144
+ """Serializes response data using the given output_model."""
145
+
146
+ async def handle_output(obj):
147
+ output = output_model(**obj) if isinstance(obj, dict) else output_model(**obj.model_dump())
148
+ if hasattr(output_model, 'to_response'):
149
+ return await output.to_response(instance=obj, data=output.model_dump())
150
+ return output.model_dump()
151
+
152
+ if isinstance(self.data, dict) or isinstance(self.data, BaseModel):
153
+ return await handle_output(self.data)
154
+
155
+ if isinstance(self.data, IterableDataTypes):
156
+ results = []
157
+ for d in self.data:
158
+ if isinstance(d, dict) or isinstance(d, BaseModel):
159
+ results.append(await handle_output(d))
160
+ else:
161
+ msg = 'Type of Response data is not match with `output_model`.\n*hint: You may want to remove `output_model`'
162
+ raise TypeError(msg)
163
+ return results
164
+
165
+ msg = 'Type of Response data is not match with `output_model`.\n*hint: You may want to remove `output_model`'
166
+ raise TypeError(msg)
170
167
 
171
168
 
172
169
  class StreamingResponse(Response):
@@ -181,27 +178,21 @@ class StreamingResponse(Response):
181
178
  if message['type'] == 'http.disconnect':
182
179
  self.connection_closed = True
183
180
 
184
- def prepare_data(self, data: any) -> AsyncGenerator:
185
- if isinstance(data, AsyncGenerator):
186
- return data
187
- elif isinstance(data, Generator):
188
- return to_async_generator(data)
189
- msg = f'Invalid Response Type: {type(data)}'
190
- raise TypeError(msg)
191
-
192
181
  @property
193
- def headers(self) -> dict:
194
- return {
195
- 'Content-Type': self.content_type,
196
- 'Access-Control-Allow-Origin': '*',
197
- } | self._headers
198
-
199
- @headers.setter
200
- def headers(self, headers: dict):
201
- self._headers = headers
182
+ def bytes_headers(self) -> list[tuple[bytes, bytes]]:
183
+ result = [(k.encode(), str(v).encode()) for k, v in self.headers.items()]
184
+ if self.cookies:
185
+ result += self.cookies
186
+ return result
202
187
 
203
188
  @property
204
189
  async def body(self) -> AsyncGenerator:
190
+ if not isinstance(self.data, (Generator, AsyncGenerator)):
191
+ raise TypeError(f'Type {type(self.data)} is not streamable, should be `Generator` or `AsyncGenerator`.')
192
+
193
+ if isinstance(self.data, Generator):
194
+ self.data = to_async_generator(self.data)
195
+
205
196
  async for chunk in self.data:
206
197
  if isinstance(chunk, bytes):
207
198
  yield chunk
@@ -210,8 +201,11 @@ class StreamingResponse(Response):
210
201
  else:
211
202
  yield json.dumps(chunk)
212
203
 
213
- async def send_body(self, send, receive, /):
214
- asyncio.create_task(self.listen_to_disconnection(receive))
204
+ async def send(self, send, receive):
205
+ # Send Headers
206
+ await send({'type': 'http.response.start', 'status': self.status_code, 'headers': self.bytes_headers})
207
+ # Send Body as chunks
208
+ asyncio.create_task(self.listen_to_disconnection(receive=receive))
215
209
  async for chunk in self.body:
216
210
  if self.connection_closed:
217
211
  break
@@ -242,13 +236,13 @@ class PlainTextResponse(Response):
242
236
 
243
237
  class TemplateResponse(HTMLResponse):
244
238
  """
245
- You may want to declare `TEMPLATES_DIR` in your configs
239
+ You may want to declare `TEMPLATES_DIR` in your configs, default is '.'
246
240
 
247
241
  Example:
248
242
  TEMPLATES_DIR = 'templates/'
249
- or
250
- TEMPLATES_DIR = '.'
243
+
251
244
  """
245
+
252
246
  def __init__(
253
247
  self,
254
248
  source: str | LiteralString | NoneType = None,
@@ -265,7 +259,20 @@ class TemplateResponse(HTMLResponse):
265
259
  :param status_code: should be int
266
260
  """
267
261
  if name:
268
- template = config.JINJA_ENVIRONMENT.get_template(name=name)
262
+ try:
263
+ template = config.JINJA_ENVIRONMENT.get_template(name=name)
264
+ except jinja2.exceptions.TemplateNotFound:
265
+ loaded_path = ' - '.join(
266
+ ' - '.join(loader.searchpath)
267
+ for loader in config.JINJA_ENVIRONMENT.loader.loaders
268
+ if isinstance(loader, jinja2.loaders.FileSystemLoader)
269
+ )
270
+ error = (
271
+ f'`{name}` Template Not Found.\n'
272
+ f'* Make sure `TEMPLATES_DIR` in your configs is correct, Current is {loaded_path}'
273
+ )
274
+ logger.error(error)
275
+ raise APIError
269
276
  else:
270
277
  template = config.JINJA_ENVIRONMENT.from_string(source=source)
271
278
  super().__init__(
@@ -281,7 +288,7 @@ class RedirectResponse(Response):
281
288
  url: str,
282
289
  headers: dict | None = None,
283
290
  status_code: int = status.HTTP_307_TEMPORARY_REDIRECT,
284
- set_cookies: list[Cookie] | None = None
291
+ set_cookies: list[Cookie] | None = None,
285
292
  ):
286
293
  headers = headers or {}
287
294
  headers['Location'] = url
panther/routings.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import re
2
+ import types
2
3
  from collections import Counter
3
4
  from collections.abc import Callable, Mapping, MutableMapping
4
5
  from copy import deepcopy
@@ -37,10 +38,12 @@ def _flattening_urls(data: dict | Callable, url: str = ''):
37
38
  def _is_url_endpoint_valid(url: str, endpoint: Callable):
38
39
  if endpoint is ...:
39
40
  raise PantherError(f"URL Can't Point To Ellipsis. ('{url}' -> ...)")
40
- elif endpoint is None:
41
+ if endpoint is None:
41
42
  raise PantherError(f"URL Can't Point To None. ('{url}' -> None)")
42
- elif url and not re.match(r'^[a-zA-Z<>0-9_/-]+$', url):
43
+ if url and not re.match(r'^[a-zA-Z<>0-9_/-]+$', url):
43
44
  raise PantherError(f"URL Is Not Valid. --> '{url}'")
45
+ elif isinstance(endpoint, types.ModuleType):
46
+ raise PantherError(f"URL Can't Point To Module. --> '{url}'")
44
47
 
45
48
 
46
49
  def finalize_urls(urls: dict) -> dict:
@@ -64,7 +67,7 @@ def finalize_urls(urls: dict) -> dict:
64
67
  return final_urls
65
68
 
66
69
 
67
- def check_urls_path_variables(urls: dict, path: str = '', ) -> None:
70
+ def check_urls_path_variables(urls: dict, path: str = '') -> None:
68
71
  middle_route_error = []
69
72
  last_route_error = []
70
73
  for key, value in urls.items():
@@ -79,13 +82,11 @@ def check_urls_path_variables(urls: dict, path: str = '', ) -> None:
79
82
 
80
83
  if len(middle_route_error) > 1:
81
84
  msg = '\n\t- ' + '\n\t- '.join(e for e in middle_route_error)
82
- raise PantherError(
83
- f"URLs can't have same-level path variables that point to a dict: {msg}")
85
+ raise PantherError(f"URLs can't have same-level path variables that point to a dict: {msg}")
84
86
 
85
87
  if len(last_route_error) > 1:
86
88
  msg = '\n\t- ' + '\n\t- '.join(e for e in last_route_error)
87
- raise PantherError(
88
- f"URLs can't have same-level path variables that point to an endpoint: {msg}")
89
+ raise PantherError(f"URLs can't have same-level path variables that point to an endpoint: {msg}")
89
90
 
90
91
 
91
92
  def _merge(destination: MutableMapping, *sources) -> MutableMapping:
@@ -185,10 +186,9 @@ def find_endpoint(path: str) -> tuple[Callable | None, str]:
185
186
  else:
186
187
  # `found` is None
187
188
  for key, value in urls.items():
188
- if key.startswith('<'):
189
- if isinstance(value, dict):
190
- found_path.append(key)
191
- urls = value
192
- break
189
+ if key.startswith('<') and isinstance(value, dict):
190
+ found_path.append(key)
191
+ urls = value
192
+ break
193
193
  else:
194
194
  return ENDPOINT_NOT_FOUND
panther/serializer.py CHANGED
@@ -1,8 +1,8 @@
1
1
  import typing
2
2
  from typing import Any
3
3
 
4
- from pydantic import create_model, BaseModel, ConfigDict
5
- from pydantic.fields import FieldInfo, Field
4
+ from pydantic import BaseModel, create_model
5
+ from pydantic.fields import Field, FieldInfo
6
6
  from pydantic_core._pydantic_core import PydanticUndefined
7
7
 
8
8
  from panther.db import Model
@@ -12,13 +12,7 @@ from panther.request import Request
12
12
  class MetaModelSerializer:
13
13
  KNOWN_CONFIGS = ['model', 'fields', 'exclude', 'required_fields', 'optional_fields']
14
14
 
15
- def __new__(
16
- cls,
17
- cls_name: str,
18
- bases: tuple[type[typing.Any], ...],
19
- namespace: dict[str, typing.Any],
20
- **kwargs
21
- ):
15
+ def __new__(cls, cls_name: str, bases: tuple[type[typing.Any], ...], namespace: dict[str, typing.Any], **kwargs):
22
16
  if cls_name == 'ModelSerializer':
23
17
  # Put `model` and `request` to the main class with `create_model()`
24
18
  namespace['__annotations__'].pop('model')
@@ -45,7 +39,7 @@ class MetaModelSerializer:
45
39
  __base__=(cls.model_serializer, BaseModel),
46
40
  model=(typing.ClassVar[type[BaseModel]], config.model),
47
41
  request=(Request, Field(None, exclude=True)),
48
- **field_definitions
42
+ **field_definitions,
49
43
  )
50
44
 
51
45
  @classmethod
@@ -108,12 +102,11 @@ class MetaModelSerializer:
108
102
  raise AttributeError(msg) from None
109
103
 
110
104
  # Check `required_fields` and `optional_fields` together
111
- if (
112
- (config.optional_fields == '*' and config.required_fields != []) or
113
- (config.required_fields == '*' and config.optional_fields != [])
105
+ if (config.optional_fields == '*' and config.required_fields != []) or (
106
+ config.required_fields == '*' and config.optional_fields != []
114
107
  ):
115
108
  msg = (
116
- f"`{cls_name}.Config.optional_fields` and "
109
+ f'`{cls_name}.Config.optional_fields` and '
117
110
  f"`{cls_name}.Config.required_fields` can't include same fields at the same time"
118
111
  )
119
112
  raise AttributeError(msg) from None
@@ -122,7 +115,7 @@ class MetaModelSerializer:
122
115
  if optional == required:
123
116
  msg = (
124
117
  f"`{optional}` can't be in `{cls_name}.Config.optional_fields` and "
125
- f"`{cls_name}.Config.required_fields` at the same time"
118
+ f'`{cls_name}.Config.required_fields` at the same time'
126
119
  )
127
120
  raise AttributeError(msg) from None
128
121
 
@@ -151,7 +144,7 @@ class MetaModelSerializer:
151
144
  for field_name in config.fields:
152
145
  field_definitions[field_name] = (
153
146
  config.model.model_fields[field_name].annotation,
154
- config.model.model_fields[field_name]
147
+ config.model.model_fields[field_name],
155
148
  )
156
149
 
157
150
  # Apply `exclude`
@@ -183,10 +176,15 @@ class MetaModelSerializer:
183
176
 
184
177
  @classmethod
185
178
  def collect_model_config(cls, config: typing.Callable, namespace: dict) -> dict:
186
- return {
187
- attr: getattr(config, attr) for attr in dir(config)
188
- if not attr.startswith('__') and attr not in cls.KNOWN_CONFIGS
189
- } | namespace.pop('model_config', {}) | {'arbitrary_types_allowed': True}
179
+ return (
180
+ {
181
+ attr: getattr(config, attr)
182
+ for attr in dir(config)
183
+ if not attr.startswith('__') and attr not in cls.KNOWN_CONFIGS
184
+ }
185
+ | namespace.pop('model_config', {})
186
+ | {'arbitrary_types_allowed': True}
187
+ )
190
188
 
191
189
 
192
190
  class ModelSerializer(metaclass=MetaModelSerializer):
@@ -202,30 +200,9 @@ class ModelSerializer(metaclass=MetaModelSerializer):
202
200
  required_fields = ['first_name', 'last_name'] # Optional
203
201
  optional_fields = ['age'] # Optional
204
202
  """
203
+
205
204
  model: type[BaseModel]
206
205
  request: Request
207
206
 
208
- async def create(self, validated_data: dict):
209
- """
210
- validated_data = ModelSerializer.model_dump()
211
- """
212
- return await self.model.insert_one(validated_data)
213
-
214
- async def update(self, instance: Model, validated_data: dict):
215
- """
216
- instance = UpdateAPI.object()
217
- validated_data = ModelSerializer.model_dump()
218
- """
219
- await instance.update(validated_data)
220
- return instance
221
-
222
- async def partial_update(self, instance: Model, validated_data: dict):
223
- """
224
- instance = UpdateAPI.object()
225
- validated_data = ModelSerializer.model_dump(exclude_none=True)
226
- """
227
- await instance.update(validated_data)
228
- return instance
229
-
230
- async def prepare_response(self, instance: Any, data: dict) -> dict:
207
+ async def to_response(self, instance: Any, data: dict) -> dict:
231
208
  return data