panther 4.0.0__py3-none-any.whl → 4.1.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 +1 -1
- panther/base_request.py +5 -7
- panther/base_websocket.py +3 -4
- panther/generics.py +19 -10
- panther/main.py +2 -1
- panther/response.py +31 -14
- panther/serializer.py +7 -4
- {panther-4.0.0.dist-info → panther-4.1.0.dist-info}/METADATA +3 -22
- {panther-4.0.0.dist-info → panther-4.1.0.dist-info}/RECORD +14 -14
- {panther-4.0.0.dist-info → panther-4.1.0.dist-info}/LICENSE +0 -0
- {panther-4.0.0.dist-info → panther-4.1.0.dist-info}/WHEEL +0 -0
- {panther-4.0.0.dist-info → panther-4.1.0.dist-info}/entry_points.txt +0 -0
- {panther-4.0.0.dist-info → panther-4.1.0.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
panther/app.py
CHANGED
@@ -95,7 +95,7 @@ class API:
|
|
95
95
|
if not isinstance(response, Response):
|
96
96
|
response = Response(data=response)
|
97
97
|
if self.output_model and response.data:
|
98
|
-
response.data = response.apply_output_model(
|
98
|
+
response.data = await response.apply_output_model(output_model=self.output_model)
|
99
99
|
|
100
100
|
# 10. Set New Response To Cache
|
101
101
|
if self.cache and self.request.method == 'GET':
|
panther/base_request.py
CHANGED
@@ -27,9 +27,9 @@ class Headers:
|
|
27
27
|
sec_websocket_version: str
|
28
28
|
sec_websocket_key: str
|
29
29
|
|
30
|
-
def __init__(self, headers):
|
31
|
-
self.__headers = headers
|
32
|
-
self.__pythonic_headers = {k.lower().replace('-', '_'): v for k, v in
|
30
|
+
def __init__(self, headers: list):
|
31
|
+
self.__headers = {header[0].decode('utf-8'): header[1].decode('utf-8') for header in headers}
|
32
|
+
self.__pythonic_headers = {k.lower().replace('-', '_'): v for k, v in self.__headers.items()}
|
33
33
|
|
34
34
|
def __getattr__(self, item: str):
|
35
35
|
if result := self.__pythonic_headers.get(item):
|
@@ -68,8 +68,7 @@ class BaseRequest:
|
|
68
68
|
@property
|
69
69
|
def headers(self) -> Headers:
|
70
70
|
if self._headers is None:
|
71
|
-
_headers =
|
72
|
-
self._headers = Headers(_headers)
|
71
|
+
self._headers = Headers(self.scope['headers'])
|
73
72
|
return self._headers
|
74
73
|
|
75
74
|
@property
|
@@ -77,8 +76,7 @@ class BaseRequest:
|
|
77
76
|
if self._params is None:
|
78
77
|
self._params = {}
|
79
78
|
if (query_string := self.scope['query_string']) != b'':
|
80
|
-
|
81
|
-
for param in query_string:
|
79
|
+
for param in query_string.decode('utf-8').split('&'):
|
82
80
|
k, *_, v = param.split('=')
|
83
81
|
self._params[k] = v
|
84
82
|
return self._params
|
panther/base_websocket.py
CHANGED
@@ -182,10 +182,9 @@ class WebsocketConnections(Singleton):
|
|
182
182
|
but they have same Manager()
|
183
183
|
"""
|
184
184
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
asyncio.create_task(self())
|
185
|
+
# Schedule the async function to run in the background,
|
186
|
+
# We don't need to await for this task
|
187
|
+
asyncio.create_task(self())
|
189
188
|
|
190
189
|
@classmethod
|
191
190
|
async def handle_authentication(cls, connection: Websocket):
|
panther/generics.py
CHANGED
@@ -27,7 +27,7 @@ class ObjectRequired:
|
|
27
27
|
logger.critical(f'`{self.__class__.__name__}.object()` should return instance of a Model --> `find_one()`')
|
28
28
|
raise APIError
|
29
29
|
|
30
|
-
async def object(self, request: Request, **kwargs)
|
30
|
+
async def object(self, request: Request, **kwargs):
|
31
31
|
"""
|
32
32
|
Used in `RetrieveAPI`, `UpdateAPI`, `DeleteAPI`
|
33
33
|
"""
|
@@ -35,18 +35,18 @@ class ObjectRequired:
|
|
35
35
|
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
36
36
|
|
37
37
|
|
38
|
-
class
|
39
|
-
def
|
38
|
+
class CursorRequired:
|
39
|
+
def _check_cursor(self, cursor):
|
40
40
|
if isinstance(cursor, (Cursor, PantherDBCursor)) is False:
|
41
|
-
logger.critical(f'`{self.__class__.__name__}.
|
41
|
+
logger.critical(f'`{self.__class__.__name__}.cursor()` should return a Cursor --> `find()`')
|
42
42
|
raise APIError
|
43
43
|
|
44
|
-
async def
|
44
|
+
async def cursor(self, request: Request, **kwargs) -> Cursor | PantherDBCursor:
|
45
45
|
"""
|
46
46
|
Used in `ListAPI`
|
47
47
|
Should return `.find()`
|
48
48
|
"""
|
49
|
-
logger.error(f'`
|
49
|
+
logger.error(f'`cursor()` method is not implemented in {self.__class__} .')
|
50
50
|
raise APIError(status_code=status.HTTP_501_NOT_IMPLEMENTED)
|
51
51
|
|
52
52
|
|
@@ -58,15 +58,19 @@ class RetrieveAPI(GenericAPI, ObjectRequired):
|
|
58
58
|
return Response(data=instance, status_code=status.HTTP_200_OK)
|
59
59
|
|
60
60
|
|
61
|
-
class ListAPI(GenericAPI,
|
61
|
+
class ListAPI(GenericAPI, CursorRequired):
|
62
62
|
sort_fields: list[str]
|
63
63
|
search_fields: list[str]
|
64
64
|
filter_fields: list[str]
|
65
65
|
pagination: type[Pagination]
|
66
66
|
|
67
67
|
async def get(self, request: Request, **kwargs):
|
68
|
-
cursor = await self.
|
69
|
-
|
68
|
+
cursor = await self.prepare_cursor(request=request, **kwargs)
|
69
|
+
return Response(data=cursor, status_code=status.HTTP_200_OK)
|
70
|
+
|
71
|
+
async def prepare_cursor(self, request: Request, **kwargs):
|
72
|
+
cursor = await self.cursor(request=request, **kwargs)
|
73
|
+
self._check_cursor(cursor)
|
70
74
|
|
71
75
|
query = {}
|
72
76
|
query |= self.process_filters(query_params=request.query_params, cursor=cursor)
|
@@ -81,7 +85,7 @@ class ListAPI(GenericAPI, ObjectsRequired):
|
|
81
85
|
if pagination := self.process_pagination(query_params=request.query_params, cursor=cursor):
|
82
86
|
cursor = await pagination.paginate()
|
83
87
|
|
84
|
-
return
|
88
|
+
return cursor
|
85
89
|
|
86
90
|
def process_filters(self, query_params: dict, cursor: Cursor | PantherDBCursor) -> dict:
|
87
91
|
_filter = {}
|
@@ -90,6 +94,7 @@ class ListAPI(GenericAPI, ObjectsRequired):
|
|
90
94
|
if field in query_params:
|
91
95
|
if config.DATABASE.__class__.__name__ == 'MongoDBConnection':
|
92
96
|
with contextlib.suppress(Exception):
|
97
|
+
# Change type of the value if it is ObjectId
|
93
98
|
if cursor.cls.model_fields[field].metadata[0].func.__name__ == 'validate_object_id':
|
94
99
|
_filter[field] = bson.ObjectId(query_params[field])
|
95
100
|
continue
|
@@ -161,3 +166,7 @@ class DeleteAPI(GenericAPI, ObjectRequired):
|
|
161
166
|
|
162
167
|
await instance.delete()
|
163
168
|
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
169
|
+
|
170
|
+
|
171
|
+
class ListCreateAPI(CreateAPI, ListAPI):
|
172
|
+
pass
|
panther/main.py
CHANGED
@@ -70,7 +70,8 @@ class Panther:
|
|
70
70
|
if scope['type'] == 'lifespan':
|
71
71
|
message = await receive()
|
72
72
|
if message["type"] == 'lifespan.startup':
|
73
|
-
|
73
|
+
if config.HAS_WS:
|
74
|
+
await config.WEBSOCKET_CONNECTIONS.start()
|
74
75
|
await Event.run_startups()
|
75
76
|
elif message["type"] == 'lifespan.shutdown':
|
76
77
|
# It's not happening :\, so handle the shutdowns in __del__ ...
|
panther/response.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
import asyncio
|
2
2
|
from types import NoneType
|
3
|
-
from typing import Generator, AsyncGenerator
|
3
|
+
from typing import Generator, AsyncGenerator, Any, Type
|
4
4
|
|
5
5
|
import orjson as json
|
6
|
-
from pydantic import BaseModel
|
7
|
-
from pydantic._internal._model_construction import ModelMetaclass
|
6
|
+
from pydantic import BaseModel
|
8
7
|
|
9
8
|
from panther import status
|
10
9
|
from panther._utils import to_async_generator
|
@@ -12,7 +11,7 @@ from panther.db.cursor import Cursor
|
|
12
11
|
from pantherdb import Cursor as PantherDBCursor
|
13
12
|
from panther.monitoring import Monitoring
|
14
13
|
|
15
|
-
ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType |
|
14
|
+
ResponseDataTypes = list | tuple | set | Cursor | PantherDBCursor | dict | int | float | str | bool | bytes | NoneType | Type[BaseModel]
|
16
15
|
IterableDataTypes = list | tuple | set | Cursor | PantherDBCursor
|
17
16
|
StreamingDataTypes = Generator | AsyncGenerator
|
18
17
|
|
@@ -32,6 +31,7 @@ class Response:
|
|
32
31
|
:param status_code: should be int
|
33
32
|
"""
|
34
33
|
self.headers = headers or {}
|
34
|
+
self.initial_data = data
|
35
35
|
self.data = self.prepare_data(data=data)
|
36
36
|
self.status_code = self.check_status_code(status_code=status_code)
|
37
37
|
|
@@ -68,7 +68,7 @@ class Response:
|
|
68
68
|
elif isinstance(data, dict):
|
69
69
|
return {key: self.prepare_data(value) for key, value in data.items()}
|
70
70
|
|
71
|
-
elif issubclass(type(data),
|
71
|
+
elif issubclass(type(data), BaseModel):
|
72
72
|
return data.model_dump()
|
73
73
|
|
74
74
|
elif isinstance(data, IterableDataTypes):
|
@@ -79,25 +79,42 @@ class Response:
|
|
79
79
|
raise TypeError(msg)
|
80
80
|
|
81
81
|
@classmethod
|
82
|
-
def check_status_code(cls, status_code:
|
82
|
+
def check_status_code(cls, status_code: Any):
|
83
83
|
if not isinstance(status_code, int):
|
84
84
|
error = f'Response `status_code` Should Be `int`. (`{status_code}` is {type(status_code)})'
|
85
85
|
raise TypeError(error)
|
86
86
|
return status_code
|
87
87
|
|
88
|
-
|
89
|
-
def apply_output_model(cls, data: any, /, output_model: ModelMetaclass):
|
88
|
+
async def apply_output_model(self, output_model: Type[BaseModel]):
|
90
89
|
"""This method is called in API.__call__"""
|
90
|
+
|
91
91
|
# Dict
|
92
|
-
if isinstance(data, dict):
|
92
|
+
if isinstance(self.data, dict):
|
93
|
+
# Apply `validation_alias` (id -> _id)
|
93
94
|
for field_name, field in output_model.model_fields.items():
|
94
|
-
if field.validation_alias and field_name in data:
|
95
|
-
data[field.validation_alias] = data.pop(field_name)
|
96
|
-
|
95
|
+
if field.validation_alias and field_name in self.data:
|
96
|
+
self.data[field.validation_alias] = self.data.pop(field_name)
|
97
|
+
output = output_model(**self.data)
|
98
|
+
if hasattr(output_model, 'prepare_response'):
|
99
|
+
return await output.prepare_response(instance=self.initial_data, data=output.model_dump())
|
100
|
+
return output.model_dump()
|
97
101
|
|
98
102
|
# Iterable
|
99
|
-
|
100
|
-
|
103
|
+
results = []
|
104
|
+
if isinstance(self.data, IterableDataTypes):
|
105
|
+
for i, d in enumerate(self.data):
|
106
|
+
# Apply `validation_alias` (id -> _id)
|
107
|
+
for field_name, field in output_model.model_fields.items():
|
108
|
+
if field.validation_alias and field_name in d:
|
109
|
+
d[field.validation_alias] = d.pop(field_name)
|
110
|
+
|
111
|
+
output = output_model(**d)
|
112
|
+
if hasattr(output_model, 'prepare_response'):
|
113
|
+
result = await output.prepare_response(instance=self.initial_data[i], data=output.model_dump())
|
114
|
+
else:
|
115
|
+
result = output.model_dump()
|
116
|
+
results.append(result)
|
117
|
+
return results
|
101
118
|
|
102
119
|
# Str | Bool | Bytes
|
103
120
|
msg = 'Type of Response data is not match with `output_model`.\n*hint: You may want to remove `output_model`'
|
panther/serializer.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import typing
|
2
|
-
from typing import
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
from pydantic import create_model, BaseModel, ConfigDict
|
5
5
|
from pydantic.fields import FieldInfo, Field
|
@@ -205,13 +205,13 @@ class ModelSerializer(metaclass=MetaModelSerializer):
|
|
205
205
|
model: type[BaseModel]
|
206
206
|
request: Request
|
207
207
|
|
208
|
-
async def create(self, validated_data: dict)
|
208
|
+
async def create(self, validated_data: dict):
|
209
209
|
"""
|
210
210
|
validated_data = ModelSerializer.model_dump()
|
211
211
|
"""
|
212
212
|
return await self.model.insert_one(validated_data)
|
213
213
|
|
214
|
-
async def update(self, instance: Model, validated_data: dict)
|
214
|
+
async def update(self, instance: Model, validated_data: dict):
|
215
215
|
"""
|
216
216
|
instance = UpdateAPI.object()
|
217
217
|
validated_data = ModelSerializer.model_dump()
|
@@ -219,10 +219,13 @@ class ModelSerializer(metaclass=MetaModelSerializer):
|
|
219
219
|
await instance.update(validated_data)
|
220
220
|
return instance
|
221
221
|
|
222
|
-
async def partial_update(self, instance: Model, validated_data: dict)
|
222
|
+
async def partial_update(self, instance: Model, validated_data: dict):
|
223
223
|
"""
|
224
224
|
instance = UpdateAPI.object()
|
225
225
|
validated_data = ModelSerializer.model_dump(exclude_none=True)
|
226
226
|
"""
|
227
227
|
await instance.update(validated_data)
|
228
228
|
return instance
|
229
|
+
|
230
|
+
async def prepare_response(self, instance: Any, data: dict) -> dict:
|
231
|
+
return data
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: panther
|
3
|
-
Version: 4.
|
3
|
+
Version: 4.1.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
|
@@ -56,7 +56,7 @@ Requires-Dist: watchfiles ~=0.21.0 ; extra == 'full'
|
|
56
56
|
- Support Custom **Middlewares**
|
57
57
|
- Support Custom **Throttling**
|
58
58
|
- Support **Function-Base** and **Class-Base** APIs
|
59
|
-
- It's One Of The **Fastest Python
|
59
|
+
- It's One Of The **Fastest Python Framework**
|
60
60
|
---
|
61
61
|
|
62
62
|
### Supported by
|
@@ -68,25 +68,6 @@ Requires-Dist: watchfiles ~=0.21.0 ; extra == 'full'
|
|
68
68
|
|
69
69
|
---
|
70
70
|
|
71
|
-
### Benchmark
|
72
|
-
|
73
|
-
| Framework | Throughput (Request/Second) |
|
74
|
-
|------------|-----------------------------|
|
75
|
-
| Blacksheep | 5,339 |
|
76
|
-
| Muffin | 5,320 |
|
77
|
-
| Panther | 5,112 |
|
78
|
-
| Sanic | 3,660 |
|
79
|
-
| FastAPI | 3,260 |
|
80
|
-
| Tornado | 2,081 |
|
81
|
-
| Bottle | 2,045 |
|
82
|
-
| Django | 821 |
|
83
|
-
| Flask | 749 |
|
84
|
-
|
85
|
-
|
86
|
-
> **More Detail:** https://GitHub.com/PantherPy/frameworks-benchmark
|
87
|
-
|
88
|
-
---
|
89
|
-
|
90
71
|
### Installation
|
91
72
|
```shell
|
92
73
|
$ pip install panther
|
@@ -105,7 +86,7 @@ $ pip install panther
|
|
105
86
|
```shell
|
106
87
|
$ panther run --reload
|
107
88
|
```
|
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
|
89
|
+
_* 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
90
|
|
110
91
|
- #### Monitoring Requests
|
111
92
|
|
@@ -1,26 +1,26 @@
|
|
1
|
-
panther/__init__.py,sha256=
|
1
|
+
panther/__init__.py,sha256=y4niTNbN1Jwrm8eXNSrzLlVOGoajNgm3E6I1fs2i7QA,110
|
2
2
|
panther/_load_configs.py,sha256=AVkoixkUFkBQiTmrLrwCmg0eiPW2U_Uw2EGNEGQRfnI,9281
|
3
3
|
panther/_utils.py,sha256=xeVR0yHvczhXv2XXrpoa6SHpGTDTFxNxiemXTdbsqjM,4279
|
4
|
-
panther/app.py,sha256=
|
4
|
+
panther/app.py,sha256=a0QLpxM5fwLyWDHZjVaYrLw0_Rfx3p2NnZWcfyCzwf4,7267
|
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=
|
8
|
-
panther/base_websocket.py,sha256=
|
7
|
+
panther/base_request.py,sha256=Fwwpm-9bjAZdpzSdakmSas5BD3gh1nrc6iGcBxwa_94,4001
|
8
|
+
panther/base_websocket.py,sha256=hJN_ItUGLpk0QMWrExlDHQahiu7hYgc_jVvHWxqqpq4,10547
|
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
13
|
panther/file_handler.py,sha256=XnomEigCUYOaXjkH4kD1kzpUbL2i9lLnR5kerruF6BA,846
|
14
|
-
panther/generics.py,sha256=
|
14
|
+
panther/generics.py,sha256=HGbCGvHEpbWaY4MrtI_Wphkm9YEGR8PV_X9uERSRGVI,6576
|
15
15
|
panther/logging.py,sha256=t0nQXsSIwIxShqFnjRGp6lhO4Ybf1SnwJraDSTqMHFM,2211
|
16
|
-
panther/main.py,sha256=
|
16
|
+
panther/main.py,sha256=UbIxwaojvY_vH9nYfBpkulRBqVEj4Lbl81Er4XW_KCY,9334
|
17
17
|
panther/monitoring.py,sha256=y1F3c8FJlnmooM-m1nSyOTa9eWq0v1nHnmw9zz-4Kls,1314
|
18
18
|
panther/pagination.py,sha256=efpsWMgLBaTWXhnhMAf6fyIrGTmVOFbmHpX03GgEJh0,1574
|
19
19
|
panther/permissions.py,sha256=9-J5vzvEKa_PITwEVQbZZv8PG2FOu05YBlD5yMrKcfc,348
|
20
20
|
panther/request.py,sha256=F9ZiAWSse7_6moAzqdoFInUN4zTKlzijh9AdU9w3Jfw,1673
|
21
|
-
panther/response.py,sha256=
|
21
|
+
panther/response.py,sha256=G0Ychc3Tp5UANtmst7o4npOrmdca6kKC_HYYoeYWLUQ,7266
|
22
22
|
panther/routings.py,sha256=1eqbjubLnUUEQRlz8mIF464ImvCMjyasiekHBtxEQoQ,6218
|
23
|
-
panther/serializer.py,sha256=
|
23
|
+
panther/serializer.py,sha256=MBT43UG8YBjp-UGaqe5-SPqQHIcDEjLAdBjHAVKyMJo,9059
|
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
|
@@ -49,9 +49,9 @@ 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.1.0.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
|
53
|
+
panther-4.1.0.dist-info/METADATA,sha256=oxZtMCsV7y5Hdhejw9V3NTrXaOof5nz0jLlvHDeE2pA,6376
|
54
|
+
panther-4.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
55
|
+
panther-4.1.0.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
|
56
|
+
panther-4.1.0.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
|
57
|
+
panther-4.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|