panther 3.7.0__tar.gz → 3.8.1__tar.gz
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-3.7.0 → panther-3.8.1}/PKG-INFO +1 -1
- {panther-3.7.0 → panther-3.8.1}/panther/__init__.py +1 -1
- {panther-3.7.0 → panther-3.8.1}/panther/base_websocket.py +71 -30
- {panther-3.7.0 → panther-3.8.1}/panther/cli/template.py +2 -3
- {panther-3.7.0 → panther-3.8.1}/panther/db/connection.py +4 -1
- {panther-3.7.0 → panther-3.8.1}/panther/main.py +23 -12
- {panther-3.7.0 → panther-3.8.1}/panther/response.py +2 -1
- {panther-3.7.0 → panther-3.8.1}/panther/serializer.py +26 -13
- panther-3.8.1/panther/websocket.py +34 -0
- {panther-3.7.0 → panther-3.8.1}/panther.egg-info/PKG-INFO +1 -1
- {panther-3.7.0 → panther-3.8.1}/tests/test_model_serializer.py +5 -8
- panther-3.7.0/panther/websocket.py +0 -61
- {panther-3.7.0 → panther-3.8.1}/LICENSE +0 -0
- {panther-3.7.0 → panther-3.8.1}/README.md +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/_load_configs.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/_utils.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/app.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/authentications.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/background_tasks.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/base_request.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/caching.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/cli/__init__.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/cli/create_command.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/cli/main.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/cli/monitor_command.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/cli/run_command.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/cli/utils.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/configs.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/__init__.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/models.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/queries/__init__.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/queries/mongodb_queries.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/queries/pantherdb_queries.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/queries/queries.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/db/utils.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/exceptions.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/file_handler.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/logging.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/middlewares/__init__.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/middlewares/base.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/middlewares/db.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/middlewares/redis.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/monitoring.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/panel/__init__.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/panel/apis.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/panel/urls.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/panel/utils.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/permissions.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/request.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/routings.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/status.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/test.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/throttling.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther/utils.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther.egg-info/SOURCES.txt +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther.egg-info/dependency_links.txt +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther.egg-info/entry_points.txt +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther.egg-info/requires.txt +0 -0
- {panther-3.7.0 → panther-3.8.1}/panther.egg-info/top_level.txt +0 -0
- {panther-3.7.0 → panther-3.8.1}/pyproject.toml +0 -0
- {panther-3.7.0 → panther-3.8.1}/setup.cfg +0 -0
- {panther-3.7.0 → panther-3.8.1}/setup.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_authentication.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_background_tasks.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_caching.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_cli.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_database.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_mongodb.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_multipart.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_panel_apis.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_request.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_routing.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_run.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_simple_responses.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_status.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_utils.py +0 -0
- {panther-3.7.0 → panther-3.8.1}/tests/test_websockets.py +0 -0
@@ -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})
|
@@ -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
|
@@ -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':
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from panther import status
|
4
|
+
from panther.base_websocket import Websocket
|
5
|
+
from panther.configs import config
|
6
|
+
|
7
|
+
|
8
|
+
class GenericWebsocket(Websocket):
|
9
|
+
async def connect(self, **kwargs):
|
10
|
+
"""
|
11
|
+
Check your conditions then `accept()` the connection
|
12
|
+
"""
|
13
|
+
|
14
|
+
async def receive(self, data: str | bytes):
|
15
|
+
"""
|
16
|
+
Received `data` of connection,
|
17
|
+
You may want to use json.loads() on the `data`
|
18
|
+
"""
|
19
|
+
|
20
|
+
async def send(self, data: any = None):
|
21
|
+
"""
|
22
|
+
We are using this method to send message to the client,
|
23
|
+
You may want to override it with your custom scenario. (not recommended)
|
24
|
+
"""
|
25
|
+
return await super().send(data=data)
|
26
|
+
|
27
|
+
|
28
|
+
async def send_message_to_websocket(connection_id: str, data: any):
|
29
|
+
config.websocket_connections.publish(connection_id=connection_id, action='send', data=data)
|
30
|
+
|
31
|
+
|
32
|
+
async def close_websocket_connection(connection_id: str, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ''):
|
33
|
+
data = {'code': code, 'reason': reason}
|
34
|
+
config.websocket_connections.publish(connection_id=connection_id, action='close', data=data)
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import asyncio
|
2
1
|
from pathlib import Path
|
3
2
|
from unittest import TestCase
|
4
3
|
|
@@ -6,7 +5,6 @@ from pydantic import Field
|
|
6
5
|
|
7
6
|
from panther import Panther
|
8
7
|
from panther.app import API
|
9
|
-
from panther.configs import config
|
10
8
|
from panther.db import Model
|
11
9
|
from panther.request import Request
|
12
10
|
from panther.serializer import ModelSerializer
|
@@ -122,7 +120,7 @@ class TestModelSerializer(TestCase):
|
|
122
120
|
pass
|
123
121
|
except Exception as e:
|
124
122
|
assert isinstance(e, AttributeError)
|
125
|
-
assert e.args[0] == "'fields' required while using 'ModelSerializer' metaclass. -> Serializer1"
|
123
|
+
assert e.args[0] == "'fields' required while using 'ModelSerializer' metaclass. -> tests.test_model_serializer.Serializer1"
|
126
124
|
else:
|
127
125
|
assert False
|
128
126
|
|
@@ -132,7 +130,7 @@ class TestModelSerializer(TestCase):
|
|
132
130
|
fields = ['ok', 'no']
|
133
131
|
except Exception as e:
|
134
132
|
assert isinstance(e, AttributeError)
|
135
|
-
assert e.args[0] == "'ok' is not in 'Book' -> Serializer2"
|
133
|
+
assert e.args[0] == "'ok' is not in 'Book' -> tests.test_model_serializer.Serializer2"
|
136
134
|
else:
|
137
135
|
assert False
|
138
136
|
|
@@ -143,7 +141,7 @@ class TestModelSerializer(TestCase):
|
|
143
141
|
required_fields = ['pages_count']
|
144
142
|
except Exception as e:
|
145
143
|
assert isinstance(e, AttributeError)
|
146
|
-
assert e.args[0] == "'pages_count' is in 'required_fields' but not in 'fields' -> Serializer3"
|
144
|
+
assert e.args[0] == "'pages_count' is in 'required_fields' but not in 'fields' -> tests.test_model_serializer.Serializer3"
|
147
145
|
else:
|
148
146
|
assert False
|
149
147
|
|
@@ -154,7 +152,7 @@ class TestModelSerializer(TestCase):
|
|
154
152
|
required_fields = ['pages_count']
|
155
153
|
except Exception as e:
|
156
154
|
assert isinstance(e, AttributeError)
|
157
|
-
assert e.args[0] == "'model' required while using 'ModelSerializer' metaclass -> Serializer4"
|
155
|
+
assert e.args[0] == "'model' required while using 'ModelSerializer' metaclass -> tests.test_model_serializer.Serializer4"
|
158
156
|
else:
|
159
157
|
assert False
|
160
158
|
|
@@ -167,7 +165,6 @@ class TestModelSerializer(TestCase):
|
|
167
165
|
Serializer5(name='alice', author='bob')
|
168
166
|
except Exception as e:
|
169
167
|
assert isinstance(e, TypeError)
|
170
|
-
assert e.args[0] ==
|
171
|
-
"you should use it as 'metaclass' -> Serializer5")
|
168
|
+
assert e.args[0] == "you should not inherit the 'ModelSerializer', you should use it as 'metaclass' -> tests.test_model_serializer.Serializer5"
|
172
169
|
else:
|
173
170
|
assert False
|
@@ -1,61 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import Literal
|
4
|
-
|
5
|
-
import orjson as json
|
6
|
-
|
7
|
-
from panther import status
|
8
|
-
from panther.base_websocket import Websocket, WebsocketConnections
|
9
|
-
from panther.configs import config
|
10
|
-
from panther.db.connection import redis
|
11
|
-
|
12
|
-
|
13
|
-
class GenericWebsocket(Websocket):
|
14
|
-
async def connect(self, **kwargs):
|
15
|
-
"""
|
16
|
-
Check your conditions then `accept()` the connection
|
17
|
-
"""
|
18
|
-
|
19
|
-
async def receive(self, data: str | bytes):
|
20
|
-
"""
|
21
|
-
Received `data` of connection,
|
22
|
-
You may want to use json.loads() on the `data`
|
23
|
-
"""
|
24
|
-
|
25
|
-
async def send(self, data: any = None):
|
26
|
-
"""
|
27
|
-
We are using this method to send message to the client,
|
28
|
-
You may want to override it with your custom scenario. (not recommended)
|
29
|
-
"""
|
30
|
-
return await super().send(data=data)
|
31
|
-
|
32
|
-
|
33
|
-
async def send_message_to_websocket(connection_id: str, data: any):
|
34
|
-
if redis.is_connected:
|
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)
|
40
|
-
|
41
|
-
|
42
|
-
async def close_websocket_connection(connection_id: str, code: int = status.WS_1000_NORMAL_CLOSURE, reason: str = ''):
|
43
|
-
if redis.is_connected:
|
44
|
-
data = {
|
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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|