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.
@@ -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
+ [![PyPI version](https://badge.fury.io/py/django_sockets.svg)](https://badge.fury.io/py/django_sockets)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://badge.fury.io/py/django_sockets.svg)](https://badge.fury.io/py/django_sockets)
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)