django-sockets 1.0.0__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.
- django_sockets-1.0.0/LICENSE +21 -0
- django_sockets-1.0.0/PKG-INFO +191 -0
- django_sockets-1.0.0/README.md +172 -0
- django_sockets-1.0.0/django_sockets/__init__.py +0 -0
- django_sockets-1.0.0/django_sockets/broadcaster.py +99 -0
- django_sockets-1.0.0/django_sockets/middleware.py +56 -0
- django_sockets-1.0.0/django_sockets/pubsub.py +315 -0
- django_sockets-1.0.0/django_sockets/sockets.py +206 -0
- django_sockets-1.0.0/django_sockets/utils.py +215 -0
- django_sockets-1.0.0/django_sockets.egg-info/PKG-INFO +191 -0
- django_sockets-1.0.0/django_sockets.egg-info/SOURCES.txt +15 -0
- django_sockets-1.0.0/django_sockets.egg-info/dependency_links.txt +1 -0
- django_sockets-1.0.0/django_sockets.egg-info/requires.txt +4 -0
- django_sockets-1.0.0/django_sockets.egg-info/top_level.txt +1 -0
- django_sockets-1.0.0/pyproject.toml +32 -0
- django_sockets-1.0.0/setup.cfg +12 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Connor Makowski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: django_sockets
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Simplified Django websocket processes designed to work with cloud caches
|
|
5
|
+
Author-email: Connor Makowski <conmak@mit.edu>
|
|
6
|
+
Project-URL: Homepage, https://github.com/connor-makowski/django_sockets
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/connor-makowski/django_sockets/issues
|
|
8
|
+
Project-URL: Documentation, https://connor-makowski.github.io/django_sockets/django_sockets.html
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: django>=5.0.0
|
|
16
|
+
Requires-Dist: asgiref>=3.0.0
|
|
17
|
+
Requires-Dist: redis>=5.0.0
|
|
18
|
+
Requires-Dist: msgpack>=1.0.0
|
|
19
|
+
|
|
20
|
+
# Django Sockets
|
|
21
|
+
[](https://badge.fury.io/py/django_sockets)
|
|
22
|
+
[](https://opensource.org/licenses/MIT)
|
|
23
|
+
|
|
24
|
+
Simplified Django websocket processes designed to work with cloud caches (valkey|redis on single|distributed|serverless)
|
|
25
|
+
|
|
26
|
+
# Setup
|
|
27
|
+
|
|
28
|
+
### General
|
|
29
|
+
|
|
30
|
+
Make sure you have Python 3.10.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
pip install django_sockets
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Other Requirements
|
|
39
|
+
|
|
40
|
+
- Make sure a cache (valkey or redis) is setup and accessible from your server.
|
|
41
|
+
|
|
42
|
+
- To run one locally (using docker) for testing, you can use the following command:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
docker run -d -p 6379:6379 --name django_sockets_cache valkey/valkey:7
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- To run the container after it has been created, you can use the following command:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
docker start django_sockets_cache
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- To kill the container later, you can use the following command:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
docker kill django_sockets_cache
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
# Usage
|
|
61
|
+
|
|
62
|
+
Low level docs: https://connor-makowski.github.io/django_sockets/django_sockets.html
|
|
63
|
+
|
|
64
|
+
## Implement in a Django Project
|
|
65
|
+
|
|
66
|
+
### TODO
|
|
67
|
+
|
|
68
|
+
## Examples Without Django
|
|
69
|
+
|
|
70
|
+
### Example Subscribing & Broadcasting
|
|
71
|
+
```py
|
|
72
|
+
from django_sockets.sockets import BaseSocketServer
|
|
73
|
+
import asyncio, time
|
|
74
|
+
|
|
75
|
+
DJANGO_SOCKETS_CONFIG = {
|
|
76
|
+
"hosts": [
|
|
77
|
+
{"address": f"redis://0.0.0.0:6379"}
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Override the send method to print the data being sent
|
|
82
|
+
# instead of sending it over a non existent websocket connection
|
|
83
|
+
async def send(ws_data):
|
|
84
|
+
print("WS SENDING:", ws_data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Create a receive queue to simulate receiving messages from a websocket client
|
|
88
|
+
base_receive = asyncio.Queue()
|
|
89
|
+
# Create a base socket server with a scope of {}
|
|
90
|
+
base_socket_server = BaseSocketServer(scope={}, receive=base_receive.get, send=send, config=DJANGO_SOCKETS_CONFIG)
|
|
91
|
+
# Start the listeners for the base socket server
|
|
92
|
+
base_socket_server.start_listeners()
|
|
93
|
+
# Subscribe to the test_channel
|
|
94
|
+
base_socket_server.subscribe("test_channel")
|
|
95
|
+
# Broadcast a message to the test_channel
|
|
96
|
+
base_socket_server.broadcast("test_channel", "test message")
|
|
97
|
+
# Give the async functions a small amount of time to complete
|
|
98
|
+
time.sleep(.5)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#=> Output:
|
|
102
|
+
#=> WS SENDING: {'type': 'websocket.send', 'text': '"test message"'}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Example Handle Websocket Messages
|
|
106
|
+
```py
|
|
107
|
+
from django_sockets.sockets import BaseSocketServer
|
|
108
|
+
import asyncio, time
|
|
109
|
+
|
|
110
|
+
DJANGO_SOCKETS_CONFIG = {
|
|
111
|
+
"hosts": [
|
|
112
|
+
{"address": f"redis://0.0.0.0:6379"}
|
|
113
|
+
],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
class CustomSocketServer(BaseSocketServer):
|
|
117
|
+
def receive(self, data):
|
|
118
|
+
"""
|
|
119
|
+
When a data message is received from a websocket client:
|
|
120
|
+
- Print the data
|
|
121
|
+
- Broadcast the data to a channel (the same channel that the socket server is subscribed to)
|
|
122
|
+
|
|
123
|
+
Normally you would want to override the receive method to do any server side processing of the data that is received
|
|
124
|
+
then broadcast any changes back to relevant channels.
|
|
125
|
+
"""
|
|
126
|
+
print("WS RECEIVED: ", data)
|
|
127
|
+
print(f"BROADCASTING TO '{self.scope['username']}'")
|
|
128
|
+
self.broadcast(self.scope['username'], data)
|
|
129
|
+
|
|
130
|
+
def connect(self):
|
|
131
|
+
"""
|
|
132
|
+
When the websocket connects, subscribe to the channel of the user.
|
|
133
|
+
|
|
134
|
+
This is an important method to override if you want to subscribe to a channel when a user frist connects.
|
|
135
|
+
|
|
136
|
+
Otherwise, you can always subscribe to a channel based on the data that is received in the receive method.
|
|
137
|
+
"""
|
|
138
|
+
print(f"CONNECTED")
|
|
139
|
+
print(f"SUSCRIBING TO '{self.scope['username']}'")
|
|
140
|
+
self.subscribe(self.scope['username'])
|
|
141
|
+
|
|
142
|
+
# Override the send method to print the data being sent
|
|
143
|
+
async def send(data):
|
|
144
|
+
"""
|
|
145
|
+
Normally you would not override the send method, but since we are not actually sending data over a websocket connection
|
|
146
|
+
we are just going to print the data that would be sent.
|
|
147
|
+
|
|
148
|
+
This is useful for testing the socket server without having to actually send data over a websocket connection
|
|
149
|
+
|
|
150
|
+
Note: This only sends the first 64 characters of the data
|
|
151
|
+
"""
|
|
152
|
+
print("WS SENDING:", str(data)[:64])
|
|
153
|
+
|
|
154
|
+
# Create a receive queue to simulate receiving messages from a websocket client
|
|
155
|
+
custom_receive = asyncio.Queue()
|
|
156
|
+
# Create a custom socket server defined above with a scope of {'username':'adam'}, the custom_receive queue, and the send method defined above
|
|
157
|
+
custom_socket_server = CustomSocketServer(scope={'username':'adam'}, receive=custom_receive.get, send=send)
|
|
158
|
+
# Start the listeners for the custom socket server
|
|
159
|
+
# - Websocket Listener - Listens for websocket messages
|
|
160
|
+
# - Broadcast Listener - Listens for messages that were broadcasted to a channel that the socket server is subscribed to
|
|
161
|
+
custom_socket_server.start_listeners()
|
|
162
|
+
# Give the async functions a small amount of time to complete
|
|
163
|
+
time.sleep(.1)
|
|
164
|
+
# Simulate a WS connection request
|
|
165
|
+
custom_receive.put_nowait({'type': 'websocket.connect'})
|
|
166
|
+
# Give the async functions a small amount of time to complete
|
|
167
|
+
time.sleep(.1)
|
|
168
|
+
# Simulate a message being received from a WS client
|
|
169
|
+
# This will call the receive method which is defined above
|
|
170
|
+
custom_receive.put_nowait({'type': 'websocket.receive', 'text': '{"data": "test"}'})
|
|
171
|
+
# Give the async functions a small amount of time to complete
|
|
172
|
+
time.sleep(.1)
|
|
173
|
+
# Simulate a WS disconnect request
|
|
174
|
+
custom_receive.put_nowait({'type': 'websocket.disconnect'})
|
|
175
|
+
# Give the async functions a small amount of time to complete
|
|
176
|
+
time.sleep(.1)
|
|
177
|
+
# Simulate a message being received from a WS client after the connection has been closed
|
|
178
|
+
# This will not do anything since the connection has been closed and the listeners have been killed
|
|
179
|
+
custom_receive.put_nowait({'type': 'websocket.receive', 'text': '{"data_after_close": "test"}'})
|
|
180
|
+
# Give the async functions a small amount of time to complete
|
|
181
|
+
time.sleep(.1)
|
|
182
|
+
|
|
183
|
+
#=> Output:
|
|
184
|
+
#=> WS SENDING: {'type': 'websocket.accept'}
|
|
185
|
+
#=> CONNECTED
|
|
186
|
+
#=> SUSCRIBING TO 'adam'
|
|
187
|
+
#=> WS RECEIVED: {'data': 'test'}
|
|
188
|
+
#=> BROADCASTING TO 'adam'
|
|
189
|
+
#=> WS SENDING: {'type': 'websocket.send', 'text': '{"data": "test"}'}
|
|
190
|
+
|
|
191
|
+
```
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Django Sockets
|
|
2
|
+
[](https://badge.fury.io/py/django_sockets)
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
Simplified Django websocket processes designed to work with cloud caches (valkey|redis on single|distributed|serverless)
|
|
6
|
+
|
|
7
|
+
# Setup
|
|
8
|
+
|
|
9
|
+
### General
|
|
10
|
+
|
|
11
|
+
Make sure you have Python 3.10.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
|
|
12
|
+
|
|
13
|
+
### Installation
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pip install django_sockets
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Other Requirements
|
|
20
|
+
|
|
21
|
+
- Make sure a cache (valkey or redis) is setup and accessible from your server.
|
|
22
|
+
|
|
23
|
+
- To run one locally (using docker) for testing, you can use the following command:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker run -d -p 6379:6379 --name django_sockets_cache valkey/valkey:7
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- To run the container after it has been created, you can use the following command:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker start django_sockets_cache
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- To kill the container later, you can use the following command:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
docker kill django_sockets_cache
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
# Usage
|
|
42
|
+
|
|
43
|
+
Low level docs: https://connor-makowski.github.io/django_sockets/django_sockets.html
|
|
44
|
+
|
|
45
|
+
## Implement in a Django Project
|
|
46
|
+
|
|
47
|
+
### TODO
|
|
48
|
+
|
|
49
|
+
## Examples Without Django
|
|
50
|
+
|
|
51
|
+
### Example Subscribing & Broadcasting
|
|
52
|
+
```py
|
|
53
|
+
from django_sockets.sockets import BaseSocketServer
|
|
54
|
+
import asyncio, time
|
|
55
|
+
|
|
56
|
+
DJANGO_SOCKETS_CONFIG = {
|
|
57
|
+
"hosts": [
|
|
58
|
+
{"address": f"redis://0.0.0.0:6379"}
|
|
59
|
+
],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Override the send method to print the data being sent
|
|
63
|
+
# instead of sending it over a non existent websocket connection
|
|
64
|
+
async def send(ws_data):
|
|
65
|
+
print("WS SENDING:", ws_data)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Create a receive queue to simulate receiving messages from a websocket client
|
|
69
|
+
base_receive = asyncio.Queue()
|
|
70
|
+
# Create a base socket server with a scope of {}
|
|
71
|
+
base_socket_server = BaseSocketServer(scope={}, receive=base_receive.get, send=send, config=DJANGO_SOCKETS_CONFIG)
|
|
72
|
+
# Start the listeners for the base socket server
|
|
73
|
+
base_socket_server.start_listeners()
|
|
74
|
+
# Subscribe to the test_channel
|
|
75
|
+
base_socket_server.subscribe("test_channel")
|
|
76
|
+
# Broadcast a message to the test_channel
|
|
77
|
+
base_socket_server.broadcast("test_channel", "test message")
|
|
78
|
+
# Give the async functions a small amount of time to complete
|
|
79
|
+
time.sleep(.5)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
#=> Output:
|
|
83
|
+
#=> WS SENDING: {'type': 'websocket.send', 'text': '"test message"'}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Example Handle Websocket Messages
|
|
87
|
+
```py
|
|
88
|
+
from django_sockets.sockets import BaseSocketServer
|
|
89
|
+
import asyncio, time
|
|
90
|
+
|
|
91
|
+
DJANGO_SOCKETS_CONFIG = {
|
|
92
|
+
"hosts": [
|
|
93
|
+
{"address": f"redis://0.0.0.0:6379"}
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
class CustomSocketServer(BaseSocketServer):
|
|
98
|
+
def receive(self, data):
|
|
99
|
+
"""
|
|
100
|
+
When a data message is received from a websocket client:
|
|
101
|
+
- Print the data
|
|
102
|
+
- Broadcast the data to a channel (the same channel that the socket server is subscribed to)
|
|
103
|
+
|
|
104
|
+
Normally you would want to override the receive method to do any server side processing of the data that is received
|
|
105
|
+
then broadcast any changes back to relevant channels.
|
|
106
|
+
"""
|
|
107
|
+
print("WS RECEIVED: ", data)
|
|
108
|
+
print(f"BROADCASTING TO '{self.scope['username']}'")
|
|
109
|
+
self.broadcast(self.scope['username'], data)
|
|
110
|
+
|
|
111
|
+
def connect(self):
|
|
112
|
+
"""
|
|
113
|
+
When the websocket connects, subscribe to the channel of the user.
|
|
114
|
+
|
|
115
|
+
This is an important method to override if you want to subscribe to a channel when a user frist connects.
|
|
116
|
+
|
|
117
|
+
Otherwise, you can always subscribe to a channel based on the data that is received in the receive method.
|
|
118
|
+
"""
|
|
119
|
+
print(f"CONNECTED")
|
|
120
|
+
print(f"SUSCRIBING TO '{self.scope['username']}'")
|
|
121
|
+
self.subscribe(self.scope['username'])
|
|
122
|
+
|
|
123
|
+
# Override the send method to print the data being sent
|
|
124
|
+
async def send(data):
|
|
125
|
+
"""
|
|
126
|
+
Normally you would not override the send method, but since we are not actually sending data over a websocket connection
|
|
127
|
+
we are just going to print the data that would be sent.
|
|
128
|
+
|
|
129
|
+
This is useful for testing the socket server without having to actually send data over a websocket connection
|
|
130
|
+
|
|
131
|
+
Note: This only sends the first 64 characters of the data
|
|
132
|
+
"""
|
|
133
|
+
print("WS SENDING:", str(data)[:64])
|
|
134
|
+
|
|
135
|
+
# Create a receive queue to simulate receiving messages from a websocket client
|
|
136
|
+
custom_receive = asyncio.Queue()
|
|
137
|
+
# Create a custom socket server defined above with a scope of {'username':'adam'}, the custom_receive queue, and the send method defined above
|
|
138
|
+
custom_socket_server = CustomSocketServer(scope={'username':'adam'}, receive=custom_receive.get, send=send)
|
|
139
|
+
# Start the listeners for the custom socket server
|
|
140
|
+
# - Websocket Listener - Listens for websocket messages
|
|
141
|
+
# - Broadcast Listener - Listens for messages that were broadcasted to a channel that the socket server is subscribed to
|
|
142
|
+
custom_socket_server.start_listeners()
|
|
143
|
+
# Give the async functions a small amount of time to complete
|
|
144
|
+
time.sleep(.1)
|
|
145
|
+
# Simulate a WS connection request
|
|
146
|
+
custom_receive.put_nowait({'type': 'websocket.connect'})
|
|
147
|
+
# Give the async functions a small amount of time to complete
|
|
148
|
+
time.sleep(.1)
|
|
149
|
+
# Simulate a message being received from a WS client
|
|
150
|
+
# This will call the receive method which is defined above
|
|
151
|
+
custom_receive.put_nowait({'type': 'websocket.receive', 'text': '{"data": "test"}'})
|
|
152
|
+
# Give the async functions a small amount of time to complete
|
|
153
|
+
time.sleep(.1)
|
|
154
|
+
# Simulate a WS disconnect request
|
|
155
|
+
custom_receive.put_nowait({'type': 'websocket.disconnect'})
|
|
156
|
+
# Give the async functions a small amount of time to complete
|
|
157
|
+
time.sleep(.1)
|
|
158
|
+
# Simulate a message being received from a WS client after the connection has been closed
|
|
159
|
+
# This will not do anything since the connection has been closed and the listeners have been killed
|
|
160
|
+
custom_receive.put_nowait({'type': 'websocket.receive', 'text': '{"data_after_close": "test"}'})
|
|
161
|
+
# Give the async functions a small amount of time to complete
|
|
162
|
+
time.sleep(.1)
|
|
163
|
+
|
|
164
|
+
#=> Output:
|
|
165
|
+
#=> WS SENDING: {'type': 'websocket.accept'}
|
|
166
|
+
#=> CONNECTED
|
|
167
|
+
#=> SUSCRIBING TO 'adam'
|
|
168
|
+
#=> WS RECEIVED: {'data': 'test'}
|
|
169
|
+
#=> BROADCASTING TO 'adam'
|
|
170
|
+
#=> WS SENDING: {'type': 'websocket.send', 'text': '{"data": "test"}'}
|
|
171
|
+
|
|
172
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import asyncio, logging
|
|
2
|
+
|
|
3
|
+
from .pubsub import PubSubLayer
|
|
4
|
+
from .utils import ensure_loop_running, get_config
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Broadcaster:
|
|
10
|
+
def __init__(self, *args, loop=None, config=None, **kwargs):
|
|
11
|
+
self.__loop__ = ensure_loop_running(loop)
|
|
12
|
+
self.pubsub_layer = self.__get_pubsub_layer__(config=config)
|
|
13
|
+
self.__usable__ = self.pubsub_layer is not None
|
|
14
|
+
|
|
15
|
+
# Sync Functions
|
|
16
|
+
def broadcast(self, channel: str, data: [dict | list]):
|
|
17
|
+
"""
|
|
18
|
+
Broadcast data to a specific channel or all channels that this socket server is subscribed to.
|
|
19
|
+
|
|
20
|
+
Requires:
|
|
21
|
+
|
|
22
|
+
- channel: str = The channel to broadcast the data to
|
|
23
|
+
- data: [dict|list] = The data to broadcast to the channel
|
|
24
|
+
- Note: This data must be JSON serializable
|
|
25
|
+
"""
|
|
26
|
+
asyncio.run_coroutine_threadsafe(
|
|
27
|
+
self.async_broadcast(channel, data), self.__loop__
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def subscribe(self, channel: str):
|
|
31
|
+
"""
|
|
32
|
+
Subscribe to a channel to receive data from broadcasts
|
|
33
|
+
|
|
34
|
+
Requires:
|
|
35
|
+
|
|
36
|
+
- channel: str = The channel to subscribe to
|
|
37
|
+
"""
|
|
38
|
+
asyncio.run_coroutine_threadsafe(
|
|
39
|
+
self.async_subscribe(channel), self.__loop__
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Async Functions
|
|
43
|
+
async def async_broadcast(self, channel: str, data):
|
|
44
|
+
"""
|
|
45
|
+
Broadcast data to a channel where all relevant clients will receive the data
|
|
46
|
+
and send it to the client
|
|
47
|
+
|
|
48
|
+
Requires:
|
|
49
|
+
|
|
50
|
+
- channel: str = The channel to broadcast the data to
|
|
51
|
+
- data: [dict|list] = The data to broadcast to the channel
|
|
52
|
+
- Note: This data must be JSON serializable
|
|
53
|
+
"""
|
|
54
|
+
self.__warn_on_not_usable__()
|
|
55
|
+
await self.pubsub_layer.send(str(channel), data)
|
|
56
|
+
|
|
57
|
+
async def async_subscribe(self, channel: str):
|
|
58
|
+
"""
|
|
59
|
+
Subscribe to a channel to receive data from broadcasts
|
|
60
|
+
|
|
61
|
+
Requires:
|
|
62
|
+
|
|
63
|
+
- channel: str = The channel to subscribe to
|
|
64
|
+
"""
|
|
65
|
+
self.__warn_on_not_usable__()
|
|
66
|
+
await self.pubsub_layer.subscribe(str(channel))
|
|
67
|
+
|
|
68
|
+
async def async_receive_broadcast(self):
|
|
69
|
+
"""
|
|
70
|
+
Receive a broadcast from a channel that this socket server is subscribed to
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
|
|
74
|
+
- channel: str = The channel that the data was broadcasted to
|
|
75
|
+
- data: [dict|list] = The data that was broadcasted to the channel
|
|
76
|
+
- Note: This data must be JSON serializable
|
|
77
|
+
"""
|
|
78
|
+
data = await self.pubsub_layer.receive()
|
|
79
|
+
return data["channel"], data["data"]
|
|
80
|
+
|
|
81
|
+
# Internal Methods
|
|
82
|
+
def __get_pubsub_layer__(self, config=None):
|
|
83
|
+
"""
|
|
84
|
+
A method to get the PubSubLayer given the settings.DJANGO_SOCKETS_CONFIG
|
|
85
|
+
"""
|
|
86
|
+
config = get_config(config)
|
|
87
|
+
if "hosts" in config:
|
|
88
|
+
return PubSubLayer(**config)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def __warn_on_not_usable__(self):
|
|
92
|
+
"""
|
|
93
|
+
Warn the user if the broadcaster is not usable
|
|
94
|
+
"""
|
|
95
|
+
if not self.__usable__:
|
|
96
|
+
logger.log(
|
|
97
|
+
logging.ERROR,
|
|
98
|
+
"No hosts provided in settings.DJANGO_SOCKETS_CONFIG. Broadcasting / Subscribing is not possible.",
|
|
99
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from .utils import database_sync_to_async
|
|
2
|
+
|
|
3
|
+
def get_anonymous_user_obj():
|
|
4
|
+
try:
|
|
5
|
+
return AnonymousUser()
|
|
6
|
+
except:
|
|
7
|
+
pass
|
|
8
|
+
try:
|
|
9
|
+
from django.contrib.auth.models import AnonymousUser
|
|
10
|
+
return AnonymousUser()
|
|
11
|
+
except:
|
|
12
|
+
pass
|
|
13
|
+
return None
|
|
14
|
+
|
|
15
|
+
def get_drf_token_obj():
|
|
16
|
+
try:
|
|
17
|
+
return Token
|
|
18
|
+
except:
|
|
19
|
+
pass
|
|
20
|
+
try:
|
|
21
|
+
from rest_framework.authtoken.models import Token
|
|
22
|
+
return Token
|
|
23
|
+
except:
|
|
24
|
+
pass
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
@database_sync_to_async
|
|
28
|
+
def get_drf_user(user_token):
|
|
29
|
+
if user_token is not None:
|
|
30
|
+
try:
|
|
31
|
+
token = get_drf_token_obj().objects.get(key=user_token)
|
|
32
|
+
return token.user
|
|
33
|
+
except:
|
|
34
|
+
pass
|
|
35
|
+
return get_anonymous_user_obj()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_query_arg(query_string, arg, default=None):
|
|
39
|
+
try:
|
|
40
|
+
return (
|
|
41
|
+
dict((x.split("=") for x in query_string.decode().split("&")))
|
|
42
|
+
).get(arg, default)
|
|
43
|
+
except:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DRFTokenAuthMiddleware:
|
|
48
|
+
def __init__(self, app):
|
|
49
|
+
self.app = app
|
|
50
|
+
|
|
51
|
+
async def __call__(self, scope, receive, send):
|
|
52
|
+
scope = dict(scope)
|
|
53
|
+
scope["user"] = await get_drf_user(
|
|
54
|
+
get_query_arg(scope["query_string"], "user_token")
|
|
55
|
+
)
|
|
56
|
+
return await self.app(scope, receive, send)
|