panther 3.7.0__py3-none-any.whl → 3.8.1__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/base_websocket.py +71 -30
- panther/cli/template.py +2 -3
- panther/db/connection.py +4 -1
- panther/main.py +23 -12
- panther/response.py +2 -1
- panther/serializer.py +26 -13
- panther/websocket.py +4 -31
- {panther-3.7.0.dist-info → panther-3.8.1.dist-info}/METADATA +1 -1
- {panther-3.7.0.dist-info → panther-3.8.1.dist-info}/RECORD +14 -14
- {panther-3.7.0.dist-info → panther-3.8.1.dist-info}/LICENSE +0 -0
- {panther-3.7.0.dist-info → panther-3.8.1.dist-info}/WHEEL +0 -0
- {panther-3.7.0.dist-info → panther-3.8.1.dist-info}/entry_points.txt +0 -0
- {panther-3.7.0.dist-info → panther-3.8.1.dist-info}/top_level.txt +0 -0
panther/__init__.py
CHANGED
panther/base_websocket.py
CHANGED
@@ -3,7 +3,8 @@ from __future__ import annotations
|
|
3
3
|
import asyncio
|
4
4
|
import contextlib
|
5
5
|
import logging
|
6
|
-
from
|
6
|
+
from multiprocessing import Manager
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
7
8
|
|
8
9
|
import orjson as json
|
9
10
|
|
@@ -11,19 +12,35 @@ from panther import status
|
|
11
12
|
from panther._utils import generate_ws_connection_id
|
12
13
|
from panther.base_request import BaseRequest
|
13
14
|
from panther.configs import config
|
15
|
+
from panther.db.connection import redis
|
14
16
|
from panther.utils import Singleton
|
15
17
|
|
16
18
|
if TYPE_CHECKING:
|
17
19
|
from redis import Redis
|
18
20
|
|
19
|
-
|
20
21
|
logger = logging.getLogger('panther')
|
21
22
|
|
22
23
|
|
24
|
+
class PubSub:
|
25
|
+
def __init__(self, manager):
|
26
|
+
self._manager = manager
|
27
|
+
self._subscribers = self._manager.list()
|
28
|
+
|
29
|
+
def subscribe(self):
|
30
|
+
queue = self._manager.Queue()
|
31
|
+
self._subscribers.append(queue)
|
32
|
+
return queue
|
33
|
+
|
34
|
+
def publish(self, msg):
|
35
|
+
for queue in self._subscribers:
|
36
|
+
queue.put(msg)
|
37
|
+
|
38
|
+
|
23
39
|
class WebsocketConnections(Singleton):
|
24
|
-
def __init__(self):
|
40
|
+
def __init__(self, manager: Manager = None):
|
25
41
|
self.connections = {}
|
26
42
|
self.connections_count = 0
|
43
|
+
self.manager = manager
|
27
44
|
|
28
45
|
def __call__(self, r: Redis | None):
|
29
46
|
if r:
|
@@ -31,39 +48,61 @@ class WebsocketConnections(Singleton):
|
|
31
48
|
subscriber.subscribe('websocket_connections')
|
32
49
|
logger.info("Subscribed to 'websocket_connections' channel")
|
33
50
|
for channel_data in subscriber.listen():
|
34
|
-
# Check Type of PubSub Message
|
35
51
|
match channel_data['type']:
|
52
|
+
# Subscribed
|
36
53
|
case 'subscribe':
|
37
54
|
continue
|
38
55
|
|
56
|
+
# Message Received
|
39
57
|
case 'message':
|
40
58
|
loaded_data = json.loads(channel_data['data'].decode())
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
59
|
+
self._handle_received_message(received_message=loaded_data)
|
60
|
+
|
61
|
+
case unknown_type:
|
62
|
+
logger.debug(f'Unknown Channel Type: {unknown_type}')
|
63
|
+
else:
|
64
|
+
self.pubsub = PubSub(manager=self.manager)
|
65
|
+
queue = self.pubsub.subscribe()
|
66
|
+
logger.info("Subscribed to 'websocket_connections' queue")
|
67
|
+
while True:
|
68
|
+
received_message = queue.get()
|
69
|
+
self._handle_received_message(received_message=received_message)
|
70
|
+
|
71
|
+
def _handle_received_message(self, received_message):
|
72
|
+
if (
|
73
|
+
isinstance(received_message, dict)
|
74
|
+
and (connection_id := received_message.get('connection_id'))
|
75
|
+
and connection_id in self.connections
|
76
|
+
and 'action' in received_message
|
77
|
+
and 'data' in received_message
|
78
|
+
):
|
79
|
+
# Check Action of WS
|
80
|
+
match received_message['action']:
|
81
|
+
case 'send':
|
82
|
+
asyncio.run(self.connections[connection_id].send(data=received_message['data']))
|
83
|
+
case 'close':
|
84
|
+
with contextlib.suppress(RuntimeError):
|
85
|
+
asyncio.run(self.connections[connection_id].close(
|
86
|
+
code=received_message['data']['code'],
|
87
|
+
reason=received_message['data']['reason']
|
88
|
+
))
|
89
|
+
# We are trying to disconnect the connection between a thread and a user
|
90
|
+
# from another thread, it's working, but we have to find another solution for it
|
91
|
+
#
|
92
|
+
# Error:
|
93
|
+
# Task <Task pending coro=<Websocket.close()>> got Future
|
94
|
+
# <Task pending coro=<WebSocketCommonProtocol.transfer_data()>>
|
95
|
+
# attached to a different loop
|
96
|
+
case unknown_action:
|
97
|
+
logger.debug(f'Unknown Message Action: {unknown_action}')
|
98
|
+
|
99
|
+
def publish(self, connection_id: str, action: Literal['send', 'close'], data: any):
|
100
|
+
publish_data = {'connection_id': connection_id, 'action': action, 'data': data}
|
101
|
+
|
102
|
+
if redis.is_connected:
|
103
|
+
redis.publish('websocket_connections', json.dumps(publish_data))
|
104
|
+
else:
|
105
|
+
self.pubsub.publish(publish_data)
|
67
106
|
|
68
107
|
async def new_connection(self, connection: Websocket) -> None:
|
69
108
|
await connection.connect(**connection.path_variables)
|
@@ -106,6 +145,7 @@ class Websocket(BaseRequest):
|
|
106
145
|
pass
|
107
146
|
|
108
147
|
async def send(self, data: any = None) -> None:
|
148
|
+
logger.debug(f'Sending WS Message to {self.connection_id}')
|
109
149
|
if data:
|
110
150
|
if isinstance(data, bytes):
|
111
151
|
await self.send_bytes(bytes_data=data)
|
@@ -121,6 +161,7 @@ class Websocket(BaseRequest):
|
|
121
161
|
await self.asgi_send({'type': 'websocket.send', 'bytes': bytes_data})
|
122
162
|
|
123
163
|
async def close(self, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = '') -> None:
|
164
|
+
logger.debug(f'Closing WS Connection {self.connection_id}')
|
124
165
|
self.is_connected = False
|
125
166
|
config['websocket_connections'].remove_connection(self)
|
126
167
|
await self.asgi_send({'type': 'websocket.close', 'code': code, 'reason': reason})
|
panther/cli/template.py
CHANGED
@@ -33,7 +33,7 @@ async def info_api(request: Request):
|
|
33
33
|
models_py = """from panther.db import Model
|
34
34
|
"""
|
35
35
|
|
36
|
-
serializers_py = """from
|
36
|
+
serializers_py = """from panther.serializer import ModelSerializer
|
37
37
|
"""
|
38
38
|
|
39
39
|
throttling_py = """from datetime import timedelta
|
@@ -70,8 +70,7 @@ SECRET_KEY = env['SECRET_KEY']{DATABASE}{USER_MODEL}{AUTHENTICATION}{MONITORING}
|
|
70
70
|
URLs = 'core.urls.url_routing'
|
71
71
|
""" % datetime.now().date().isoformat()
|
72
72
|
|
73
|
-
env = """
|
74
|
-
SECRET_KEY = '%s'
|
73
|
+
env = """SECRET_KEY='%s'
|
75
74
|
""" % generate_secret_key()
|
76
75
|
|
77
76
|
main_py = """from panther import Panther
|
panther/db/connection.py
CHANGED
@@ -54,12 +54,15 @@ class DBSession(Singleton):
|
|
54
54
|
self._session: Database = self._client.get_database()
|
55
55
|
|
56
56
|
def _create_pantherdb_session(self, db_url: str) -> None:
|
57
|
+
params = {'db_name': db_url, 'return_dict': True}
|
57
58
|
if config['pantherdb_encryption']:
|
58
59
|
try:
|
59
60
|
import cryptography
|
60
61
|
except ModuleNotFoundError as e:
|
61
62
|
import_error(e, package='cryptography')
|
62
|
-
|
63
|
+
else:
|
64
|
+
params['secret_key'] = config['secret_key']
|
65
|
+
self._session: PantherDB = PantherDB(**params)
|
63
66
|
|
64
67
|
def close(self) -> None:
|
65
68
|
if self._db_name == 'mongodb':
|
panther/main.py
CHANGED
@@ -5,6 +5,7 @@ import sys
|
|
5
5
|
import types
|
6
6
|
from collections.abc import Callable
|
7
7
|
from logging.config import dictConfig
|
8
|
+
from multiprocessing import Manager
|
8
9
|
from pathlib import Path
|
9
10
|
from threading import Thread
|
10
11
|
|
@@ -52,14 +53,6 @@ class Panther:
|
|
52
53
|
# Print Info
|
53
54
|
print_info(config)
|
54
55
|
|
55
|
-
# Start Websocket Listener (Redis Required)
|
56
|
-
if config['has_ws']:
|
57
|
-
Thread(
|
58
|
-
target=config['websocket_connections'],
|
59
|
-
daemon=True,
|
60
|
-
args=(self.ws_redis_connection,),
|
61
|
-
).start()
|
62
|
-
|
63
56
|
def load_configs(self) -> None:
|
64
57
|
|
65
58
|
# Check & Read The Configs File
|
@@ -98,8 +91,7 @@ class Panther:
|
|
98
91
|
self._create_ws_connections_instance()
|
99
92
|
|
100
93
|
def _create_ws_connections_instance(self):
|
101
|
-
from panther.base_websocket import Websocket
|
102
|
-
from panther.websocket import WebsocketConnections
|
94
|
+
from panther.base_websocket import Websocket, WebsocketConnections
|
103
95
|
|
104
96
|
# Check do we have ws endpoint
|
105
97
|
for endpoint in config['flat_urls'].values():
|
@@ -111,7 +103,6 @@ class Panther:
|
|
111
103
|
|
112
104
|
# Create websocket connections instance
|
113
105
|
if config['has_ws']:
|
114
|
-
config['websocket_connections'] = WebsocketConnections()
|
115
106
|
# Websocket Redis Connection
|
116
107
|
for middleware in config['http_middlewares']:
|
117
108
|
if middleware.__class__.__name__ == 'RedisMiddleware':
|
@@ -120,6 +111,10 @@ class Panther:
|
|
120
111
|
else:
|
121
112
|
self.ws_redis_connection = None
|
122
113
|
|
114
|
+
# Don't create Manager() if we are going to use Redis for PubSub
|
115
|
+
manager = None if self.ws_redis_connection else Manager()
|
116
|
+
config['websocket_connections'] = WebsocketConnections(manager=manager)
|
117
|
+
|
123
118
|
async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
|
124
119
|
"""
|
125
120
|
1.
|
@@ -138,6 +133,7 @@ class Panther:
|
|
138
133
|
if scope['type'] == 'lifespan':
|
139
134
|
message = await receive()
|
140
135
|
if message["type"] == "lifespan.startup":
|
136
|
+
await self.handle_ws_listener()
|
141
137
|
await self.handle_startup()
|
142
138
|
return
|
143
139
|
|
@@ -262,6 +258,15 @@ class Panther:
|
|
262
258
|
body=response.body,
|
263
259
|
)
|
264
260
|
|
261
|
+
async def handle_ws_listener(self):
|
262
|
+
# Start Websocket Listener (Redis/ Queue)
|
263
|
+
if config['has_ws']:
|
264
|
+
Thread(
|
265
|
+
target=config['websocket_connections'],
|
266
|
+
daemon=True,
|
267
|
+
args=(self.ws_redis_connection,),
|
268
|
+
).start()
|
269
|
+
|
265
270
|
async def handle_startup(self):
|
266
271
|
if startup := config['startup'] or self._startup:
|
267
272
|
if is_function_async(startup):
|
@@ -272,7 +277,13 @@ class Panther:
|
|
272
277
|
def handle_shutdown(self):
|
273
278
|
if shutdown := config['shutdown'] or self._shutdown:
|
274
279
|
if is_function_async(shutdown):
|
275
|
-
|
280
|
+
try:
|
281
|
+
asyncio.run(shutdown())
|
282
|
+
except ModuleNotFoundError:
|
283
|
+
# Error: import of asyncio halted; None in sys.modules
|
284
|
+
# And as I figured it out, it only happens when we running with
|
285
|
+
# gunicorn and Uvicorn workers (-k uvicorn.workers.UvicornWorker)
|
286
|
+
pass
|
276
287
|
else:
|
277
288
|
shutdown()
|
278
289
|
|
panther/response.py
CHANGED
@@ -35,9 +35,10 @@ class Response:
|
|
35
35
|
|
36
36
|
@property
|
37
37
|
def headers(self) -> dict:
|
38
|
+
content_length = 0 if self.body == b'null' else len(self.body)
|
38
39
|
return {
|
39
40
|
'content-type': self.content_type,
|
40
|
-
'content-length':
|
41
|
+
'content-length': content_length,
|
41
42
|
'access-control-allow-origin': '*',
|
42
43
|
} | (self._headers or {})
|
43
44
|
|
panther/serializer.py
CHANGED
@@ -3,31 +3,44 @@ from pydantic_core._pydantic_core import PydanticUndefined
|
|
3
3
|
|
4
4
|
|
5
5
|
class ModelSerializer:
|
6
|
-
def __new__(cls, *args, **kwargs):
|
6
|
+
def __new__(cls, *args, model=None, **kwargs):
|
7
|
+
# Check `metaclass`
|
7
8
|
if len(args) == 0:
|
8
|
-
|
9
|
+
address = f'{cls.__module__}.{cls.__name__}'
|
10
|
+
msg = f"you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> {address}"
|
9
11
|
raise TypeError(msg)
|
12
|
+
|
10
13
|
model_name = args[0]
|
11
|
-
|
12
|
-
|
14
|
+
data = args[2]
|
15
|
+
address = f'{data["__module__"]}.{model_name}'
|
16
|
+
|
17
|
+
# Check `model`
|
18
|
+
if model is None:
|
19
|
+
msg = f"'model' required while using 'ModelSerializer' metaclass -> {address}"
|
13
20
|
raise AttributeError(msg)
|
21
|
+
# Check `fields`
|
22
|
+
if 'fields' not in data:
|
23
|
+
msg = f"'fields' required while using 'ModelSerializer' metaclass. -> {address}"
|
24
|
+
raise AttributeError(msg) from None
|
14
25
|
|
15
|
-
model_fields =
|
26
|
+
model_fields = model.model_fields
|
16
27
|
field_definitions = {}
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
for field_name in args[2]['fields']:
|
28
|
+
|
29
|
+
# Collect `fields`
|
30
|
+
for field_name in data['fields']:
|
21
31
|
if field_name not in model_fields:
|
22
|
-
msg = f"'{field_name}' is not in '{
|
32
|
+
msg = f"'{field_name}' is not in '{model.__name__}' -> {address}"
|
23
33
|
raise AttributeError(msg) from None
|
24
|
-
|
25
34
|
field_definitions[field_name] = (model_fields[field_name].annotation, model_fields[field_name])
|
26
|
-
|
35
|
+
|
36
|
+
# Change `required_fields
|
37
|
+
for required in data.get('required_fields', []):
|
27
38
|
if required not in field_definitions:
|
28
|
-
msg = f"'{required}' is in 'required_fields' but not in 'fields' -> {
|
39
|
+
msg = f"'{required}' is in 'required_fields' but not in 'fields' -> {address}"
|
29
40
|
raise AttributeError(msg) from None
|
30
41
|
field_definitions[required][1].default = PydanticUndefined
|
42
|
+
|
43
|
+
# Create Model
|
31
44
|
return create_model(
|
32
45
|
__model_name=model_name,
|
33
46
|
**field_definitions
|
panther/websocket.py
CHANGED
@@ -1,13 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from typing import Literal
|
4
|
-
|
5
|
-
import orjson as json
|
6
|
-
|
7
3
|
from panther import status
|
8
|
-
from panther.base_websocket import Websocket
|
4
|
+
from panther.base_websocket import Websocket
|
9
5
|
from panther.configs import config
|
10
|
-
from panther.db.connection import redis
|
11
6
|
|
12
7
|
|
13
8
|
class GenericWebsocket(Websocket):
|
@@ -31,31 +26,9 @@ class GenericWebsocket(Websocket):
|
|
31
26
|
|
32
27
|
|
33
28
|
async def send_message_to_websocket(connection_id: str, data: any):
|
34
|
-
|
35
|
-
_publish_to_ws_channel(connection_id=connection_id, action='send', data=data)
|
36
|
-
else:
|
37
|
-
websocket_connections: WebsocketConnections = config['websocket_connections']
|
38
|
-
if connection := websocket_connections.connections.get(connection_id):
|
39
|
-
await connection.send(data=data)
|
29
|
+
config.websocket_connections.publish(connection_id=connection_id, action='send', data=data)
|
40
30
|
|
41
31
|
|
42
32
|
async def close_websocket_connection(connection_id: str, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ''):
|
43
|
-
|
44
|
-
|
45
|
-
'code': code,
|
46
|
-
'reason': reason,
|
47
|
-
}
|
48
|
-
_publish_to_ws_channel(connection_id=connection_id, action='close', data=data)
|
49
|
-
else:
|
50
|
-
websocket_connections: WebsocketConnections = config['websocket_connections']
|
51
|
-
if connection := websocket_connections.connections.get(connection_id):
|
52
|
-
await connection.close(code=code, reason=reason)
|
53
|
-
|
54
|
-
|
55
|
-
def _publish_to_ws_channel(connection_id: str, action: Literal['send', 'close'], data: any):
|
56
|
-
from panther.db.connection import redis
|
57
|
-
|
58
|
-
assert redis.is_connected, 'Redis Is Not Connected.'
|
59
|
-
|
60
|
-
p_data = json.dumps({'connection_id': connection_id, 'action': action, 'data': data})
|
61
|
-
redis.publish('websocket_connections', p_data)
|
33
|
+
data = {'code': code, 'reason': reason}
|
34
|
+
config.websocket_connections.publish(connection_id=connection_id, action='close', data=data)
|
@@ -1,37 +1,37 @@
|
|
1
|
-
panther/__init__.py,sha256=
|
1
|
+
panther/__init__.py,sha256=sxGEJd2i5R8BqUGWsC5YBeZeY0C0YICZHJM27Wu09bc,110
|
2
2
|
panther/_load_configs.py,sha256=jlDO5rK040z3LLmMFMJ6jUL9oTKaDKCWebwmdYJPDjw,9567
|
3
3
|
panther/_utils.py,sha256=BHAOdpESn3dt90FtpPKxEZdnkXnDC7MFcncJYburEaQ,4846
|
4
4
|
panther/app.py,sha256=3Qf0b-OtczqU5xs7AT7_hFr3t1eFMqzaThQOFu-RoqE,8060
|
5
5
|
panther/authentications.py,sha256=II3397DOtB2A1zvPbnCibQXekO_dKeclQ8dgU0s_WAc,4397
|
6
6
|
panther/background_tasks.py,sha256=J6JrjiEzyGohYsaFZHnZtXeP52piP84RlGFLsKOFa-Y,7225
|
7
7
|
panther/base_request.py,sha256=94mmloCULYtmiRWz-egb5annYp1KQAKFYIAcmIJZZ3o,2897
|
8
|
-
panther/base_websocket.py,sha256=
|
8
|
+
panther/base_websocket.py,sha256=p8nNr3EolttHmMPtIGgFB3evM2N5XZiX9TwXnZbyMqI,7608
|
9
9
|
panther/caching.py,sha256=YTFP40aLFR0VabbVYAlHilaALWlCJOrlTNDhG0cRC7c,3075
|
10
10
|
panther/configs.py,sha256=8pVMWa2QKaeNOlIUCLX3qfNXl0vNdraiGL4OGykFPXI,2884
|
11
11
|
panther/exceptions.py,sha256=DB6nGfU5LxrglEN_I-HInMqdIA3ZmN8rRv0ynEuQyGA,1332
|
12
12
|
panther/file_handler.py,sha256=YhqYpGhNybcQBqcxqDEYwI1Hqd-7U5JcwxMb7UM_DbA,854
|
13
13
|
panther/logging.py,sha256=DZVf3nxzLodT-hD4820J1jEAffU8zIxXRPKs2lbP8ho,2074
|
14
|
-
panther/main.py,sha256=
|
14
|
+
panther/main.py,sha256=0q-ViNYzrR6EgNLZiGqWOlOhpRiJEnunmiqJVfHq6o8,12246
|
15
15
|
panther/monitoring.py,sha256=krmcoTUcV12pHwCFVAHywgsp1-US9cyiMlgsrMJdUQ0,1203
|
16
16
|
panther/permissions.py,sha256=Q-l25369yQaP-tY11tFQm-v9PPh8iVImbfUpY3pnQUk,355
|
17
17
|
panther/request.py,sha256=u_-pyttspRimlSwpSwyVIV0U87hHDIthsFU9Qr8LDvI,1762
|
18
|
-
panther/response.py,sha256=
|
18
|
+
panther/response.py,sha256=5t9-HwgwRo3egqzs1uFNhIq5i5wZW-TRrVX8YEdFrgA,3724
|
19
19
|
panther/routings.py,sha256=GfwBZL7bt3yVOnpdjOZ-49kiGVrRoF0i8VwS1qkF_oI,5273
|
20
|
-
panther/serializer.py,sha256=
|
20
|
+
panther/serializer.py,sha256=ZvLWwHV2KQdl49ZFRshz4HWXVmFNQtUClggSWkH9pa8,1839
|
21
21
|
panther/status.py,sha256=Gc_PnYrHfInTsZpGbqiCfDB-py1C7Rh8KMdb6Lq9Exs,3346
|
22
22
|
panther/test.py,sha256=Se0cF51JRAmABpB_QTIJeTDRKDcyOMlibEN6rrr4Sd8,6152
|
23
23
|
panther/throttling.py,sha256=mVa_mGv6w_Ad7LLtV4eG5QpDwwNsk4QjFFi0mIHQBnE,231
|
24
24
|
panther/utils.py,sha256=fWzUrVnXc-98iOC6RIOD4E1KL2rkd6EtbSqqg5ZTG_U,3142
|
25
|
-
panther/websocket.py,sha256=
|
25
|
+
panther/websocket.py,sha256=HEyp3lOTKwitPPX76IfwYdp_gbjjmmrHMizBlLHRmNA,1158
|
26
26
|
panther/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
27
27
|
panther/cli/create_command.py,sha256=AshVxzhSpX-L_63OLP7ZgQ5a45NINGomTbvJkCaadks,9671
|
28
28
|
panther/cli/main.py,sha256=pCqnOTazgMhTvFHTugutIsiFXueU5kx2VmGngwAl54Q,1679
|
29
29
|
panther/cli/monitor_command.py,sha256=cZ7wOxeQ8jKXgRLP28SgFI9pbIEp4RK6VmVf74WKT6c,2447
|
30
30
|
panther/cli/run_command.py,sha256=X_T9jP_Z8X_fm9S4LSoR6ARsPp4rCTMi1E5c7QWREjM,2120
|
31
|
-
panther/cli/template.py,sha256=
|
31
|
+
panther/cli/template.py,sha256=i4YUgsdVjYhWGoCpoCNQOBo2rKSVk0yx-o_GBmG4yx8,4941
|
32
32
|
panther/cli/utils.py,sha256=nhtdr9nhYAbCm2S-17vTc3_unjQJ450uxT0ZVyZueIw,4838
|
33
33
|
panther/db/__init__.py,sha256=w9lEL0vRqb18Qx_iUJipUR_fi5GQ5uVX0DWycx14x08,50
|
34
|
-
panther/db/connection.py,sha256=
|
34
|
+
panther/db/connection.py,sha256=9lXzMinGNgQraeu4Sjy92AueR7_AGRsDb0EE0_K4VP4,3039
|
35
35
|
panther/db/models.py,sha256=7dJ_l0ejVOAVc6qietOYenaRVXBb_O5LNXoyBCLi55g,1434
|
36
36
|
panther/db/utils.py,sha256=AFVQz-vvfnAj3hcNq4IMo0T7EB8BR9snM-tGsK-Saw4,1667
|
37
37
|
panther/db/queries/__init__.py,sha256=FpSQGNHGMs5PJow8Qan4eBAld6QH6wfMvj7lC92vKcU,55
|
@@ -46,9 +46,9 @@ panther/panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
46
|
panther/panel/apis.py,sha256=HUf9Km81wWtO3pbgW0Oi5MDbLvX5LIWdiFRy05UsitE,1773
|
47
47
|
panther/panel/urls.py,sha256=BQkWqSJBPP3VEQYeorKSHIRx-PUl21Y7Z6NFylmhs1I,192
|
48
48
|
panther/panel/utils.py,sha256=0Rv79oR5IEqalqwpRKQHMn1p5duVY5mxMqDKiA5mWx4,437
|
49
|
-
panther-3.
|
50
|
-
panther-3.
|
51
|
-
panther-3.
|
52
|
-
panther-3.
|
53
|
-
panther-3.
|
54
|
-
panther-3.
|
49
|
+
panther-3.8.1.dist-info/LICENSE,sha256=2aF1hL2aC0zRPjzUkSxJUzZbn2_uLoOkn7DHjzZni-I,1524
|
50
|
+
panther-3.8.1.dist-info/METADATA,sha256=KMHrczETrp-LQzP2SxiPcHqaiKuLz2TkUT6dLv5TSU4,6023
|
51
|
+
panther-3.8.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
52
|
+
panther-3.8.1.dist-info/entry_points.txt,sha256=6GPxYFGuzVfNB4YpHFJvYex6iWah5_tLnirAHwj2Qsg,51
|
53
|
+
panther-3.8.1.dist-info/top_level.txt,sha256=VbBs02JGXTIoHMzsX-eLOk2MCbBZzQbLhWiYpI7xI2g,8
|
54
|
+
panther-3.8.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|