panther 1.7.1__py3-none-any.whl → 1.7.3__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 .main import Panther
2
2
 
3
- __version__ = '1.7.1'
3
+ __version__ = '1.7.3'
4
4
 
5
5
 
6
6
  def version():
panther/app.py CHANGED
@@ -41,44 +41,44 @@ class API:
41
41
  async def wrapper(*args, **kwargs):
42
42
  self.request: Request = kwargs.pop('request') # NOQA: Non-self attribute could not be type hinted
43
43
 
44
- # Handle Authentication
44
+ # 1. Authentication
45
45
  self.handle_authentications()
46
46
 
47
- # Handle Throttling
47
+ # 2. Throttling
48
48
  self.handle_throttling()
49
49
 
50
- # Handle Authentication
50
+ # 3. Permissions
51
51
  self.handle_permissions()
52
52
 
53
- # Validate Input
53
+ # 4. Validate Input
54
54
  self.validate_input()
55
55
 
56
- # Validate Path Variables
56
+ # 5. Validate Path Variables
57
57
  self.validate_path_variables(func, kwargs)
58
58
 
59
- # Get Cached Response
59
+ # 6. Get Cached Response
60
60
  if self.cache and self.request.method == 'GET':
61
61
  if cached := get_cached_response_data(request=self.request):
62
62
  return Response(data=cached.data, status_code=cached.status_code)
63
63
 
64
- # Put Request In kwargs
64
+ # 7. Put Request In kwargs
65
65
  if req_arg := [k for k, v in func.__annotations__.items() if v == Request]:
66
66
  kwargs[req_arg[0]] = self.request
67
67
 
68
- # Call Endpoint
68
+ # 8. Call Endpoint
69
69
  response = await func(**kwargs)
70
70
 
71
- # Clean Output
71
+ # 9. Clean Output
72
72
  if not isinstance(response, Response):
73
73
  response = Response(data=response)
74
74
  data = self.serialize_response_data(data=response._data) # NOQA: Access to a protected member
75
75
  response.set_data(data)
76
76
 
77
- # Set New Response To Cache
77
+ # 10. Set New Response To Cache
78
78
  if self.cache and self.request.method == 'GET':
79
79
  set_cache_response(request=self.request, response=response, cache_exp_time=self.cache_exp_time)
80
80
 
81
- # Warning CacheExpTime
81
+ # 11. Warning CacheExpTime
82
82
  if self.cache_exp_time and self.cache is False:
83
83
  logger.warning('"cache_exp_time" won\'t work while "cache" is False')
84
84
 
@@ -1,44 +1,36 @@
1
1
  import os
2
2
  from collections import deque
3
3
 
4
- from rich import box
5
- from rich.align import Align
6
- from rich.console import Console, Group
4
+ from rich.console import Group
7
5
  from rich.layout import Layout
8
- from rich.live import Live
6
+ from rich.align import Align
9
7
  from rich.panel import Panel
10
8
  from rich.table import Table
11
9
  from watchfiles import watch
12
-
13
- from panther.cli.utils import cli_error
10
+ from rich.live import Live
11
+ from rich import box
14
12
 
15
13
 
16
14
  def monitor() -> None:
17
- def _generate_table(rows):
15
+ monitoring_log_file = 'logs/monitoring.log'
16
+
17
+ def _generate_table(rows: deque):
18
18
  layout = Layout()
19
- console = Console()
20
19
 
21
20
  rows = list(rows)
22
- n_rows = os.get_terminal_size()[1]
23
-
24
- while n_rows >= 0:
25
- table = Table(box=box.MINIMAL_DOUBLE_HEAD)
26
- table.add_column('Datetime', justify='center', style='magenta', no_wrap=True)
27
- table.add_column('Method', justify='center', style='cyan')
28
- table.add_column('Path', justify='center', style='cyan')
29
- table.add_column('Client', justify='center', style='cyan')
30
- table.add_column('Response Time', justify='center', style='blue')
31
- table.add_column('Status Code', justify='center', style='blue')
21
+ _, lines = os.get_terminal_size()
32
22
 
33
- for row in rows[-n_rows:]:
34
- table.add_row(*row)
35
- layout.update(table)
36
- render_map = layout.render(console, console.options)
23
+ table = Table(box=box.MINIMAL_DOUBLE_HEAD)
24
+ table.add_column('Datetime', justify='center', style='magenta', no_wrap=True)
25
+ table.add_column('Method', justify='center', style='cyan')
26
+ table.add_column('Path', justify='center', style='cyan')
27
+ table.add_column('Client', justify='center', style='cyan')
28
+ table.add_column('Response Time', justify='center', style='blue')
29
+ table.add_column('Status Code', justify='center', style='blue')
37
30
 
38
- if len(render_map[layout].render[-1]) > 2:
39
- n_rows -= 1 # The table is overflowing
40
- else:
41
- break
31
+ for row in rows[-lines:]: # It will give us "lines" last lines of "rows"
32
+ table.add_row(*row)
33
+ layout.update(table)
42
34
 
43
35
  return Panel(
44
36
  Align.center(Group(table)),
@@ -48,22 +40,19 @@ def monitor() -> None:
48
40
  border_style='bright_blue',
49
41
  )
50
42
 
51
- try:
52
- with open('logs/monitoring.log') as f:
53
- f.readlines()
54
- width, height = os.get_terminal_size()
55
- messages = deque(maxlen=height - 8) # Save space for header and footer
43
+ with open(monitoring_log_file) as f:
44
+ f.readlines() # Set cursor at the end of file
56
45
 
57
- with Live(_generate_table(messages), auto_refresh=False, vertical_overflow='visible', screen=True) as live:
58
- # TODO: Is it only watch logs/monitoring.log or the whole directory ?
59
- for _ in watch('logs/monitoring.log'):
46
+ _, init_lines_count = os.get_terminal_size()
47
+ messages = deque(maxlen=init_lines_count - 10) # Save space for header and footer
48
+
49
+ with Live(_generate_table(messages), auto_refresh=False, vertical_overflow='visible', screen=True) as live:
50
+ try:
51
+ for _ in watch(monitoring_log_file):
60
52
  data = f.readline().split('|')
53
+ # 2023-03-24 01:42:52 | GET | /user/317/ | 127.0.0.1:48856 | 0.0366 ms | 200
61
54
  messages.append(data)
62
55
  live.update(_generate_table(messages))
63
56
  live.refresh()
64
-
65
- except FileNotFoundError:
66
- cli_error("Monitor Log File Does Not Exists.\n\nHint: Make sure 'Monitor' is True in 'core/configs' "
67
- "or you are in a correct directory.")
68
- except KeyboardInterrupt:
69
- pass
57
+ except KeyboardInterrupt:
58
+ pass
panther/cli/template.py CHANGED
@@ -10,15 +10,16 @@ from panther.configs import config
10
10
  from panther import version, status
11
11
  from panther.request import Request
12
12
  from panther.response import Response
13
+ from app.throttling import InfoThrottling
13
14
 
14
15
 
15
16
  @API()
16
- async def hello_world():
17
+ async def hello_world_api():
17
18
  return {'detail': 'Hello World'}
18
19
 
19
20
 
20
- @API(cache=True)
21
- async def info(request: Request):
21
+ @API(cache=True, throttling=InfoThrottling)
22
+ async def info_api(request: Request):
22
23
  data = {
23
24
  'version': version(),
24
25
  'datetime_now': datetime.now().isoformat(),
@@ -34,11 +35,18 @@ models_py = """from panther.db import Model
34
35
  serializers_py = """from pydantic import BaseModel as Serializer
35
36
  """
36
37
 
37
- app_urls_py = """from app.apis import hello_world, info
38
+ throttling_py = """from datetime import timedelta
39
+ from panther.throttling import Throttling
40
+
41
+
42
+ InfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))
43
+ """
44
+
45
+ app_urls_py = """from app.apis import hello_world_api, info_api
38
46
 
39
47
  urls = {
40
- '': hello_world,
41
- 'info/': info,
48
+ '': hello_world_api,
49
+ 'info/': info_api,
42
50
  }
43
51
  """
44
52
 
@@ -56,23 +64,29 @@ env = load_env(BASE_DIR / '.env')
56
64
  DB_NAME = env['DB_NAME']
57
65
  SECRET_KEY = env['SECRET_KEY']
58
66
 
59
-
67
+ # # # More Info: Https://PantherPy.GitHub.io/middlewares/
60
68
  MIDDLEWARES = [
61
69
  ('panther.middlewares.db.Middleware', {'url': f'pantherdb://{BASE_DIR}/{DB_NAME}.pantherdb'}),
62
70
  ]
63
71
 
64
72
  USER_MODEL = 'panther.db.models.User'
65
73
 
74
+ # # # More Info: Https://PantherPy.GitHub.io/authentications/
75
+ AUTHENTICATION = 'panther.authentications.JWTAuthentication'
76
+
77
+ # # # More Info: Https://PantherPy.GitHub.io/monitoring/
66
78
  MONITORING = True
67
79
 
80
+ # # # More Info: Https://PantherPy.GitHub.io/log_queries/
68
81
  LOG_QUERIES = True
69
82
 
83
+ # # # More Info: Https://PantherPy.GitHub.io/throttling/
84
+ THROTTLING = Throttling(rate=60, duration=timedelta(minutes=1))
85
+
86
+ # # # More Info: Https://PantherPy.GitHub.io/urls/
70
87
  URLs = 'core/urls.py'
71
88
  """ % datetime.now().date().isoformat()
72
89
 
73
- middlewares = """from panther.middlewares import BaseMiddleware
74
- """
75
-
76
90
  env = """
77
91
  SECRET_KEY = '%s'
78
92
 
@@ -94,8 +108,9 @@ urls = {
94
108
  git_ignore = """__pycache__/
95
109
  .venv/
96
110
  .idea/
97
- .env
98
111
  logs/
112
+
113
+ .env
99
114
  *.pantherdb
100
115
  """
101
116
 
@@ -107,11 +122,11 @@ Template = {
107
122
  'apis.py': apis_py,
108
123
  'models.py': models_py,
109
124
  'serializers.py': serializers_py,
125
+ 'throttling.py': throttling_py,
110
126
  'urls.py': app_urls_py,
111
127
  },
112
128
  'core': {
113
129
  'configs.py': configs_py,
114
- 'middlewares.py': middlewares,
115
130
  'urls.py': urls_py,
116
131
  },
117
132
  'main.py': main_py,
panther/db/utils.py CHANGED
@@ -26,7 +26,7 @@ def clean_object_id(_id: bson.ObjectId | str) -> bson.ObjectId:
26
26
  try:
27
27
  return bson.ObjectId(_id)
28
28
  except Exception:
29
- raise bson.errors.InvalidId
29
+ raise bson.errors.InvalidId # NOQA: Py Unresolved References
30
30
 
31
31
 
32
32
  def clean_object_id_in_dicts(*args):
panther/logger.py CHANGED
@@ -2,8 +2,8 @@ import os
2
2
  import logging
3
3
  from pydantic import BaseModel
4
4
 
5
- from logging.config import dictConfig
6
5
  from panther.configs import config
6
+ from logging.config import dictConfig
7
7
 
8
8
 
9
9
  LOGS_DIR = config['base_dir'] / 'logs'
panther/main.py CHANGED
@@ -180,11 +180,10 @@ class Panther:
180
180
  if path.find('panther.middlewares.db.Middleware') != -1:
181
181
  config['db_engine'] = data['url'].split(':')[0]
182
182
 
183
- # noinspection PyPep8Naming
184
- Middleware = import_class(path)
183
+ Middleware = import_class(path) # NOQA: Py Pep8 Naming
185
184
  if not issubclass(Middleware, BaseMiddleware):
186
185
  logger.critical(f'{Middleware} is not a sub class of BaseMiddleware.')
187
186
  continue
188
- # noinspection PyArgumentList
189
- middlewares.append(Middleware(**data))
187
+
188
+ middlewares.append(Middleware(**data)) # NOQA: Py Argument List
190
189
  return middlewares
@@ -20,17 +20,9 @@ class Middleware(BaseMiddleware):
20
20
  self.kwargs['host'] = '127.0.0.1'
21
21
 
22
22
  def validate_port(self):
23
- if port := self.kwargs.get('port'):
24
- if isinstance(port, str):
25
- try:
26
- self.kwargs['port'] = int(port)
27
- except ValueError:
28
- logger.critical('Redis "port" should be number')
29
-
30
- elif not isinstance(port, int):
31
- logger.critical('Redis "port" is not valid.')
32
- else:
33
- self.kwargs['port'] = 6379
23
+ port = self.kwargs.setdefault('port', 6379)
24
+ if not isinstance(port, int):
25
+ logger.critical('Redis "port" is not valid.')
34
26
 
35
27
  async def before(self, request: Request) -> Request:
36
28
  self.redis = RedisConnection(**self.kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: panther
3
- Version: 1.7.1
3
+ Version: 1.7.3
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
@@ -12,35 +12,41 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Requires-Python: >=3.11
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
- Requires-Dist: pantherdb (>=1.2.0)
16
- Requires-Dist: pydantic (>=1.10.5)
17
- Requires-Dist: watchfiles (>=0.18.1)
18
- Requires-Dist: uvicorn[standard]
19
15
  Requires-Dist: bpython (>=0.24)
20
16
  Requires-Dist: bson (>=0.5.10)
21
- Requires-Dist: redis (>=4.0.2)
22
- Requires-Dist: rich (>=13.3.1)
17
+ Requires-Dist: httptools (>=0.5.0)
18
+ Requires-Dist: pantherdb (>=1.2.2)
19
+ Requires-Dist: pydantic (>=1.10.7)
20
+ Requires-Dist: redis (>=4.5.3)
21
+ Requires-Dist: rich (>=13.3.2)
22
+ Requires-Dist: uvicorn (>=0.21.1)
23
+ Requires-Dist: uvloop (>=0.17.0)
24
+ Requires-Dist: watchfiles (>=0.18.1)
23
25
  Provides-Extra: full
24
26
  Requires-Dist: python-jose (>=3.3.0) ; extra == 'full'
25
27
  Requires-Dist: pymongo (>=4.3.3) ; extra == 'full'
26
28
 
27
-
28
29
  ## Panther
29
30
  <b>Is A Fast & Friendly, Web Framework For Building Async APIs With Python 3.11+</b>
30
31
 
32
+ <p align="center">
33
+ <img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo.png" alt="logo" style="width: 200px">
34
+ </p>
35
+
31
36
  >_Full Documentation_ -> [https://pantherpy.github.io](https://pantherpy.github.io)
32
37
  >
33
38
  >_PyPI_ -> [https://pypi.org/project/panther/](https://pypi.org/project/panther/)
34
39
 
35
40
  ---
36
41
 
37
- ### Features
42
+ ### Why Use Panther ?
38
43
  - Document-oriented Databases ODM ([PantherDB](https://pypi.org/project/pantherdb/), MongoDB)
39
44
  - Visual API Monitoring (In Terminal)
40
- - Cache APIs (In Memory, In Redis)
45
+ - Caching for APIs (In Memory, In Redis)
41
46
  - Built-in Authentication Classes (Customizable)
42
47
  - Built-in Permission Classes (Customizable)
43
48
  - Handle Custom Middlewares
49
+ - Handle Custom Throttling
44
50
  ---
45
51
 
46
52
  ### Benchmark
@@ -160,13 +166,14 @@ with [https://github.com/nakabonne/ali](https://github.com/nakabonne/ali) and he
160
166
  **app/apis.py**:
161
167
 
162
168
  ```python
163
- from datetime import datetime
169
+ from datetime import datetime, timedelta
164
170
 
165
171
  from panther.app import API
166
172
  from panther.configs import config
167
173
  from panther import version, status
168
174
  from panther.request import Request
169
175
  from panther.response import Response
176
+ from panther.throttling import Throttling
170
177
 
171
178
 
172
179
  @API()
@@ -174,7 +181,7 @@ with [https://github.com/nakabonne/ali](https://github.com/nakabonne/ali) and he
174
181
  return {'detail': 'Hello World'}
175
182
 
176
183
 
177
- @API(cache=True)
184
+ @API(cache=True, throttling=Throttling(rate=5, duration=timedelta(minutes=1)))
178
185
  async def info(request: Request):
179
186
  data = {
180
187
  'version': version(),
@@ -1,12 +1,12 @@
1
- panther/__init__.py,sha256=3AJjmWhBodf_pBho1kKI_7Pu4BOxGInDcBBKOsiXBX8,89
1
+ panther/__init__.py,sha256=ZNlrk0ToNVSaok7NcXQHwGl68nxc1gOv0q8amPR0wKU,89
2
2
  panther/_utils.py,sha256=EC-5HJnVsUmfY9LCoauUGtRsShGJVbL0ssvYn8CRvSU,3668
3
- panther/app.py,sha256=vjaCkbV5SnkQPMu31ttGubyHA-NiqqVPXdebBtmOjC0,6252
3
+ panther/app.py,sha256=gHyxMH1nQrj_JlBKfdfYAXUXB_Yc3Xka9AYd5s8rVCo,6263
4
4
  panther/authentications.py,sha256=ZOpgWzaQXy5sGgXrWY_DtfHQ55hYLYOBt-BHejIYbZM,3124
5
5
  panther/caching.py,sha256=ja7r2iNJxP6-XUuKBKAcnQQ2s72XYRPpAnbV-YMobxE,2382
6
6
  panther/configs.py,sha256=HZuLA4UvJGfBB8PUDuKjOkyKS0EAvZZBVcw5epEYKnQ,1076
7
7
  panther/exceptions.py,sha256=hXwV0NQSNZZxMtbzDHKIC2363AWMSyFPIHb-DqU2SLw,1150
8
- panther/logger.py,sha256=1skaZg4w55mTh4pSrCohkGp-7dg62lyEIoFAJKMP5L8,2499
9
- panther/main.py,sha256=LwOuvKJ0utK-Zfhs9k-m19AeQKIfsHy5FUZjJQy_v1s,7866
8
+ panther/logger.py,sha256=FjOG9CNj-Sqo-pCEy-T3019R8oswA44r8W6g9WE0zok,2499
9
+ panther/main.py,sha256=AmCFA3mH-70arhzirk3CLS4qB7i-ANK_5cHWvCMSA9o,7835
10
10
  panther/permissions.py,sha256=iKKUNbYH6GKCgS0Au1ildsDsW_gGOs_XlaYiZBtaR-s,383
11
11
  panther/request.py,sha256=BjpsE_u9uMjG1F1FFHa32LhdhxN7Jlv2rqJg3CiCgh0,4382
12
12
  panther/response.py,sha256=h57bWRGR8neKxKyfcoi1w3hKIVWjDCrC3zgMr4pf4bg,1592
@@ -17,14 +17,14 @@ panther/utils.py,sha256=8RyLF4zOf8OQkwf-n2n2RwYFr-mR_rg12tVli1pAhHI,947
17
17
  panther/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  panther/cli/create_command.py,sha256=F472b-4WH5vC7ZkvZ1qp3MHPr_j5BMM-Zdy2pAllFLI,2910
19
19
  panther/cli/main.py,sha256=spotgTcfXquc07Sx4tdFpxdLJG2vcYJRpTyZZ9Up0sM,739
20
- panther/cli/monitor_command.py,sha256=7A76Yya761mkmDB2Rqp_vIAPgtflBaDfn1kAYtmHqu0,2470
20
+ panther/cli/monitor_command.py,sha256=ahInyna59bWW0LhIfI-CLoE8BKFFApLK9OtKR71vk4o,2076
21
21
  panther/cli/run_command.py,sha256=qpK2Bxe2_tyNppGmhhP1LRKDBVmTdlbZHlSNGAI33Jo,1509
22
- panther/cli/template.py,sha256=GA2DpYU2GvmzJqB0tGJ62Eo-CEVs76MVx-bliJMFdtE,2313
22
+ panther/cli/template.py,sha256=S9Q89WIyWqoBvA6izyBYIsLtBWZjG9XznLTxoZQbe8k,2974
23
23
  panther/cli/utils.py,sha256=rpjSK0T0-0m_UxAnYGPl0-zJ0fVlxmIFRYqhLAddBcY,8539
24
24
  panther/db/__init__.py,sha256=0mo9HwD_JAjJ-kRudbZqWNgzSxJ2t0ewh7Pa-83FhPY,50
25
25
  panther/db/connection.py,sha256=jQkY-JJu4LMHUau8-G6AQzlODwzESkghcLALe6wsR4g,2207
26
26
  panther/db/models.py,sha256=1s2F2STMLvqZoZGw_oqy_hrWk-T-wM_yKQT1odfrci8,1020
27
- panther/db/utils.py,sha256=Z9qG6zncy6TrkQYuTxh7TkPIVBb0Rr2x7g-upp9-bMw,1255
27
+ panther/db/utils.py,sha256=Axf7XvkHCr-Ky7c9CJPpQbqgf9kWgW_gVdTqbQlZc94,1289
28
28
  panther/db/queries/__init__.py,sha256=BMffHS9RbHE-AUAeT9C5uY3L-hpDh0WGRduDUQ9Kpuc,41
29
29
  panther/db/queries/mongodb_queries.py,sha256=uXd81Q2ukJskFDYlNfOiuCCkHuH7URyfZMkD6MUiVkk,3205
30
30
  panther/db/queries/pantherdb_queries.py,sha256=Ee3Kb5WIGQ-ZDeAYFHYx_Y34f-I0RgLT0XYFEof5FLM,2505
@@ -33,10 +33,10 @@ panther/middlewares/__init__.py,sha256=7RtHuS-MfybnJc6pcBSGhi9teXNhDsnJ3n7h_cXSk
33
33
  panther/middlewares/base.py,sha256=Php29ckITeGZm6GfauFG3i61bcsb4qoU8RpPLTqsfls,240
34
34
  panther/middlewares/db.py,sha256=C_PevTIaMykJl0NaaMYEfwE_oLdSLfKW2HR9UoPN1dU,508
35
35
  panther/middlewares/monitoring.py,sha256=oRFRGRNuoBOkGd46VYsXpRHRIklSuSglfuy574kHM4Y,863
36
- panther/middlewares/redis.py,sha256=bmKPDUH3Bs1ax-gYd7QX91KnvxROKAnuihvUlPqsNRI,1317
37
- panther-1.7.1.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
38
- panther-1.7.1.dist-info/METADATA,sha256=Ry2z4ZATzoAD3W2H0anrtPGxkBSHQSBfDrdQ8jCzuy0,5160
39
- panther-1.7.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
40
- panther-1.7.1.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
41
- panther-1.7.1.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
42
- panther-1.7.1.dist-info/RECORD,,
36
+ panther/middlewares/redis.py,sha256=m_a0QPqUP_OJyuDJtRxCQpn5m_DLdrDIVHHFcytQmAU,1046
37
+ panther-1.7.3.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
38
+ panther-1.7.3.dist-info/METADATA,sha256=4GHwPbXf3VMfZCuJFO_pmTGsp8MYI69hFL2oZQarkVo,5531
39
+ panther-1.7.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
40
+ panther-1.7.3.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
41
+ panther-1.7.3.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
42
+ panther-1.7.3.dist-info/RECORD,,