panther 4.1.2__py3-none-any.whl → 4.2.0__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 +25 -5
- panther/base_request.py +2 -5
- panther/cli/create_command.py +1 -1
- panther/cli/template.py +1 -0
- panther/db/models.py +1 -1
- panther/db/queries/mongodb_queries.py +25 -10
- panther/db/queries/queries.py +7 -3
- panther/db/utils.py +2 -2
- panther/file_handler.py +18 -1
- panther/generics.py +23 -4
- panther/serializer.py +2 -2
- {panther-4.1.2.dist-info → panther-4.2.0.dist-info}/METADATA +1 -1
- {panther-4.1.2.dist-info → panther-4.2.0.dist-info}/RECORD +18 -18
- {panther-4.1.2.dist-info → panther-4.2.0.dist-info}/LICENSE +0 -0
- {panther-4.1.2.dist-info → panther-4.2.0.dist-info}/WHEEL +0 -0
- {panther-4.1.2.dist-info → panther-4.2.0.dist-info}/entry_points.txt +0 -0
- {panther-4.1.2.dist-info → panther-4.2.0.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
panther/app.py
CHANGED
@@ -60,6 +60,10 @@ class API:
|
|
60
60
|
async def wrapper(request: Request) -> Response:
|
61
61
|
self.request = request
|
62
62
|
|
63
|
+
# 0. Preflight
|
64
|
+
if self.request.method == 'OPTIONS':
|
65
|
+
return self.options()
|
66
|
+
|
63
67
|
# 1. Check Method
|
64
68
|
if self.methods and self.request.method not in self.methods:
|
65
69
|
raise MethodNotAllowedAPIError
|
@@ -141,6 +145,14 @@ class API:
|
|
141
145
|
if self.input_model:
|
142
146
|
self.request.validated_data = self.validate_input(model=self.input_model, request=self.request)
|
143
147
|
|
148
|
+
@classmethod
|
149
|
+
def options(cls):
|
150
|
+
headers = {
|
151
|
+
'Access-Control-Allow-Methods': 'DELETE, GET, PATCH, POST, PUT, OPTIONS',
|
152
|
+
'Access-Control-Allow-Headers': 'Accept, Authorization, User-Agent, Content-Type',
|
153
|
+
}
|
154
|
+
return Response(headers=headers)
|
155
|
+
|
144
156
|
@classmethod
|
145
157
|
def validate_input(cls, model, request: Request):
|
146
158
|
if isinstance(request.data, bytes):
|
@@ -151,15 +163,15 @@ class API:
|
|
151
163
|
# `request` will be ignored in regular `BaseModel`
|
152
164
|
return model(**request.data, request=request)
|
153
165
|
except ValidationError as validation_error:
|
154
|
-
error = {'.'.join(loc for loc in e['loc']): e['msg'] for e in validation_error.errors()}
|
166
|
+
error = {'.'.join(str(loc) for loc in e['loc']): e['msg'] for e in validation_error.errors()}
|
155
167
|
raise BadRequestAPIError(detail=error)
|
156
168
|
except JSONDecodeError:
|
157
169
|
raise JSONDecodeAPIError
|
158
170
|
|
159
171
|
|
160
172
|
class GenericAPI:
|
161
|
-
input_model: type[ModelSerializer] | type[BaseModel] = None
|
162
|
-
output_model: type[ModelSerializer] | type[BaseModel] = None
|
173
|
+
input_model: type[ModelSerializer] | type[BaseModel] | None = None
|
174
|
+
output_model: type[ModelSerializer] | type[BaseModel] | None = None
|
163
175
|
auth: bool = False
|
164
176
|
permissions: list | None = None
|
165
177
|
throttling: Throttling | None = None
|
@@ -181,6 +193,12 @@ class GenericAPI:
|
|
181
193
|
async def delete(self, *args, **kwargs):
|
182
194
|
raise MethodNotAllowedAPIError
|
183
195
|
|
196
|
+
async def get_input_model(self, request: Request) -> type[ModelSerializer] | type[BaseModel] | None:
|
197
|
+
return None
|
198
|
+
|
199
|
+
async def get_output_model(self, request: Request) -> type[ModelSerializer] | type[BaseModel] | None:
|
200
|
+
return None
|
201
|
+
|
184
202
|
async def call_method(self, request: Request):
|
185
203
|
match request.method:
|
186
204
|
case 'GET':
|
@@ -193,12 +211,14 @@ class GenericAPI:
|
|
193
211
|
func = self.patch
|
194
212
|
case 'DELETE':
|
195
213
|
func = self.delete
|
214
|
+
case 'OPTIONS':
|
215
|
+
func = API.options
|
196
216
|
case _:
|
197
217
|
raise MethodNotAllowedAPIError
|
198
218
|
|
199
219
|
return await API(
|
200
|
-
input_model=self.input_model,
|
201
|
-
output_model=self.output_model,
|
220
|
+
input_model=self.input_model or await self.get_input_model(request=request),
|
221
|
+
output_model=self.output_model or await self.get_output_model(request=request),
|
202
222
|
auth=self.auth,
|
203
223
|
permissions=self.permissions,
|
204
224
|
throttling=self.throttling,
|
panther/base_request.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from collections import namedtuple
|
2
2
|
from collections.abc import Callable
|
3
|
+
from urllib.parse import parse_qsl
|
3
4
|
|
4
5
|
from panther.db import Model
|
5
6
|
from panther.exceptions import InvalidPathVariableAPIError
|
@@ -74,11 +75,7 @@ class BaseRequest:
|
|
74
75
|
@property
|
75
76
|
def query_params(self) -> dict:
|
76
77
|
if self._params is None:
|
77
|
-
self._params = {}
|
78
|
-
if (query_string := self.scope['query_string']) != b'':
|
79
|
-
for param in query_string.decode('utf-8').split('&'):
|
80
|
-
k, *_, v = param.split('=')
|
81
|
-
self._params[k] = v
|
78
|
+
self._params = {k: v for k, v in parse_qsl(self.scope['query_string'].decode('utf-8'))}
|
82
79
|
return self._params
|
83
80
|
|
84
81
|
@property
|
panther/cli/create_command.py
CHANGED
@@ -61,7 +61,7 @@ class CreateProject:
|
|
61
61
|
},
|
62
62
|
{
|
63
63
|
'field': 'database',
|
64
|
-
'message': ' 0: PantherDB (File-Base, No Requirements)\n 1: MongoDB (Required `
|
64
|
+
'message': ' 0: PantherDB (File-Base, No Requirements)\n 1: MongoDB (Required `motor`)\n 2: No Database\nChoose Your Database (default is 0)',
|
65
65
|
'validation_func': lambda x: x in ['0', '1', '2'],
|
66
66
|
'error_message': "Invalid Choice, '{}' not in ['0', '1', '2']",
|
67
67
|
},
|
panther/cli/template.py
CHANGED
panther/db/models.py
CHANGED
@@ -61,7 +61,7 @@ class BaseMongoDBQuery(BaseQuery):
|
|
61
61
|
|
62
62
|
@classmethod
|
63
63
|
async def aggregate(cls, pipeline: Sequence[dict]) -> Iterable[dict]:
|
64
|
-
return await db.session[cls.__name__].aggregate(pipeline)
|
64
|
+
return await db.session[cls.__name__].aggregate(pipeline).to_list(None)
|
65
65
|
|
66
66
|
# # # # # Count # # # # #
|
67
67
|
@classmethod
|
@@ -102,21 +102,36 @@ class BaseMongoDBQuery(BaseQuery):
|
|
102
102
|
|
103
103
|
# # # # # Update # # # # #
|
104
104
|
async def update(self, _update: dict | None = None, /, **kwargs) -> None:
|
105
|
-
|
106
|
-
|
107
|
-
self._validate_data(data=document, is_updating=True)
|
105
|
+
merged_update_query = self._merge(_update, kwargs)
|
106
|
+
merged_update_query.pop('_id', None)
|
108
107
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
108
|
+
self._validate_data(data=merged_update_query, is_updating=True)
|
109
|
+
|
110
|
+
update_query = {}
|
111
|
+
for field, value in merged_update_query.items():
|
112
|
+
if field.startswith('$'):
|
113
|
+
update_query[field] = value
|
114
|
+
else:
|
115
|
+
update_query['$set'] = update_query.get('$set', {})
|
116
|
+
update_query['$set'][field] = value
|
117
|
+
setattr(self, field, value)
|
118
|
+
|
119
|
+
await db.session[self.__class__.__name__].update_one({'_id': self._id}, update_query)
|
113
120
|
|
114
121
|
@classmethod
|
115
122
|
async def update_one(cls, _filter: dict, _update: dict | None = None, /, **kwargs) -> bool:
|
116
123
|
prepare_id_for_query(_filter, is_mongo=True)
|
117
|
-
|
124
|
+
merged_update_query = cls._merge(_update, kwargs)
|
125
|
+
|
126
|
+
update_query = {}
|
127
|
+
for field, value in merged_update_query.items():
|
128
|
+
if field.startswith('$'):
|
129
|
+
update_query[field] = value
|
130
|
+
else:
|
131
|
+
update_query['$set'] = update_query.get('$set', {})
|
132
|
+
update_query['$set'][field] = value
|
118
133
|
|
119
|
-
result = await db.session[cls.__name__].update_one(_filter,
|
134
|
+
result = await db.session[cls.__name__].update_one(_filter, update_query)
|
120
135
|
return bool(result.matched_count)
|
121
136
|
|
122
137
|
@classmethod
|
panther/db/queries/queries.py
CHANGED
@@ -364,8 +364,6 @@ class Query(BaseQuery):
|
|
364
364
|
|
365
365
|
raise NotFoundAPIError(detail=f'{cls.__name__} Does Not Exist')
|
366
366
|
|
367
|
-
@check_connection
|
368
|
-
@log_query
|
369
367
|
async def save(self) -> None:
|
370
368
|
"""
|
371
369
|
Save the document
|
@@ -384,8 +382,14 @@ class Query(BaseQuery):
|
|
384
382
|
>>> user = User(name='Ali')
|
385
383
|
>>> await user.save()
|
386
384
|
"""
|
387
|
-
document = self.
|
385
|
+
document = {field: getattr(self, field) for field in self.model_fields_set if field != 'request'}
|
386
|
+
|
388
387
|
if self.id:
|
389
388
|
await self.update(document)
|
390
389
|
else:
|
391
390
|
await self.insert_one(document)
|
391
|
+
|
392
|
+
async def reload(self) -> Self:
|
393
|
+
new_obj = await self.find_one(id=self.id)
|
394
|
+
[setattr(self, f, getattr(new_obj, f)) for f in new_obj.model_fields]
|
395
|
+
return self
|
panther/db/utils.py
CHANGED
panther/file_handler.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from functools import cached_property
|
2
2
|
|
3
3
|
from panther import status
|
4
|
-
from pydantic import BaseModel, field_validator
|
4
|
+
from pydantic import BaseModel, field_validator, model_serializer
|
5
5
|
|
6
6
|
from panther.exceptions import APIError
|
7
7
|
|
@@ -15,6 +15,23 @@ class File(BaseModel):
|
|
15
15
|
def size(self):
|
16
16
|
return len(self.file)
|
17
17
|
|
18
|
+
def save(self) -> str:
|
19
|
+
if hasattr(self, '_file_name'):
|
20
|
+
return self._file_name
|
21
|
+
|
22
|
+
self._file_name = self.file_name
|
23
|
+
# TODO: check for duplication
|
24
|
+
with open(self._file_name, 'wb') as file:
|
25
|
+
file.write(self.file)
|
26
|
+
|
27
|
+
return self.file_name
|
28
|
+
|
29
|
+
@model_serializer(mode='wrap')
|
30
|
+
def _serialize(self, handler):
|
31
|
+
result = handler(self)
|
32
|
+
result['path'] = self.save()
|
33
|
+
return result
|
34
|
+
|
18
35
|
def __repr__(self) -> str:
|
19
36
|
return f'{self.__repr_name__()}(file_name={self.file_name}, content_type={self.content_type})'
|
20
37
|
|
panther/generics.py
CHANGED
@@ -23,7 +23,7 @@ logger = logging.getLogger('panther')
|
|
23
23
|
|
24
24
|
class ObjectRequired:
|
25
25
|
def _check_object(self, instance):
|
26
|
-
if issubclass(type(instance), Model) is False:
|
26
|
+
if instance and issubclass(type(instance), Model) is False:
|
27
27
|
logger.critical(f'`{self.__class__.__name__}.object()` should return instance of a Model --> `find_one()`')
|
28
28
|
raise APIError
|
29
29
|
|
@@ -129,9 +129,11 @@ class CreateAPI(GenericAPI):
|
|
129
129
|
input_model: type[ModelSerializer]
|
130
130
|
|
131
131
|
async def post(self, request: Request, **kwargs):
|
132
|
-
instance = await request.validated_data.create(
|
133
|
-
|
134
|
-
|
132
|
+
instance = await request.validated_data.create(validated_data={
|
133
|
+
field: getattr(request.validated_data, field)
|
134
|
+
for field in request.validated_data.model_fields_set
|
135
|
+
if field != 'request'
|
136
|
+
})
|
135
137
|
return Response(data=instance, status_code=status.HTTP_201_CREATED)
|
136
138
|
|
137
139
|
|
@@ -160,13 +162,30 @@ class UpdateAPI(GenericAPI, ObjectRequired):
|
|
160
162
|
|
161
163
|
|
162
164
|
class DeleteAPI(GenericAPI, ObjectRequired):
|
165
|
+
async def pre_delete(self, instance, request: Request, **kwargs):
|
166
|
+
pass
|
167
|
+
|
168
|
+
async def post_delete(self, instance, request: Request, **kwargs):
|
169
|
+
pass
|
170
|
+
|
163
171
|
async def delete(self, request: Request, **kwargs):
|
164
172
|
instance = await self.object(request=request, **kwargs)
|
165
173
|
self._check_object(instance)
|
166
174
|
|
175
|
+
await self.pre_delete(instance, request=request, **kwargs)
|
167
176
|
await instance.delete()
|
177
|
+
await self.post_delete(instance, request=request, **kwargs)
|
178
|
+
|
168
179
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
169
180
|
|
170
181
|
|
171
182
|
class ListCreateAPI(CreateAPI, ListAPI):
|
172
183
|
pass
|
184
|
+
|
185
|
+
|
186
|
+
class UpdateDeleteAPI(UpdateAPI, DeleteAPI):
|
187
|
+
pass
|
188
|
+
|
189
|
+
|
190
|
+
class RetrieveUpdateDeleteAPI(RetrieveAPI, UpdateAPI, DeleteAPI):
|
191
|
+
pass
|
panther/serializer.py
CHANGED
@@ -65,8 +65,8 @@ class MetaModelSerializer:
|
|
65
65
|
|
66
66
|
# Check `model` type
|
67
67
|
try:
|
68
|
-
if not issubclass(model, Model):
|
69
|
-
msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model`.'
|
68
|
+
if not issubclass(model, (Model, BaseModel)):
|
69
|
+
msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model` or `pydantic.BaseModel`.'
|
70
70
|
raise AttributeError(msg) from None
|
71
71
|
except TypeError:
|
72
72
|
msg = f'`{cls_name}.Config.model` is not subclass of `panther.db.Model`.'
|
@@ -1,17 +1,17 @@
|
|
1
|
-
panther/__init__.py,sha256=
|
1
|
+
panther/__init__.py,sha256=sduiFCRbp6Nv6i1LB-sOh5Eb8lOJMR-Plz6Th79QwCs,110
|
2
2
|
panther/_load_configs.py,sha256=AVkoixkUFkBQiTmrLrwCmg0eiPW2U_Uw2EGNEGQRfnI,9281
|
3
3
|
panther/_utils.py,sha256=j0rwIxTf0rtcZAAD-1nGE-_bWpvinyKtnwt3uO0hMmY,4330
|
4
|
-
panther/app.py,sha256=
|
4
|
+
panther/app.py,sha256=e2eb4sXIaBje5vpcm4pbvvEO_sj83pLfBHCIZJFFX38,8222
|
5
5
|
panther/authentications.py,sha256=gf7BVyQ8vXKhiumJAtD0aAK7uIHWx_snbOKYAKrYuVw,5677
|
6
6
|
panther/background_tasks.py,sha256=HBYubDIiO_673cl_5fqCUP9zzimzRgRkDSkag9Msnbs,7656
|
7
|
-
panther/base_request.py,sha256=
|
7
|
+
panther/base_request.py,sha256=XD2v1gLWcCKHePowRxT6_fYnS4tdKFxTLINMX0HQu8M,3880
|
8
8
|
panther/base_websocket.py,sha256=iJUIbrfnh3ZLXlmKxTswMw158eNvtBFi8RZ-aBBmc8w,10643
|
9
9
|
panther/caching.py,sha256=ltuJYdjNiAaKIs3jpO5EBpL8Y6CF1vAIQqh8J_Np10g,4098
|
10
10
|
panther/configs.py,sha256=EaLApT6nYcguBoNXBG_8n6DU6HTNxsulI2943j8UAkE,3174
|
11
11
|
panther/events.py,sha256=bxDqrfiNNBlvD03vEk2LDK4xbMzTMFVcgAjx2ein7mI,1158
|
12
12
|
panther/exceptions.py,sha256=7rHdJIES2__kqOStIqbHl3Uxask2lzKgLQlkZvvDwFA,1591
|
13
|
-
panther/file_handler.py,sha256=
|
14
|
-
panther/generics.py,sha256=
|
13
|
+
panther/file_handler.py,sha256=I94tpbtTVniBnnUMkFr3Eis6kPDt8sLzS5u8TzFrR5I,1323
|
14
|
+
panther/generics.py,sha256=D2ia7M4ML15kMZiuCIMpL7ZfQhMmKpqE4wCmuRE-q4Y,7233
|
15
15
|
panther/logging.py,sha256=k__vzvSrPpr1IsA4OLrBt1JHuRUBXr7ekPlBW0-9rbM,2209
|
16
16
|
panther/main.py,sha256=UbIxwaojvY_vH9nYfBpkulRBqVEj4Lbl81Er4XW_KCY,9334
|
17
17
|
panther/monitoring.py,sha256=y1F3c8FJlnmooM-m1nSyOTa9eWq0v1nHnmw9zz-4Kls,1314
|
@@ -20,38 +20,38 @@ panther/permissions.py,sha256=9-J5vzvEKa_PITwEVQbZZv8PG2FOu05YBlD5yMrKcfc,348
|
|
20
20
|
panther/request.py,sha256=F9ZiAWSse7_6moAzqdoFInUN4zTKlzijh9AdU9w3Jfw,1673
|
21
21
|
panther/response.py,sha256=Njp4zJozNic8J4ucG8Sgh-xeBZOgtoz2cfdDkJlGOWU,7582
|
22
22
|
panther/routings.py,sha256=1eqbjubLnUUEQRlz8mIF464ImvCMjyasiekHBtxEQoQ,6218
|
23
|
-
panther/serializer.py,sha256=
|
23
|
+
panther/serializer.py,sha256=UX-cVS-11KnxijUhPXsBs_Pb-Sm3EVzUQFTf9bFQT0A,9096
|
24
24
|
panther/status.py,sha256=Gc_PnYrHfInTsZpGbqiCfDB-py1C7Rh8KMdb6Lq9Exs,3346
|
25
25
|
panther/test.py,sha256=RsQtP5IURLWR__BihOjruWoX3NscmGDqDqj1CfAb3bI,7037
|
26
26
|
panther/throttling.py,sha256=mVa_mGv6w_Ad7LLtV4eG5QpDwwNsk4QjFFi0mIHQBnE,231
|
27
27
|
panther/utils.py,sha256=Iq5q1suIgBBQGO5UctwR4HXs8E6zclXNh5lYc8k1Vjg,3409
|
28
28
|
panther/websocket.py,sha256=5WLw--Oa-6kGYbeRvO79hjbd0ARFcTTF40-hO_bdjmQ,1206
|
29
29
|
panther/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
30
|
-
panther/cli/create_command.py,sha256=
|
30
|
+
panther/cli/create_command.py,sha256=mT5GFzsTYZbzqShNHlw_UIkMcWLw9btm2mTwcg7TlfI,10292
|
31
31
|
panther/cli/main.py,sha256=pCqnOTazgMhTvFHTugutIsiFXueU5kx2VmGngwAl54Q,1679
|
32
32
|
panther/cli/monitor_command.py,sha256=7N1-4W0Lu7yl5maehJowe04WH4nxZ1DumGDRATh82SQ,3139
|
33
33
|
panther/cli/run_command.py,sha256=yWcDoWC-c4ph4M5EDj0jvR9xSjh-apG5r6-NpDdArUo,2195
|
34
|
-
panther/cli/template.py,sha256=
|
34
|
+
panther/cli/template.py,sha256=hVkY1A3HZDVGEZzRkMtYte6FagKGTAxoFeG0wot7Zn4,5320
|
35
35
|
panther/cli/utils.py,sha256=Jd4YQ9H6lapVktl7ZmiORt30WVmKI85xcwsY-fMRq3c,5289
|
36
36
|
panther/db/__init__.py,sha256=w9lEL0vRqb18Qx_iUJipUR_fi5GQ5uVX0DWycx14x08,50
|
37
37
|
panther/db/connections.py,sha256=rps48Ic2r3SV2HD3df1OU7C4Pv8j98PVXU1O_FqF9Ak,4210
|
38
38
|
panther/db/cursor.py,sha256=jJ6bhz_Zljt3-AoeVdi563e2q3MSDJPP33WVbQk-goE,1287
|
39
|
-
panther/db/models.py,sha256=
|
40
|
-
panther/db/utils.py,sha256=
|
39
|
+
panther/db/models.py,sha256=GRbKXJiwnxQJ_SjuPfXPzA5miH_djZNTPPyqiRp5DI8,2561
|
40
|
+
panther/db/utils.py,sha256=frSekBisjeHUHTiucgeiJNOGdrNcf3mjodupyeZjyKk,1575
|
41
41
|
panther/db/queries/__init__.py,sha256=uF4gvBjLBJ-Yl3WLqoZEVNtHCVhFRKW3_Vi44pJxDNI,45
|
42
42
|
panther/db/queries/base_queries.py,sha256=8HhdlsSW-lgz3-IrZYfOtHNC3TBWbCNErDR4XE718AY,3764
|
43
|
-
panther/db/queries/mongodb_queries.py,sha256=
|
43
|
+
panther/db/queries/mongodb_queries.py,sha256=4CQRdmgmKFIpnv6xijS0BMJO-Sf5beyuzmw8Z9jLtnA,5599
|
44
44
|
panther/db/queries/pantherdb_queries.py,sha256=_dA4gXk1IA5jzIy6_6o1zgdZeeka6SPihvQeSkj7h68,4481
|
45
|
-
panther/db/queries/queries.py,sha256=
|
45
|
+
panther/db/queries/queries.py,sha256=d6V4whHEYjtufoeqIC5lBNa_FGDW8_7AYeps48ilHDk,11665
|
46
46
|
panther/middlewares/__init__.py,sha256=ydo0bSadGqa2v7Xy1oCTkF2uXrImedXjiyx2vPTwPhE,66
|
47
47
|
panther/middlewares/base.py,sha256=tX0MBvDBkbsAB_DilRIYvcggSAqCzazRTb9MegZNdlA,843
|
48
48
|
panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
49
49
|
panther/panel/apis.py,sha256=COsbwKZyTgyHvHYbpDfusifAH9ojMS3z1KhZCt9M-Ms,2428
|
50
50
|
panther/panel/urls.py,sha256=JiV-H4dWE-m_bfaTTVxzOxTvJmOWhyLOvcbM7xU3Bn4,240
|
51
51
|
panther/panel/utils.py,sha256=0Rv79oR5IEqalqwpRKQHMn1p5duVY5mxMqDKiA5mWx4,437
|
52
|
-
panther-4.
|
53
|
-
panther-4.
|
54
|
-
panther-4.
|
55
|
-
panther-4.
|
56
|
-
panther-4.
|
57
|
-
panther-4.
|
52
|
+
panther-4.2.0.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
|
53
|
+
panther-4.2.0.dist-info/METADATA,sha256=N0SkpG2_UqpwuGhnGC8zOUdmnMYKNaaJG4sJaYl0UEk,6376
|
54
|
+
panther-4.2.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
55
|
+
panther-4.2.0.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
|
56
|
+
panther-4.2.0.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
|
57
|
+
panther-4.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|