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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from panther.main import Panther # noqa: F401
2
2
 
3
- __version__ = '4.1.2'
3
+ __version__ = '4.2.0'
4
4
 
5
5
 
6
6
  def version():
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
@@ -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 `pymongo`)\n 2: No Database\nChoose Your Database (default is 0)',
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
@@ -140,6 +140,7 @@ InfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))
140
140
 
141
141
  TIMEZONE = 'UTC'
142
142
 
143
+
143
144
  @API()
144
145
  async def hello_world_api():
145
146
  return {'detail': 'Hello World'}
panther/db/models.py CHANGED
@@ -21,7 +21,7 @@ def validate_object_id(value, handler):
21
21
  else:
22
22
  try:
23
23
  return bson.ObjectId(value)
24
- except bson.objectid.InvalidId as e:
24
+ except Exception as e:
25
25
  msg = 'Invalid ObjectId'
26
26
  raise ValueError(msg) from e
27
27
  return str(value)
@@ -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
- document = self._merge(_update, kwargs)
106
- document.pop('_id', None)
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
- for field, value in document.items():
110
- setattr(self, field, value)
111
- update_fields = {'$set': document}
112
- await db.session[self.__class__.__name__].update_one({'_id': self._id}, update_fields)
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
- update_fields = {'$set': cls._merge(_update, kwargs)}
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, update_fields)
134
+ result = await db.session[cls.__name__].update_one(_filter, update_query)
120
135
  return bool(result.matched_count)
121
136
 
122
137
  @classmethod
@@ -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.model_dump(exclude=['_id'])
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
@@ -54,5 +54,5 @@ def _convert_to_object_id(_id):
54
54
  try:
55
55
  return bson.ObjectId(_id)
56
56
  except bson.objectid.InvalidId:
57
- msg = f'id={_id} is invalid bson.ObjectId'
58
- raise bson.errors.InvalidId(msg)
57
+ logger.warning(f'id={_id} is not a valid bson.ObjectId')
58
+ return None
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
- validated_data=request.validated_data.model_dump()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 4.1.2
3
+ Version: 4.2.0
4
4
  Summary: Fast & Friendly, Web Framework For Building Async APIs
5
5
  Home-page: https://github.com/alirn76/panther
6
6
  Author: Ali RajabNezhad
@@ -1,17 +1,17 @@
1
- panther/__init__.py,sha256=jygFzW2zuio44KfIpTdDG3C8iAGBD_vxBn8GVGODSDk,110
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=vb4j8CKidFHD5HIfK1t96fr8URMYxkroW8dQH9SVj14,7385
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=Fwwpm-9bjAZdpzSdakmSas5BD3gh1nrc6iGcBxwa_94,4001
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=XnomEigCUYOaXjkH4kD1kzpUbL2i9lLnR5kerruF6BA,846
14
- panther/generics.py,sha256=SIK1Wqpfb_jKKt4xJPbYIhMY0QhtbhOXS68dIW4Y0bU,6671
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=MBT43UG8YBjp-UGaqe5-SPqQHIcDEjLAdBjHAVKyMJo,9059
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=cVhz0VQOvEbpufFsevH9T1rYZ8T_Wsa89cpTiIVKTC0,10294
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=rsyKOQ0l2v3kdwmLiZxt5ecIhDzmFprCCv0uVAv7eQI,5319
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=4g6Jm2TBTViC2ttpENzuRJyvPbvXYzEqHIZLiWekRUk,2575
40
- panther/db/utils.py,sha256=Uxh7UebkBv4thMCfooYW1pkuorFgocsbnBZJi-hHtdY,1582
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=GZkkmfFTSjLVNTH3jSl3VM7MVHnXE0Fg6ob77yEYRPQ,5034
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=IK7NmSodftRbmWXrW4ddxyOHWU4L3WyuMbY0uG1WeGE,11462
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.1.2.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
53
- panther-4.1.2.dist-info/METADATA,sha256=c0gW17fnBtGxqL-u4mM6gVckd4r3nrZRsbEKeh7MAOY,6376
54
- panther-4.1.2.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
55
- panther-4.1.2.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
56
- panther-4.1.2.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
57
- panther-4.1.2.dist-info/RECORD,,
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,,