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 +1 -1
- panther/app.py +9 -9
- panther/db/connections.py +12 -0
- panther/db/cursor.py +3 -0
- panther/db/models.py +2 -2
- panther/db/queries/base_queries.py +2 -0
- panther/db/queries/mongodb_queries.py +4 -1
- panther/generics.py +74 -103
- panther/response.py +49 -48
- panther/serializer.py +1 -23
- panther-5.0.0b5.dist-info/METADATA +188 -0
- {panther-5.0.0b4.dist-info → panther-5.0.0b5.dist-info}/RECORD +16 -16
- panther-5.0.0b4.dist-info/METADATA +0 -223
- {panther-5.0.0b4.dist-info → panther-5.0.0b5.dist-info}/WHEEL +0 -0
- {panther-5.0.0b4.dist-info → panther-5.0.0b5.dist-info}/entry_points.txt +0 -0
- {panther-5.0.0b4.dist-info → panther-5.0.0b5.dist-info}/licenses/LICENSE +0 -0
- {panther-5.0.0b4.dist-info → panther-5.0.0b5.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
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
|
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
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
|
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 == '
|
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
|
25
|
-
|
26
|
-
|
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
|
-
|
31
|
+
Should return an instance of Model, e.g. `await User.find_one()`
|
33
32
|
"""
|
34
|
-
logger.error(f'`
|
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
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
47
|
+
async def get_query(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
|
45
48
|
"""
|
46
|
-
|
47
|
-
Should return `.find()`
|
49
|
+
Should return a Cursor, e.g. `await User.find()`
|
48
50
|
"""
|
49
|
-
logger.error(f'`
|
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.
|
73
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
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.
|
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
|
144
|
-
input_model: type[ModelSerializer]
|
121
|
+
class UpdateAPI(GenericAPI):
|
122
|
+
input_model: type[ModelSerializer] | None = None
|
145
123
|
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
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.
|
158
|
-
|
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
|
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.
|
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 `
|
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
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
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
|
+
[](https://pypi.org/project/panther/) [](https://pypi.org/project/panther/) [](https://codecov.io/github/AliRn76/panther) [](https://pepy.tech/project/panther) [](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=
|
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=
|
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=
|
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=
|
20
|
+
panther/response.py,sha256=WSWKlwb8l804W5SzmtKAQIavhmrdi3LHsG3xjBaMaos,10854
|
21
21
|
panther/routings.py,sha256=QwE7EyQD1wdgXS8JK80tV36tIrrBR7fRZ1OkhpA8m7s,6482
|
22
|
-
panther/serializer.py,sha256=
|
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=
|
37
|
-
panther/db/cursor.py,sha256=
|
38
|
-
panther/db/models.py,sha256=
|
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=
|
42
|
-
panther/db/queries/mongodb_queries.py,sha256=
|
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.
|
71
|
-
panther-5.0.
|
72
|
-
panther-5.0.
|
73
|
-
panther-5.0.
|
74
|
-
panther-5.0.
|
75
|
-
panther-5.0.
|
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
|
-
[](https://pypi.org/project/panther/) [](https://pypi.org/project/panther/) [](https://codecov.io/github/AliRn76/panther) [](https://pepy.tech/project/panther) [](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
|
-

|
214
|
-
|
215
|
-
---
|
216
|
-
|
217
|
-
### Roadmap
|
218
|
-
|
219
|
-

|
220
|
-
|
221
|
-
---
|
222
|
-
|
223
|
-
**If you find this project useful, please give it a star ⭐️.**
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|