Skip to content

Commit 9f21867

Browse files
user sessions
1 parent 391a454 commit 9f21867

9 files changed

+360
-0
lines changed

docs/server.rst

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,70 @@ during the broadcast.
275275
def message(sid, data):
276276
sio.emit('my reply', data, room='chat_users', skip_sid=sid)
277277

278+
User Sessions
279+
-------------
280+
281+
The server can maintain application-specific information in a user session
282+
dedicated to each connected client. Applications can use the user session to
283+
write any details about the user that need to be preserved throughout the life
284+
of the connection, such as usernames or user ids.
285+
286+
The ``save_session()`` and ``get_session()`` methods are used to store and
287+
retrieve information in the user session::
288+
289+
@sio.on('connect')
290+
def on_connect(sid, environ):
291+
username = authenticate_user(environ)
292+
sio.save_session(sid, {'username': username})
293+
294+
@sio.on('message')
295+
def on_message(sid, data):
296+
session = sio.get_session(sid)
297+
print('message from ', session['username'])
298+
299+
For the ``asyncio`` server, these methods are coroutines::
300+
301+
302+
@sio.on('connect')
303+
async def on_connect(sid, environ):
304+
username = authenticate_user(environ)
305+
await sio.save_session(sid, {'username': username})
306+
307+
@sio.on('message')
308+
async def on_message(sid, data):
309+
session = await sio.get_session(sid)
310+
print('message from ', session['username'])
311+
312+
The session can also be manipulated with the `session()` context manager::
313+
314+
@sio.on('connect')
315+
def on_connect(sid, environ):
316+
username = authenticate_user(environ)
317+
with sio.session(sid) as session:
318+
session['username'] = username
319+
320+
@sio.on('message')
321+
def on_message(sid, data):
322+
with sio.session(sid) as session:
323+
print('message from ', session['username'])
324+
325+
For the ``asyncio`` server, an asynchronous context manager is used::
326+
327+
@sio.on('connect')
328+
def on_connect(sid, environ):
329+
username = authenticate_user(environ)
330+
async with sio.session(sid) as session:
331+
session['username'] = username
332+
333+
@sio.on('message')
334+
def on_message(sid, data):
335+
async with sio.session(sid) as session:
336+
print('message from ', session['username'])
337+
338+
The ``get_session()``, ``save_session()`` and ``session()`` methods take an
339+
optional ``namespace`` argument. If this argument isn't provided, the session
340+
is attached to the default namespace.
341+
278342
Using a Message Queue
279343
---------------------
280344

socketio/asyncio_namespace.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,39 @@ async def close_room(self, room, namespace=None):
8282
return await self.server.close_room(
8383
room, namespace=namespace or self.namespace)
8484

85+
async def get_session(self, sid, namespace=None):
86+
"""Return the user session for a client.
87+
88+
The only difference with the :func:`socketio.Server.get_session`
89+
method is that when the ``namespace`` argument is not given the
90+
namespace associated with the class is used.
91+
92+
Note: this method is a coroutine.
93+
"""
94+
return await self.server.get_session(
95+
sid, namespace=namespace or self.namespace)
96+
97+
async def save_session(self, sid, session, namespace=None):
98+
"""Store the user session for a client.
99+
100+
The only difference with the :func:`socketio.Server.save_session`
101+
method is that when the ``namespace`` argument is not given the
102+
namespace associated with the class is used.
103+
104+
Note: this method is a coroutine.
105+
"""
106+
return await self.server.save_session(
107+
sid, session, namespace=namespace or self.namespace)
108+
109+
def session(self, sid, namespace=None):
110+
"""Return the user session for a client with context manager syntax.
111+
112+
The only difference with the :func:`socketio.Server.session` method is
113+
that when the ``namespace`` argument is not given the namespace
114+
associated with the class is used.
115+
"""
116+
return self.server.session(sid, namespace=namespace or self.namespace)
117+
85118
async def disconnect(self, sid, namespace=None):
86119
"""Disconnect a client.
87120

socketio/asyncio_server.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,73 @@ async def close_room(self, room, namespace=None):
169169
self.logger.info('room %s is closing [%s]', room, namespace)
170170
await self.manager.close_room(room, namespace)
171171

172+
async def get_session(self, sid, namespace=None):
173+
"""Return the user session for a client.
174+
175+
:param sid: The session id of the client.
176+
:param namespace: The Socket.IO namespace. If this argument is omitted
177+
the default namespace is used.
178+
179+
The return value is a dictionary. Modifications made to this
180+
dictionary are not guaranteed to be preserved. If you want to modify
181+
the user session, use the ``session`` context manager instead.
182+
"""
183+
namespace = namespace or '/'
184+
eio_session = await self.eio.get_session(sid)
185+
return eio_session.setdefault(namespace, {})
186+
187+
async def save_session(self, sid, session, namespace=None):
188+
"""Store the user session for a client.
189+
190+
:param sid: The session id of the client.
191+
:param session: The session dictionary.
192+
:param namespace: The Socket.IO namespace. If this argument is omitted
193+
the default namespace is used.
194+
"""
195+
namespace = namespace or '/'
196+
eio_session = await self.eio.get_session(sid)
197+
eio_session[namespace] = session
198+
199+
def session(self, sid, namespace=None):
200+
"""Return the user session for a client with context manager syntax.
201+
202+
:param sid: The session id of the client.
203+
204+
This is a context manager that returns the user session dictionary for
205+
the client. Any changes that are made to this dictionary inside the
206+
context manager block are saved back to the session. Example usage::
207+
208+
@eio.on('connect')
209+
def on_connect(sid, environ):
210+
username = authenticate_user(environ)
211+
if not username:
212+
return False
213+
with eio.session(sid) as session:
214+
session['username'] = username
215+
216+
@eio.on('message')
217+
def on_message(sid, msg):
218+
async with eio.session(sid) as session:
219+
print('received message from ', session['username'])
220+
"""
221+
class _session_context_manager(object):
222+
def __init__(self, server, sid, namespace):
223+
self.server = server
224+
self.sid = sid
225+
self.namespace = namespace
226+
self.session = None
227+
228+
async def __aenter__(self):
229+
self.session = await self.server.get_session(
230+
sid, namespace=self.namespace)
231+
return self.session
232+
233+
async def __aexit__(self, *args):
234+
await self.server.save_session(sid, self.session,
235+
namespace=self.namespace)
236+
237+
return _session_context_manager(self, sid, namespace)
238+
172239
async def disconnect(self, sid, namespace=None):
173240
"""Disconnect a client.
174241

socketio/namespace.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,35 @@ def rooms(self, sid, namespace=None):
100100
"""
101101
return self.server.rooms(sid, namespace=namespace or self.namespace)
102102

103+
def get_session(self, sid, namespace=None):
104+
"""Return the user session for a client.
105+
106+
The only difference with the :func:`socketio.Server.get_session`
107+
method is that when the ``namespace`` argument is not given the
108+
namespace associated with the class is used.
109+
"""
110+
return self.server.get_session(
111+
sid, namespace=namespace or self.namespace)
112+
113+
def save_session(self, sid, session, namespace=None):
114+
"""Store the user session for a client.
115+
116+
The only difference with the :func:`socketio.Server.save_session`
117+
method is that when the ``namespace`` argument is not given the
118+
namespace associated with the class is used.
119+
"""
120+
return self.server.save_session(
121+
sid, session, namespace=namespace or self.namespace)
122+
123+
def session(self, sid, namespace=None):
124+
"""Return the user session for a client with context manager syntax.
125+
126+
The only difference with the :func:`socketio.Server.session` method is
127+
that when the ``namespace`` argument is not given the namespace
128+
associated with the class is used.
129+
"""
130+
return self.server.session(sid, namespace=namespace or self.namespace)
131+
103132
def disconnect(self, sid, namespace=None):
104133
"""Disconnect a client.
105134

socketio/server.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,74 @@ def rooms(self, sid, namespace=None):
317317
namespace = namespace or '/'
318318
return self.manager.get_rooms(sid, namespace)
319319

320+
def get_session(self, sid, namespace=None):
321+
"""Return the user session for a client.
322+
323+
:param sid: The session id of the client.
324+
:param namespace: The Socket.IO namespace. If this argument is omitted
325+
the default namespace is used.
326+
327+
The return value is a dictionary. Modifications made to this
328+
dictionary are not guaranteed to be preserved unless
329+
``save_session()`` is called, or when the ``session`` context manager
330+
is used.
331+
"""
332+
namespace = namespace or '/'
333+
eio_session = self.eio.get_session(sid)
334+
return eio_session.setdefault(namespace, {})
335+
336+
def save_session(self, sid, session, namespace=None):
337+
"""Store the user session for a client.
338+
339+
:param sid: The session id of the client.
340+
:param session: The session dictionary.
341+
:param namespace: The Socket.IO namespace. If this argument is omitted
342+
the default namespace is used.
343+
"""
344+
namespace = namespace or '/'
345+
eio_session = self.eio.get_session(sid)
346+
eio_session[namespace] = session
347+
348+
def session(self, sid, namespace=None):
349+
"""Return the user session for a client with context manager syntax.
350+
351+
:param sid: The session id of the client.
352+
353+
This is a context manager that returns the user session dictionary for
354+
the client. Any changes that are made to this dictionary inside the
355+
context manager block are saved back to the session. Example usage::
356+
357+
@sio.on('connect')
358+
def on_connect(sid, environ):
359+
username = authenticate_user(environ)
360+
if not username:
361+
return False
362+
with sio.session(sid) as session:
363+
session['username'] = username
364+
365+
@sio.on('message')
366+
def on_message(sid, msg):
367+
with sio.session(sid) as session:
368+
print('received message from ', session['username'])
369+
"""
370+
class _session_context_manager(object):
371+
def __init__(self, server, sid, namespace):
372+
self.server = server
373+
self.sid = sid
374+
self.namespace = namespace
375+
self.session = None
376+
377+
def __enter__(self):
378+
self.session = self.server.get_session(sid,
379+
namespace=namespace)
380+
return self.session
381+
382+
def __exit__(self, *args):
383+
self.server.save_session(sid, self.session,
384+
namespace=namespace)
385+
386+
return _session_context_manager(self, sid, namespace)
387+
320388
def disconnect(self, sid, namespace=None):
321389
"""Disconnect a client.
322390

tests/test_asyncio_namespace.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,27 @@ def test_rooms(self):
170170
ns.rooms('sid', namespace='/bar')
171171
ns.server.rooms.assert_called_with('sid', namespace='/bar')
172172

173+
def test_session(self):
174+
ns = asyncio_namespace.AsyncNamespace('/foo')
175+
mock_server = mock.MagicMock()
176+
mock_server.get_session = AsyncMock()
177+
mock_server.save_session = AsyncMock()
178+
ns._set_server(mock_server)
179+
_run(ns.get_session('sid'))
180+
ns.server.get_session.mock.assert_called_with('sid', namespace='/foo')
181+
_run(ns.get_session('sid', namespace='/bar'))
182+
ns.server.get_session.mock.assert_called_with('sid', namespace='/bar')
183+
_run(ns.save_session('sid', {'a': 'b'}))
184+
ns.server.save_session.mock.assert_called_with('sid', {'a': 'b'},
185+
namespace='/foo')
186+
_run(ns.save_session('sid', {'a': 'b'}, namespace='/bar'))
187+
ns.server.save_session.mock.assert_called_with('sid', {'a': 'b'},
188+
namespace='/bar')
189+
ns.session('sid')
190+
ns.server.session.assert_called_with('sid', namespace='/foo')
191+
ns.session('sid', namespace='/bar')
192+
ns.server.session.assert_called_with('sid', namespace='/bar')
193+
173194
def test_disconnect(self):
174195
ns = asyncio_namespace.AsyncNamespace('/foo')
175196
mock_server = mock.MagicMock()

tests/test_asyncio_server.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,38 @@ def test_send_with_ack_namespace(self, eio):
454454
_run(s._handle_eio_message('123', '3/foo,1["foo",2]'))
455455
cb.assert_called_once_with('foo', 2)
456456

457+
def test_session(self, eio):
458+
fake_session = {}
459+
460+
async def fake_get_session(sid):
461+
return fake_session
462+
463+
async def fake_save_session(sid, session):
464+
global fake_session
465+
fake_session = session
466+
467+
eio.return_value.send = AsyncMock()
468+
s = asyncio_server.AsyncServer()
469+
s.eio.get_session = fake_get_session
470+
s.eio.save_session = fake_save_session
471+
472+
async def _test():
473+
await s._handle_eio_connect('123', 'environ')
474+
await s.save_session('123', {'foo': 'bar'})
475+
async with s.session('123') as session:
476+
self.assertEqual(session, {'foo': 'bar'})
477+
session['foo'] = 'baz'
478+
session['bar'] = 'foo'
479+
self.assertEqual(await s.get_session('123'), {'foo': 'baz', 'bar': 'foo'})
480+
self.assertEqual(fake_session, {'/': {'foo': 'baz', 'bar': 'foo'}})
481+
async with s.session('123', namespace='/ns') as session:
482+
self.assertEqual(session, {})
483+
session['a'] = 'b'
484+
self.assertEqual(await s.get_session('123', namespace='/ns'), {'a': 'b'})
485+
self.assertEqual(fake_session, {'/': {'foo': 'baz', 'bar': 'foo'},
486+
'/ns': {'a': 'b'}})
487+
_run(_test())
488+
457489
def test_disconnect(self, eio):
458490
eio.return_value.send = AsyncMock()
459491
s = asyncio_server.AsyncServer()

tests/test_namespace.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ def test_rooms(self):
120120
ns.rooms('sid', namespace='/bar')
121121
ns.server.rooms.assert_called_with('sid', namespace='/bar')
122122

123+
def test_session(self):
124+
ns = namespace.Namespace('/foo')
125+
ns._set_server(mock.MagicMock())
126+
ns.get_session('sid')
127+
ns.server.get_session.assert_called_with('sid', namespace='/foo')
128+
ns.get_session('sid', namespace='/bar')
129+
ns.server.get_session.assert_called_with('sid', namespace='/bar')
130+
ns.save_session('sid', {'a': 'b'})
131+
ns.server.save_session.assert_called_with('sid', {'a': 'b'},
132+
namespace='/foo')
133+
ns.save_session('sid', {'a': 'b'}, namespace='/bar')
134+
ns.server.save_session.assert_called_with('sid', {'a': 'b'},
135+
namespace='/bar')
136+
ns.session('sid')
137+
ns.server.session.assert_called_with('sid', namespace='/foo')
138+
ns.session('sid', namespace='/bar')
139+
ns.server.session.assert_called_with('sid', namespace='/bar')
140+
123141
def test_disconnect(self):
124142
ns = namespace.Namespace('/foo')
125143
ns._set_server(mock.MagicMock())

0 commit comments

Comments
 (0)