panther 5.0.0b4__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.
panther/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '5.0.0beta4'
3
+ __version__ = '5.0.0beta5'
4
4
 
5
5
 
6
6
  def version():
panther/app.py CHANGED
@@ -42,6 +42,7 @@ class API:
42
42
  methods: Specify the allowed methods.
43
43
  input_model: The `request.data` will be validated with this attribute, It will raise an
44
44
  `panther.exceptions.BadRequestAPIError` or put the validated data in the `request.validated_data`.
45
+ output_model: The `response.data` will be passed through this class to filter its attributes.
45
46
  output_schema: This attribute only used in creation of OpenAPI scheme which is available in `panther.openapi.urls`
46
47
  You may want to add its `url` to your urls.
47
48
  auth: It will authenticate the user with header of its request or raise an
@@ -59,6 +60,7 @@ class API:
59
60
  *,
60
61
  methods: list[Literal['GET', 'POST', 'PUT', 'PATCH', 'DELETE']] | None = None,
61
62
  input_model: type[ModelSerializer] | type[BaseModel] | None = None,
63
+ output_model: type[ModelSerializer] | type[BaseModel] | None = None,
62
64
  output_schema: OutputSchema | None = None,
63
65
  auth: bool = False,
64
66
  permissions: list[type[BasePermission]] | None = None,
@@ -69,21 +71,14 @@ class API:
69
71
  ):
70
72
  self.methods = {m.upper() for m in methods} if methods else {'GET', 'POST', 'PUT', 'PATCH', 'DELETE'}
71
73
  self.input_model = input_model
74
+ self.output_model = output_model
72
75
  self.output_schema = output_schema
73
76
  self.auth = auth
74
77
  self.permissions = permissions or []
75
78
  self.throttling = throttling
76
79
  self.cache = cache
77
- self.middlewares: list[[HTTPMiddleware]] | None = middlewares
80
+ self.middlewares = middlewares
78
81
  self.request: Request | None = None
79
- if kwargs.pop('output_model', None):
80
- deprecation_message = (
81
- traceback.format_stack(limit=2)[0]
82
- + '\nThe `output_model` argument has been removed in Panther v5 and is no longer available.'
83
- '\nPlease update your code to use the new approach. More info: '
84
- 'https://pantherpy.github.io/open_api/'
85
- )
86
- raise PantherError(deprecation_message)
87
82
  if kwargs.pop('cache_exp_time', None):
88
83
  deprecation_message = (
89
84
  traceback.format_stack(limit=2)[0]
@@ -182,6 +177,8 @@ class API:
182
177
  # 9. Clean Response
183
178
  if not isinstance(response, Response):
184
179
  response = Response(data=response)
180
+ if self.output_model and response.data:
181
+ response.data = await response.serialize_output(output_model=self.output_model)
185
182
  if response.pagination:
186
183
  response.data = await response.pagination.template(response.data)
187
184
 
@@ -228,6 +225,7 @@ class GenericAPI(metaclass=MetaGenericAPI):
228
225
  """
229
226
 
230
227
  input_model: type[ModelSerializer] | type[BaseModel] | None = None
228
+ output_model: type[ModelSerializer] | type[BaseModel] | None = None
231
229
  output_schema: OutputSchema | None = None
232
230
  auth: bool = False
233
231
  permissions: list[type[BasePermission]] | None = None
@@ -239,6 +237,7 @@ class GenericAPI(metaclass=MetaGenericAPI):
239
237
  # Creating API instance to validate the attributes.
240
238
  API(
241
239
  input_model=cls.input_model,
240
+ output_model=cls.output_model,
242
241
  output_schema=cls.output_schema,
243
242
  auth=cls.auth,
244
243
  permissions=cls.permissions,
@@ -279,6 +278,7 @@ class GenericAPI(metaclass=MetaGenericAPI):
279
278
 
280
279
  return await API(
281
280
  input_model=self.input_model,
281
+ output_model=self.output_model,
282
282
  output_schema=self.output_schema,
283
283
  auth=self.auth,
284
284
  permissions=self.permissions,
panther/db/connections.py CHANGED
@@ -73,6 +73,10 @@ class MongoDBConnection(BaseDatabaseConnection):
73
73
  def session(self):
74
74
  return self._database
75
75
 
76
+ @property
77
+ def client(self):
78
+ return self._client
79
+
76
80
 
77
81
  class PantherDBConnection(BaseDatabaseConnection):
78
82
  def init(self, path: str | None = None, encryption: bool = False):
@@ -90,12 +94,20 @@ class PantherDBConnection(BaseDatabaseConnection):
90
94
  def session(self):
91
95
  return self._connection
92
96
 
97
+ @property
98
+ def client(self):
99
+ return self._connection
100
+
93
101
 
94
102
  class DatabaseConnection(Singleton):
95
103
  @property
96
104
  def session(self):
97
105
  return config.DATABASE.session
98
106
 
107
+ @property
108
+ def client(self):
109
+ return config.DATABASE.client
110
+
99
111
 
100
112
  class RedisConnection(Singleton, _Redis):
101
113
  is_connected: bool = False
panther/db/cursor.py CHANGED
@@ -36,6 +36,9 @@ class Cursor(_Cursor):
36
36
  def __aiter__(self) -> Self:
37
37
  return self
38
38
 
39
+ def __iter__(self) -> Self:
40
+ return self
41
+
39
42
  async def next(self) -> Self:
40
43
  return await self.cls._create_model_instance(document=super().next())
41
44
 
panther/db/models.py CHANGED
@@ -37,7 +37,7 @@ def validate_object_id(value, handler):
37
37
  raise ValueError(msg) from e
38
38
 
39
39
 
40
- ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)]
40
+ ID = Annotated[str, WrapValidator(validate_object_id), PlainSerializer(lambda x: str(x), return_type=str)] | None
41
41
 
42
42
 
43
43
  class Model(PydanticBaseModel, Query):
@@ -46,7 +46,7 @@ class Model(PydanticBaseModel, Query):
46
46
  return
47
47
  config.MODELS.append(cls)
48
48
 
49
- id: ID | None = Field(None, validation_alias='_id', alias='_id')
49
+ id: ID = None
50
50
 
51
51
  @property
52
52
  def _id(self):
@@ -45,6 +45,8 @@ class BaseQuery:
45
45
  @classmethod
46
46
  async def _create_model_instance(cls, document: dict):
47
47
  """Prevent getting errors from document insertion"""
48
+ if '_id' in document:
49
+ document['id'] = document.pop('_id')
48
50
  try:
49
51
  return cls(**document)
50
52
  except ValidationError as validation_error:
@@ -104,7 +104,7 @@ class BaseMongoDBQuery(BaseQuery):
104
104
  @classmethod
105
105
  async def _create_field(cls, model: type, field_name: str, value: Any) -> Any:
106
106
  # Handle primary key field directly
107
- if field_name == '_id':
107
+ if field_name == 'id':
108
108
  return value
109
109
 
110
110
  if field_name not in model.model_fields:
@@ -155,6 +155,9 @@ class BaseMongoDBQuery(BaseQuery):
155
155
  @classmethod
156
156
  async def _create_model_instance(cls, document: dict) -> Self:
157
157
  """Prepares document and creates an instance of the model."""
158
+ if '_id' in document:
159
+ document['id'] = document.pop('_id')
160
+
158
161
  processed_document = {
159
162
  field_name: await cls._create_field(model=cls, field_name=field_name, value=field_value)
160
163
  for field_name, field_value in document.items()
panther/generics.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import contextlib
2
2
  import logging
3
+ from abc import abstractmethod
3
4
 
4
5
  from pantherdb import Cursor as PantherDBCursor
5
6
 
@@ -7,7 +8,9 @@ from panther import status
7
8
  from panther.app import GenericAPI
8
9
  from panther.configs import config
9
10
  from panther.db import Model
11
+ from panther.db.connections import MongoDBConnection
10
12
  from panther.db.cursor import Cursor
13
+ from panther.db.models import ID
11
14
  from panther.exceptions import APIError
12
15
  from panther.pagination import Pagination
13
16
  from panther.request import Request
@@ -21,56 +24,42 @@ with contextlib.suppress(ImportError):
21
24
  logger = logging.getLogger('panther')
22
25
 
23
26
 
24
- class ObjectRequired:
25
- def _check_object(self, instance):
26
- if instance and issubclass(type(instance), Model) is False:
27
- logger.critical(f'`{self.__class__.__name__}.object()` should return instance of a Model --> `find_one()`')
28
- raise APIError
29
-
30
- async def object(self, request: Request, **kwargs):
27
+ class RetrieveAPI(GenericAPI):
28
+ @abstractmethod
29
+ async def get_instance(self, request: Request, **kwargs) -> Model:
31
30
  """
32
- Used in `RetrieveAPI`, `UpdateAPI`, `DeleteAPI`
31
+ Should return an instance of Model, e.g. `await User.find_one()`
33
32
  """
34
- logger.error(f'`object()` method is not implemented in {self.__class__} .')
33
+ logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
35
34
  raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
36
35
 
36
+ async def get(self, request: Request, **kwargs):
37
+ instance = await self.get_instance(request=request, **kwargs)
38
+ return Response(data=instance, status_code=status.HTTP_200_OK)
39
+
37
40
 
38
- class CursorRequired:
39
- def _check_cursor(self, cursor):
40
- if isinstance(cursor, (Cursor, PantherDBCursor)) is False:
41
- logger.critical(f'`{self.__class__.__name__}.cursor()` should return a Cursor --> `find()`')
42
- raise APIError
41
+ class ListAPI(GenericAPI):
42
+ sort_fields: list[str] = []
43
+ search_fields: list[str] = []
44
+ filter_fields: list[str] = []
45
+ pagination: type[Pagination] | None = None
43
46
 
44
- async def cursor(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
47
+ async def get_query(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
45
48
  """
46
- Used in `ListAPI`
47
- Should return `.find()`
49
+ Should return a Cursor, e.g. `await User.find()`
48
50
  """
49
- logger.error(f'`cursor()` method is not implemented in {self.__class__} .')
51
+ logger.error(f'`get_query()` method is not implemented in {self.__class__} .')
50
52
  raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
51
53
 
52
-
53
- class RetrieveAPI(GenericAPI, ObjectRequired):
54
- async def get(self, request: Request, **kwargs):
55
- instance = await self.object(request=request, **kwargs)
56
- self._check_object(instance)
57
-
58
- return Response(data=instance, status_code=status.HTTP_200_OK)
59
-
60
-
61
- class ListAPI(GenericAPI, CursorRequired):
62
- sort_fields: list[str]
63
- search_fields: list[str]
64
- filter_fields: list[str]
65
- pagination: type[Pagination]
66
-
67
54
  async def get(self, request: Request, **kwargs):
68
55
  cursor, pagination = await self.prepare_cursor(request=request, **kwargs)
69
56
  return Response(data=cursor, pagination=pagination, status_code=status.HTTP_200_OK)
70
57
 
71
58
  async def prepare_cursor(self, request: Request, **kwargs) -> tuple[Cursor | PantherDBCursor, Pagination | None]:
72
- cursor = await self.cursor(request=request, **kwargs)
73
- self._check_cursor(cursor)
59
+ cursor = await self.get_query(request=request, **kwargs)
60
+ if not isinstance(cursor, (Cursor, PantherDBCursor)):
61
+ logger.error(f'`{self.__class__.__name__}.get_query()` should return a Cursor, e.g. `await Model.find()`')
62
+ raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
74
63
 
75
64
  query = {}
76
65
  query |= self.process_filters(query_params=request.query_params, cursor=cursor)
@@ -89,106 +78,88 @@ class ListAPI(GenericAPI, CursorRequired):
89
78
 
90
79
  def process_filters(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> dict:
91
80
  _filter = {}
92
- if hasattr(self, 'filter_fields'):
93
- for field in self.filter_fields:
94
- if field in query_params:
95
- if config.DATABASE.__class__.__name__ == 'MongoDBConnection':
96
- with contextlib.suppress(Exception):
97
- # Change type of the value if it is ObjectId
98
- if cursor.cls.model_fields[field].metadata[0].func.__name__ == 'validate_object_id':
99
- _filter[field] = bson.ObjectId(query_params[field])
100
- continue
101
- _filter[field] = query_params[field]
81
+ for field in self.filter_fields:
82
+ if field in query_params:
83
+ _filter[field] = query_params[field]
84
+ if isinstance(config.DATABASE, MongoDBConnection) and cursor.cls.model_fields[field].annotation == ID:
85
+ _filter[field] = bson.ObjectId(_filter[field])
102
86
  return _filter
103
87
 
104
88
  def process_search(self, query_params: dict) -> dict:
105
- if hasattr(self, 'search_fields') and 'search' in query_params:
106
- value = query_params['search']
107
- if config.DATABASE.__class__.__name__ == 'MongoDBConnection':
108
- if search := [{field: {'$regex': value}} for field in self.search_fields]:
109
- return {'$or': search}
110
- else:
111
- logger.warning(f'`?search={value} does not work well while using `PantherDB` as Database')
112
- return {field: value for field in self.search_fields}
113
- return {}
89
+ search_param = query_params.get('search')
90
+ if not self.search_fields or not search_param:
91
+ return {}
92
+ if isinstance(config.DATABASE, MongoDBConnection):
93
+ if search := [{field: {'$regex': search_param}} for field in self.search_fields]:
94
+ return {'$or': search}
95
+ return {field: search_param for field in self.search_fields}
114
96
 
115
97
  def process_sort(self, query_params: dict) -> list:
116
- if hasattr(self, 'sort_fields') and 'sort' in query_params:
117
- return [
118
- (field, -1 if param[0] == '-' else 1)
119
- for field in self.sort_fields
120
- for param in query_params['sort'].split(',')
121
- if field == param.removeprefix('-')
122
- ]
98
+ sort_param = query_params.get('sort')
99
+ if not self.sort_fields or not sort_param:
100
+ return []
101
+ return [
102
+ (field, -1 if param.startswith('-') else 1)
103
+ for param in sort_param.split(',')
104
+ for field in self.sort_fields
105
+ if field == param.removeprefix('-')
106
+ ]
123
107
 
124
108
  def process_pagination(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> Pagination | None:
125
- if hasattr(self, 'pagination'):
109
+ if self.pagination:
126
110
  return self.pagination(query_params=query_params, cursor=cursor)
127
111
 
128
112
 
129
113
  class CreateAPI(GenericAPI):
130
- input_model: type[ModelSerializer]
114
+ input_model: type[ModelSerializer] | None = None
131
115
 
132
116
  async def post(self, request: Request, **kwargs):
133
- instance = await request.validated_data.create(
134
- validated_data={
135
- field: getattr(request.validated_data, field)
136
- for field in request.validated_data.model_fields_set
137
- if field != 'request'
138
- },
139
- )
117
+ instance = await request.validated_data.model.insert_one(request.validated_data.model_dump())
140
118
  return Response(data=instance, status_code=status.HTTP_201_CREATED)
141
119
 
142
120
 
143
- class UpdateAPI(GenericAPI, ObjectRequired):
144
- input_model: type[ModelSerializer]
121
+ class UpdateAPI(GenericAPI):
122
+ input_model: type[ModelSerializer] | None = None
145
123
 
146
- async def put(self, request: Request, **kwargs):
147
- instance = await self.object(request=request, **kwargs)
148
- self._check_object(instance)
124
+ @abstractmethod
125
+ async def get_instance(self, request: Request, **kwargs) -> Model:
126
+ """
127
+ Should return an instance of Model, e.g. `await User.find_one()`
128
+ """
129
+ logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
130
+ raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
149
131
 
150
- await request.validated_data.update(
151
- instance=instance,
152
- validated_data=request.validated_data.model_dump(by_alias=True),
153
- )
132
+ async def put(self, request: Request, **kwargs):
133
+ instance = await self.get_instance(request=request, **kwargs)
134
+ await instance.update(request.validated_data.model_dump())
154
135
  return Response(data=instance, status_code=status.HTTP_200_OK)
155
136
 
156
137
  async def patch(self, request: Request, **kwargs):
157
- instance = await self.object(request=request, **kwargs)
158
- self._check_object(instance)
159
-
160
- await request.validated_data.partial_update(
161
- instance=instance,
162
- validated_data=request.validated_data.model_dump(exclude_none=True, by_alias=True),
163
- )
138
+ instance = await self.get_instance(request=request, **kwargs)
139
+ await instance.update(request.validated_data.model_dump(exclude_none=True))
164
140
  return Response(data=instance, status_code=status.HTTP_200_OK)
165
141
 
166
142
 
167
- class DeleteAPI(GenericAPI, ObjectRequired):
143
+ class DeleteAPI(GenericAPI):
144
+ @abstractmethod
145
+ async def get_instance(self, request: Request, **kwargs) -> Model:
146
+ """
147
+ Should return an instance of Model, e.g. `await User.find_one()`
148
+ """
149
+ logger.error(f'`get_instance()` method is not implemented in {self.__class__} .')
150
+ raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
151
+
168
152
  async def pre_delete(self, instance, request: Request, **kwargs):
153
+ """Hook for logic before deletion."""
169
154
  pass
170
155
 
171
156
  async def post_delete(self, instance, request: Request, **kwargs):
157
+ """Hook for logic after deletion."""
172
158
  pass
173
159
 
174
160
  async def delete(self, request: Request, **kwargs):
175
- instance = await self.object(request=request, **kwargs)
176
- self._check_object(instance)
177
-
161
+ instance = await self.get_instance(request=request, **kwargs)
178
162
  await self.pre_delete(instance, request=request, **kwargs)
179
163
  await instance.delete()
180
164
  await self.post_delete(instance, request=request, **kwargs)
181
-
182
165
  return Response(status_code=status.HTTP_204_NO_CONTENT)
183
-
184
-
185
- class ListCreateAPI(CreateAPI, ListAPI):
186
- pass
187
-
188
-
189
- class UpdateDeleteAPI(UpdateAPI, DeleteAPI):
190
- pass
191
-
192
-
193
- class RetrieveUpdateDeleteAPI(RetrieveAPI, UpdateAPI, DeleteAPI):
194
- pass
panther/response.py CHANGED
@@ -82,16 +82,15 @@ class Response:
82
82
  :param headers: should be dict of headers
83
83
  :param pagination: an instance of Pagination or None
84
84
  The `pagination.template()` method will be used
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
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.
87
87
  """
88
+ if isinstance(data, (Cursor, PantherDBCursor)):
89
+ data = list(data)
90
+ self.data = data
91
+ self.status_code = status_code
88
92
  self.headers = {'Content-Type': self.content_type} | (headers or {})
89
93
  self.pagination: Pagination | None = pagination
90
- if isinstance(data, Cursor):
91
- data = list(data)
92
- self.initial_data = data
93
- self.data = self.prepare_data(data=data)
94
- self.status_code = self.check_status_code(status_code=status_code)
95
94
  self.cookies = None
96
95
  if set_cookies:
97
96
  c = cookies.SimpleCookie()
@@ -109,13 +108,25 @@ class Response:
109
108
  c[cookie.key]['max-age'] = cookie.max_age
110
109
  self.cookies = [(b'Set-Cookie', cookie.OutputString().encode()) for cookie in c.values()]
111
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__
117
+
112
118
  @property
113
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
+
114
125
  if isinstance(self.data, bytes):
115
126
  return self.data
116
127
  if self.data is None:
117
128
  return b''
118
- return json.dumps(self.data)
129
+ return json.dumps(self.data, default=default)
119
130
 
120
131
  @property
121
132
  def bytes_headers(self) -> list[tuple[bytes, bytes]]:
@@ -125,42 +136,34 @@ class Response:
125
136
  result += self.cookies
126
137
  return result
127
138
 
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
139
  async def send(self, send, receive):
155
140
  await send({'type': 'http.response.start', 'status': self.status_code, 'headers': self.bytes_headers})
156
141
  await send({'type': 'http.response.body', 'body': self.body, 'more_body': False})
157
142
 
158
- def __str__(self):
159
- if len(data := str(self.data)) > 30:
160
- data = f'{data:.27}...'
161
- return f'Response(status_code={self.status_code}, data={data})'
162
-
163
- __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)
164
167
 
165
168
 
166
169
  class StreamingResponse(Response):
@@ -175,14 +178,6 @@ class StreamingResponse(Response):
175
178
  if message['type'] == 'http.disconnect':
176
179
  self.connection_closed = True
177
180
 
178
- def prepare_data(self, data: any) -> AsyncGenerator:
179
- if isinstance(data, AsyncGenerator):
180
- return data
181
- elif isinstance(data, Generator):
182
- return to_async_generator(data)
183
- msg = f'Invalid Response Type: {type(data)}'
184
- raise TypeError(msg)
185
-
186
181
  @property
187
182
  def bytes_headers(self) -> list[tuple[bytes, bytes]]:
188
183
  result = [(k.encode(), str(v).encode()) for k, v in self.headers.items()]
@@ -192,6 +187,12 @@ class StreamingResponse(Response):
192
187
 
193
188
  @property
194
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
+
195
196
  async for chunk in self.data:
196
197
  if isinstance(chunk, bytes):
197
198
  yield chunk
panther/serializer.py CHANGED
@@ -204,27 +204,5 @@ class ModelSerializer(metaclass=MetaModelSerializer):
204
204
  model: type[BaseModel]
205
205
  request: Request
206
206
 
207
- async def create(self, validated_data: dict):
208
- """
209
- validated_data = ModelSerializer.model_dump()
210
- """
211
- return await self.model.insert_one(validated_data)
212
-
213
- async def update(self, instance: Model, validated_data: dict):
214
- """
215
- instance = UpdateAPI.object()
216
- validated_data = ModelSerializer.model_dump()
217
- """
218
- await instance.update(validated_data)
219
- return instance
220
-
221
- async def partial_update(self, instance: Model, validated_data: dict):
222
- """
223
- instance = UpdateAPI.object()
224
- validated_data = ModelSerializer.model_dump(exclude_none=True)
225
- """
226
- await instance.update(validated_data)
227
- return instance
228
-
229
- async def prepare_response(self, instance: Any, data: dict) -> dict:
207
+ async def to_response(self, instance: Any, data: dict) -> dict:
230
208
  return data
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: panther
3
+ Version: 5.0.0b5
4
+ Summary: Fast & Friendly, Web Framework For Building Async APIs
5
+ Home-page: https://github.com/alirn76/panther
6
+ Author: Ali RajabNezhad
7
+ Author-email: alirn76@yahoo.com
8
+ License: BSD-3-Clause license
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: pantherdb~=2.3.0
18
+ Requires-Dist: orjson~=3.9.15
19
+ Requires-Dist: pydantic~=2.10.6
20
+ Requires-Dist: rich~=13.9.4
21
+ Requires-Dist: uvicorn~=0.34.0
22
+ Requires-Dist: pytz~=2025.2
23
+ Requires-Dist: Jinja2~=3.1
24
+ Requires-Dist: simple-ulid~=1.0.0
25
+ Requires-Dist: httptools~=0.6.4
26
+ Provides-Extra: full
27
+ Requires-Dist: redis==5.2.1; extra == "full"
28
+ Requires-Dist: motor~=3.7.0; extra == "full"
29
+ Requires-Dist: ipython~=9.0.2; extra == "full"
30
+ Requires-Dist: python-jose~=3.4.0; extra == "full"
31
+ Requires-Dist: ruff~=0.11.2; extra == "full"
32
+ Requires-Dist: websockets~=15.0.1; extra == "full"
33
+ Requires-Dist: cryptography~=44.0.2; extra == "full"
34
+ Requires-Dist: watchfiles~=1.0.4; extra == "full"
35
+ Provides-Extra: dev
36
+ Requires-Dist: ruff~=0.11.2; extra == "dev"
37
+ Requires-Dist: pytest~=8.3.5; extra == "dev"
38
+ Dynamic: author
39
+ Dynamic: author-email
40
+ Dynamic: classifier
41
+ Dynamic: description
42
+ Dynamic: description-content-type
43
+ Dynamic: home-page
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: provides-extra
47
+ Dynamic: requires-dist
48
+ Dynamic: requires-python
49
+ Dynamic: summary
50
+
51
+ [![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)
52
+
53
+ <div align="center">
54
+ <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="Panther Logo" width="450">
55
+
56
+ # Panther
57
+
58
+ **A Fast & Friendly Web Framework for Building Async APIs with Python 3.10+**
59
+
60
+ [📚 Documentation](https://pantherpy.github.io)
61
+ </div>
62
+
63
+ ---
64
+
65
+ ## 🐾 Why Choose Panther?
66
+
67
+ Panther is designed to be **fast**, **simple**, and **powerful**. Here's what makes it special:
68
+
69
+ - **One of the fastest Python frameworks** available ([Benchmark](https://www.techempower.com/benchmarks/#section=data-r23&l=zijzen-pa7&c=4))
70
+ - **File-based database** ([PantherDB](https://pypi.org/project/pantherdb/)) - No external database setup required
71
+ - **Document-oriented ODM** - Supports MongoDB & PantherDB with familiar syntax
72
+ - **API caching system** - In-memory and Redis support
73
+ - **OpenAPI/Swagger** - Auto-generated API documentation
74
+ - **WebSocket support** - Real-time communication out of the box
75
+ - **Authentication & Permissions** - Built-in security features
76
+ - **Background tasks** - Handle long-running operations
77
+ - **Middleware & Throttling** - Extensible and configurable
78
+
79
+ ---
80
+
81
+ ## Quick Start
82
+
83
+ ### Installation
84
+
85
+ ```bash
86
+ pip install panther
87
+ ```
88
+
89
+ - Create a `main.py` file with one of the examples below.
90
+
91
+ ### Your First API
92
+
93
+ Here's a simple REST API endpoint that returns a "Hello World" message:
94
+
95
+ ```python
96
+ from datetime import datetime, timedelta
97
+ from panther import status, Panther
98
+ from panther.app import GenericAPI
99
+ from panther.openapi.urls import url_routing as openapi_url_routing
100
+ from panther.response import Response
101
+
102
+ class HelloAPI(GenericAPI):
103
+ # Cache responses for 10 seconds
104
+ cache = timedelta(seconds=10)
105
+
106
+ def get(self):
107
+ current_time = datetime.now().isoformat()
108
+ return Response(
109
+ data={'message': f'Hello from Panther! 🐾 | {current_time}'},
110
+ status_code=status.HTTP_200_OK
111
+ )
112
+
113
+ # URL routing configuration
114
+ url_routing = {
115
+ '/': HelloAPI,
116
+ 'swagger/': openapi_url_routing, # Auto-generated API docs
117
+ }
118
+
119
+ # Create your Panther app
120
+ app = Panther(__name__, configs=__name__, urls=url_routing)
121
+ ```
122
+
123
+ ### WebSocket Echo Server
124
+
125
+ Here's a simple WebSocket echo server that sends back any message it receives:
126
+
127
+ ```python
128
+ from panther import Panther
129
+ from panther.app import GenericAPI
130
+ from panther.response import HTMLResponse
131
+ from panther.websocket import GenericWebsocket
132
+
133
+ class EchoWebsocket(GenericWebsocket):
134
+ async def connect(self, **kwargs):
135
+ await self.accept()
136
+ await self.send("Connected to Panther WebSocket!")
137
+
138
+ async def receive(self, data: str | bytes):
139
+ # Echo back the received message
140
+ await self.send(f"Echo: {data}")
141
+
142
+ class WebSocketPage(GenericAPI):
143
+ def get(self):
144
+ template = """
145
+ <h2>🐾 Panther WebSocket Echo Server</h2>
146
+ <input id="msg"><button onclick="s.send(msg.value)">Send</button>
147
+ <ul id="log"></ul>
148
+ <script>
149
+ const s = new WebSocket('ws://127.0.0.1:8000/ws');
150
+ s.onmessage = e => log.innerHTML += `<li><- ${msg.value}</li><li>-> ${e.data}</li>`;
151
+ </script>
152
+ """
153
+ return HTMLResponse(template)
154
+
155
+ url_routing = {
156
+ '': WebSocketPage,
157
+ 'ws': EchoWebsocket,
158
+ }
159
+ app = Panther(__name__, configs=__name__, urls=url_routing)
160
+ ```
161
+
162
+ ### Run Your Application
163
+
164
+ 1. **Start the development server**
165
+ ```shell
166
+ $ panther run main:app
167
+ ```
168
+
169
+ 2. **Test your application**
170
+ - For the _API_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) to see the "Hello World" response
171
+ - For the _WebSocket_ example: Visit [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and send a message.
172
+
173
+ ---
174
+
175
+ ## 🙏 Acknowledgments
176
+
177
+ <div align="center">
178
+ <p>Supported by</p>
179
+ <a href="https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing">
180
+ <img alt="JetBrains" src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png">
181
+ </a>
182
+ </div>
183
+
184
+ ---
185
+
186
+ <div align="center">
187
+ <p>⭐️ If you find Panther useful, please give it a star!</p>
188
+ </div>
@@ -1,7 +1,7 @@
1
- panther/__init__.py,sha256=X4CUQB2xphMKSsHepAZPm5n5K0uNc-zhZt9i3Hs_jN8,115
1
+ panther/__init__.py,sha256=ytLbZ9XWNWjCIG-7DU0bpP1TRmk2oBs5u8RKm2A8uTA,115
2
2
  panther/_load_configs.py,sha256=9SMiJm4N5wOZYYpM5BfchvHuTg7PZOmvfIkiloUQLDk,11283
3
3
  panther/_utils.py,sha256=5UN0DBNTEqHejK6EOnG9IYyH1gK9OvGXYlNp5G0iFuU,4720
4
- panther/app.py,sha256=1WsVwzWZBxA5lO6v18Renq9oYqySY0gRFFT42cPkbFY,11409
4
+ panther/app.py,sha256=H_tMo64KIFi79WIacEI_d8IcWxtvv9fUxNjQzTMwdqg,11449
5
5
  panther/authentications.py,sha256=JdCeXKvo6iHmxeXsZEmFvXQsLkI149g1dIR_md6blV8,7844
6
6
  panther/background_tasks.py,sha256=A__lY4IijGbRD9GKtbUK_c8cChtFW0jPaxoQHJ25bsk,7539
7
7
  panther/base_request.py,sha256=MkzTv_Si4scJFZgHRaZurwjN6KthrKf1aqIN8u811z0,4950
@@ -11,15 +11,15 @@ panther/configs.py,sha256=Hg-4B9mD4QL5aALEd7NJ8bTMikJWS1dhVtKe0n42Buc,3834
11
11
  panther/events.py,sha256=-AFsJwZe9RpQ9xQQArUfqCPjv4ZRaFZ0shzTuO5WmWc,1576
12
12
  panther/exceptions.py,sha256=QubEyGPnKlo4e7dR_SU2JbRB20vZ42LcUH3JvmOK5Xg,2231
13
13
  panther/file_handler.py,sha256=6zXe36eaCyqtZFX2bMT9xl8tjimoHMcD7csLoPx_8EA,1323
14
- panther/generics.py,sha256=v66yp6-jNdXh7EW5U-1g_f0zrp8_KaGr6OcbXsYbuzM,7318
14
+ panther/generics.py,sha256=O0XHXNKwRy3KbbE4UNJ5m-Tzn2qtNQZuu0OSf1ES03A,6806
15
15
  panther/logging.py,sha256=g-RUuyCveqdMrEQXWIjIPZi2jYCJmOmZV8TvD_uMrEU,2075
16
16
  panther/main.py,sha256=0i5HoJ4IGY2bF25lK1V6x7_f-boxceVz6zLj6Q6vTi8,7557
17
17
  panther/pagination.py,sha256=bQEpf-FMil6zOwGuGD6VEowht2_13sT5jl-Cflwo_-E,1644
18
18
  panther/permissions.py,sha256=UdPHVZYLWIYaf94OauE1QdVlj66_iE8B3rb336MBBcU,400
19
19
  panther/request.py,sha256=IVuDdLdceCzo2vmICnWwoD2ag1eNc09C5XHZnULQxUw,1888
20
- panther/response.py,sha256=k8ZQ6BIKuLc3vRRfSunm7twGixD6145ByWloSAqOrQQ,10495
20
+ panther/response.py,sha256=WSWKlwb8l804W5SzmtKAQIavhmrdi3LHsG3xjBaMaos,10854
21
21
  panther/routings.py,sha256=QwE7EyQD1wdgXS8JK80tV36tIrrBR7fRZ1OkhpA8m7s,6482
22
- panther/serializer.py,sha256=SrVROW9HyZS2QZDwlcrs_5npByIk-4bijm6_vAj2M24,9064
22
+ panther/serializer.py,sha256=e6iM09Uh6y6JrVGEzDlOfbB8vMTtSECW0Dy9_D6pn0A,8338
23
23
  panther/status.py,sha256=Gc_PnYrHfInTsZpGbqiCfDB-py1C7Rh8KMdb6Lq9Exs,3346
24
24
  panther/test.py,sha256=EReFLKhDtOoGQVTPSdtI31xi-u4SfwirA179G9_rIAE,7374
25
25
  panther/throttling.py,sha256=EnU9PtulAwNTxsheun-s-kjJ1YL3jgj0bpxe8jGowlQ,2630
@@ -33,13 +33,13 @@ panther/cli/run_command.py,sha256=ZInQQGV-QaLS7XUEUPqP_3iR2Nrto9unaOvYAs3mF9M,35
33
33
  panther/cli/template.py,sha256=C3jb6m_NQRzur-_DNtEKiptMYtxTvd5MNM1qIgpFMNA,5331
34
34
  panther/cli/utils.py,sha256=SjqggWpgGVH_JiMNQFnXPWzoMYxIHI2p9WO3-c59wU4,5542
35
35
  panther/db/__init__.py,sha256=w9lEL0vRqb18Qx_iUJipUR_fi5GQ5uVX0DWycx14x08,50
36
- panther/db/connections.py,sha256=AJpl7qgPuUTqDvuGW6VkLPRhNwrgTVoIDak3i7uI4GY,3982
37
- panther/db/cursor.py,sha256=glFyfVriOmluajlq77u7cKF2Gnmo6iF5qLqT63yeigg,1783
38
- panther/db/models.py,sha256=6WvWb9NkxZz7Srw0YCdZeIDNZt68nSEkuOoazh-Efm0,3250
36
+ panther/db/connections.py,sha256=RMcnArf1eurxjySpSg5afNmyUCxo_ifxhG1I8mr9L7M,4191
37
+ panther/db/cursor.py,sha256=TnbMUvEDpXGUuL42gDWT9QKFu5ymo0kLLo-Socgw7rM,1836
38
+ panther/db/models.py,sha256=E9y0ibCp1nPAKejMBtAQrkngmp3fXdFkgHfsXtfCBYM,3206
39
39
  panther/db/utils.py,sha256=GiRQ4t9csEFKmGViej7dyfZaaiWMdTAQeWzdoCWTJac,1574
40
40
  panther/db/queries/__init__.py,sha256=uF4gvBjLBJ-Yl3WLqoZEVNtHCVhFRKW3_Vi44pJxDNI,45
41
- panther/db/queries/base_queries.py,sha256=GFPEvSV7SGAVpJFHiIkhfKBf0xkLsyXvmW7bRI88HOU,3738
42
- panther/db/queries/mongodb_queries.py,sha256=FTWcJc5vDy163Pn-_vyeeRlljs8SOKlARQ_gi3AvBtc,13389
41
+ panther/db/queries/base_queries.py,sha256=0c1IxRl79C93JyEn5uno8WDBvyKTql_kyNND2ep5zqI,3817
42
+ panther/db/queries/mongodb_queries.py,sha256=rN0vKUQHtimQ0ogNacwuz5c2irkPHkn8ydjF9dU7aJQ,13468
43
43
  panther/db/queries/pantherdb_queries.py,sha256=GlRRFvbaeVR3x2dYqlQIvsWxAWUcPflZ2u6kuJYvSIM,4620
44
44
  panther/db/queries/queries.py,sha256=nhjrFk02O-rLUZ5slS3jHZ9wnxPrFLmiAZLaeVePKiA,12408
45
45
  panther/middlewares/__init__.py,sha256=8VXd-K3L0a5ZkGb-NUipn3K8wxWAVIiOM7fQrcm_dTM,87
@@ -67,9 +67,9 @@ panther/panel/templates/login.html,sha256=W6V1rgHAno7yTbP6Il38ZvJp4LdlJ8BjM4UuyP
67
67
  panther/panel/templates/sidebar.html,sha256=XikovZsJrth0nvKogvZoh3Eb2Bq7xdeGTlsdlyud450,618
68
68
  panther/panel/templates/table.html,sha256=fWdaIHEHAuwuPaAfOtXkD-3yvSocyDmtys00_D2yRh8,2176
69
69
  panther/panel/templates/table.js,sha256=MTdf77571Gtmg4l8HkY-5fM-utIL3lc0O8hv6vLBCYk,10414
70
- panther-5.0.0b4.dist-info/licenses/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
71
- panther-5.0.0b4.dist-info/METADATA,sha256=2NiQy6W9FrVHUevwkWiTVcoJGUy7Z5dT_iKPKjZN7bI,7026
72
- panther-5.0.0b4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
- panther-5.0.0b4.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
74
- panther-5.0.0b4.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
75
- panther-5.0.0b4.dist-info/RECORD,,
70
+ panther-5.0.0b5.dist-info/licenses/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
71
+ panther-5.0.0b5.dist-info/METADATA,sha256=pBsRQN6g2DoB1-aXY0DAAZt0A_HBwnpgdXuisbqbxoU,6269
72
+ panther-5.0.0b5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
73
+ panther-5.0.0b5.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
74
+ panther-5.0.0b5.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
75
+ panther-5.0.0b5.dist-info/RECORD,,
@@ -1,223 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: panther
3
- Version: 5.0.0b4
4
- Summary: Fast & Friendly, Web Framework For Building Async APIs
5
- Home-page: https://github.com/alirn76/panther
6
- Author: Ali RajabNezhad
7
- Author-email: alirn76@yahoo.com
8
- License: BSD-3-Clause license
9
- Classifier: Operating System :: OS Independent
10
- Classifier: Programming Language :: Python :: 3.10
11
- Classifier: Programming Language :: Python :: 3.11
12
- Classifier: Programming Language :: Python :: 3.12
13
- Classifier: Programming Language :: Python :: 3.13
14
- Requires-Python: >=3.10
15
- Description-Content-Type: text/markdown
16
- License-File: LICENSE
17
- Requires-Dist: pantherdb~=2.2.3
18
- Requires-Dist: orjson~=3.9.15
19
- Requires-Dist: pydantic~=2.10.6
20
- Requires-Dist: rich~=13.9.4
21
- Requires-Dist: uvicorn~=0.34.0
22
- Requires-Dist: pytz~=2025.2
23
- Requires-Dist: Jinja2~=3.1
24
- Requires-Dist: simple-ulid~=1.0.0
25
- Requires-Dist: httptools~=0.6.4
26
- Provides-Extra: full
27
- Requires-Dist: redis==5.2.1; extra == "full"
28
- Requires-Dist: motor~=3.7.0; extra == "full"
29
- Requires-Dist: ipython~=9.0.2; extra == "full"
30
- Requires-Dist: python-jose~=3.4.0; extra == "full"
31
- Requires-Dist: ruff~=0.11.2; extra == "full"
32
- Requires-Dist: websockets~=15.0.1; extra == "full"
33
- Requires-Dist: cryptography~=44.0.2; extra == "full"
34
- Requires-Dist: watchfiles~=1.0.4; extra == "full"
35
- Provides-Extra: dev
36
- Requires-Dist: ruff~=0.11.2; extra == "dev"
37
- Requires-Dist: pytest~=8.3.5; extra == "dev"
38
- Dynamic: author
39
- Dynamic: author-email
40
- Dynamic: classifier
41
- Dynamic: description
42
- Dynamic: description-content-type
43
- Dynamic: home-page
44
- Dynamic: license
45
- Dynamic: license-file
46
- Dynamic: provides-extra
47
- Dynamic: requires-dist
48
- Dynamic: requires-python
49
- Dynamic: summary
50
-
51
-
52
- [![PyPI](https://img.shields.io/pypi/v/panther?label=PyPI)](https://pypi.org/project/panther/) [![PyVersion](https://img.shields.io/pypi/pyversions/panther.svg)](https://pypi.org/project/panther/) [![codecov](https://codecov.io/github/AliRn76/panther/graph/badge.svg?token=YWFQA43GSP)](https://codecov.io/github/AliRn76/panther) [![Downloads](https://static.pepy.tech/badge/panther/month)](https://pepy.tech/project/panther) [![license](https://img.shields.io/github/license/alirn76/panther.svg)](https://github.com/alirn76/panther/blob/main/LICENSE)
53
-
54
-
55
- ## Panther
56
- <b>Is A Fast & Friendly Web Framework For Building Async APIs With Python 3.10+</b>
57
-
58
- <p align="center">
59
- <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="logo" style="width: 450px">
60
- </p>
61
-
62
- **_📚 Full Documentation:_** [PantherPy.GitHub.io](https://pantherpy.github.io)
63
-
64
- ---
65
-
66
- ### Why Use Panther ?
67
- - Include Simple **File-Base** Database ([PantherDB](https://pypi.org/project/pantherdb/))
68
- - Built-in Document-oriented Databases **ODM** (**MongoDB**, PantherDB)
69
- - Built-in **Websocket** Support
70
- - Built-in API **Caching** System (In Memory, **Redis**)
71
- - Built-in **Authentication** Classes
72
- - Built-in **Permission** Classes
73
- - Built-in Visual API **Monitoring** (In Terminal)
74
- - Support Custom **Background Tasks**
75
- - Support Custom **Middlewares**
76
- - Support Custom **Throttling**
77
- - Support **Function-Base** and **Class-Base** APIs
78
- - It's One Of The **Fastest Python Framework** ([Benchmark](https://www.techempower.com/benchmarks/#section=test&runid=d3364379-1bf7-465f-bcb1-e9c65b4840f9&hw=ph&test=fortune&l=zik0zj-6bi))
79
- ---
80
-
81
- ### Supported by
82
- <center>
83
- <a href="https://drive.google.com/file/d/17xe1hicIiRF7SQ-clg9SETdc19SktCbV/view?usp=sharing">
84
- <img alt="jetbrains" src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/jb_beam_50x50.png">
85
- </a>
86
- </center>
87
-
88
- ---
89
-
90
- ### Installation
91
- ```shell
92
- $ pip install panther
93
- ```
94
-
95
- ### Usage
96
-
97
- - #### Create Project
98
-
99
- ```shell
100
- $ panther create
101
- ```
102
-
103
- - #### Run Project
104
-
105
- ```shell
106
- $ panther run --reload
107
- ```
108
- _* Panther uses [Uvicorn](https://github.com/encode/uvicorn) as ASGI (Asynchronous Server Gateway Interface) but you can run the project with [Granian](https://pypi.org/project/granian/), [daphne](https://pypi.org/project/daphne/) or any ASGI server_
109
-
110
- - #### Monitoring Requests
111
-
112
- ```shell
113
- $ panther monitor
114
- ```
115
-
116
- - #### Python Shell
117
-
118
- ```shell
119
- $ panther shell
120
- ```
121
-
122
- ---
123
-
124
- ### API Example
125
- - Create `main.py`
126
-
127
- ```python
128
- from datetime import datetime, timedelta
129
-
130
- from panther import status, Panther
131
- from panther.app import GenericAPI
132
- from panther.response import Response
133
-
134
-
135
- class FirstAPI(GenericAPI):
136
- # Cache Response For 10 Seconds
137
- cache = True
138
- cache_exp_time = timedelta(seconds=10)
139
-
140
- def get(self):
141
- date_time = datetime.now().isoformat()
142
- data = {'detail': f'Hello World | {date_time}'}
143
- return Response(data=data, status_code=status.HTTP_202_ACCEPTED)
144
-
145
-
146
- url_routing = {'': FirstAPI}
147
- app = Panther(__name__, configs=__name__, urls=url_routing)
148
- ```
149
-
150
- - Run the project:
151
- - `$ panther run --reload`
152
-
153
- - Checkout the [http://127.0.0.1:8000/](http://127.0.0.1:8000/)
154
-
155
- ### WebSocket Echo Example
156
- - Create `main.py`
157
-
158
- ```python
159
- from panther import Panther
160
- from panther.app import GenericAPI
161
- from panther.response import HTMLResponse
162
- from panther.websocket import GenericWebsocket
163
-
164
-
165
- class FirstWebsocket(GenericWebsocket):
166
- async def connect(self, **kwargs):
167
- await self.accept()
168
-
169
- async def receive(self, data: str | bytes):
170
- await self.send(data)
171
-
172
-
173
- class MainPage(GenericAPI):
174
- def get(self):
175
- template = """
176
- <input type="text" id="messageInput">
177
- <button id="sendButton">Send Message</button>
178
- <ul id="messages"></ul>
179
- <script>
180
- var socket = new WebSocket('ws://127.0.0.1:8000/ws');
181
- socket.addEventListener('message', function (event) {
182
- var li = document.createElement('li');
183
- document.getElementById('messages').appendChild(li).textContent = 'Server: ' + event.data;
184
- });
185
- function sendMessage() {
186
- socket.send(document.getElementById('messageInput').value);
187
- }
188
- document.getElementById('sendButton').addEventListener('click', sendMessage);
189
- </script>
190
- """
191
- return HTMLResponse(template)
192
-
193
- url_routing = {
194
- '': MainPage,
195
- 'ws': FirstWebsocket,
196
- }
197
- app = Panther(__name__, configs=__name__, urls=url_routing)
198
-
199
- ```
200
-
201
- - Run the project:
202
- - `$ panther run --reload`
203
- - Go to [http://127.0.0.1:8000/](http://127.0.0.1:8000/) and work with your `websocket`
204
-
205
-
206
-
207
- > **Next Step: [First CRUD](https://pantherpy.github.io/function_first_crud)**
208
-
209
- ---
210
-
211
- ### How Panther Works!
212
-
213
- ![diagram](https://raw.githubusercontent.com/AliRn76/panther/master/docs/docs/images/diagram.png)
214
-
215
- ---
216
-
217
- ### Roadmap
218
-
219
- ![roadmap](https://raw.githubusercontent.com/AliRn76/panther/master/docs/docs/images/roadmap.jpg)
220
-
221
- ---
222
-
223
- **If you find this project useful, please give it a star ⭐️.**