Merge pull request #10 from smithk86/mongodb

add session handler for mongodb: MongoDBSessionInterface
This commit is contained in:
Sander
2022-03-20 12:55:20 +02:00
committed by GitHub
4 changed files with 130 additions and 14 deletions
+12 -1
View File
@@ -101,6 +101,18 @@ app.config['SESSION_TYPE'] = 'memcached'
Session(app) Session(app)
``` ```
### MongoDB
via `motor`.
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'mongodb'
app.config['SESSION_MONGODB_URI'] = 'mongodb://localhost:27017/my_database'
app.config['SESSION_MONGODB_COLLECTION'] = 'sessions'
Session(app)
```
### JSON serializer ### JSON serializer
[flask-session](https://pypi.org/project/Flask-Session/) uses `pickle` [flask-session](https://pypi.org/project/Flask-Session/) uses `pickle`
@@ -174,7 +186,6 @@ by explicitly setting `SESSION_REVERSE_PROXY` to `True`.
## Future development ## Future development
- `MongoDBSessionInterface`
- `FileSystemSessionInterface` - `FileSystemSessionInterface`
- `GoogleCloudDatastoreSessionInterface` - `GoogleCloudDatastoreSessionInterface`
- Pytest - Pytest
+18 -2
View File
@@ -10,13 +10,19 @@
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
__version__ = '1.0.4' __version__ = '1.0.5-dev'
import os import os
from quart import Quart from quart import Quart
from .sessions import RedisSessionInterface, RedisTrioSessionInterface, MemcachedSessionInterface, NullSessionInterface from .sessions import (
RedisSessionInterface,
RedisTrioSessionInterface,
MemcachedSessionInterface,
MongoDBSessionInterface,
NullSessionInterface
)
class Session(object): class Session(object):
@@ -133,6 +139,16 @@ class Session(object):
use_signer=config['SESSION_USE_SIGNER'], use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'], permanent=config['SESSION_PERMANENT'],
**config) **config)
elif config['SESSION_TYPE'] == 'mongodb':
session_interface = MongoDBSessionInterface(
mongodb_uri=config['SESSION_MONGODB_URI'],
collection=config['SESSION_MONGODB_COLLECTION'],
client_kwargs=config.get('SESSION_MONGODB_CLIENT_KWARGS', {}),
set_callback=config.get('SESSION_MONGODB_SET_CALLBACK'),
key_prefix=config['SESSION_KEY_PREFIX'],
use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'],
**config)
elif config['SESSION_TYPE'] == 'null': elif config['SESSION_TYPE'] == 'null':
app.logger.warning(f"{backend_warning}. Currently using: null") app.logger.warning(f"{backend_warning}. Currently using: null")
session_interface = NullSessionInterface( session_interface = NullSessionInterface(
+89 -1
View File
@@ -11,8 +11,9 @@
""" """
import time import time
from typing import Optional from typing import Optional
from uuid import uuid4 from uuid import uuid4, UUID
import asyncio import asyncio
import functools
from quart import Quart, current_app from quart import Quart, current_app
from quart.wrappers import BaseRequestWebsocket, Response from quart.wrappers import BaseRequestWebsocket, Response
@@ -60,6 +61,10 @@ class MemcachedSession(ServerSideSession):
pass pass
class MongoDBSession(ServerSideSession):
pass
class NullSession(ServerSideSession): class NullSession(ServerSideSession):
pass pass
@@ -120,6 +125,9 @@ class SessionInterface(QuartSessionInterface):
options['sid'] = self._generate_sid() options['sid'] = self._generate_sid()
return self.session_class(**options) return self.session_class(**options)
if self.serializer is None:
data = val
else:
try: try:
data = self.serializer.loads(val) data = self.serializer.loads(val)
except: except:
@@ -169,7 +177,11 @@ class SessionInterface(QuartSessionInterface):
secure = self.get_cookie_secure(app) secure = self.get_cookie_secure(app)
expires = self.get_expiration_time(app, session) expires = self.get_expiration_time(app, session)
if self.serializer is None:
val = dict(session)
else:
val = self.serializer.dumps(dict(session)) val = self.serializer.dumps(dict(session))
await self.set(key=session_key, value=val, app=app) await self.set(key=session_key, value=val, app=app)
if self.use_signer: if self.use_signer:
session_id = self._get_signer(app).sign(want_bytes(session.sid)) session_id = self._get_signer(app).sign(want_bytes(session.sid))
@@ -360,6 +372,82 @@ class MemcachedSessionInterface(SessionInterface):
return await self.backend.delete(key) return await self.backend.delete(key)
def _convert_key_to_uuid(func):
"""
convert the session UUID to a UUID object for mongodb
example:
"session:b8ebbf02-cc7a-4b0b-824f-22a984c8c0b8" ->
UUID("b8ebbf02-cc7a-4b0b-824f-22a984c8c0b8")
"""
@functools.wraps(func)
async def wrapper(*args, **kwargs):
if 'key' in kwargs:
key = kwargs['key']
try:
if key.startswith('session:'):
_, _uuid = tuple(key.split(':'))
kwargs['key'] = UUID(_uuid)
except Exception as e:
current_app.logger.warning(
f"session could not be converted to a uuid object: {key}"
)
return await func(*args, **kwargs)
return wrapper
class MongoDBSessionInterface(SessionInterface):
# mongodb does not a serializer as many object types are properly handled by the connector
serializer = None
session_class = MongoDBSession
def __init__(self, mongodb_uri, collection, client_kwargs={}, set_callback=None, **kwargs):
from motor.motor_asyncio import AsyncIOMotorClient
super().__init__(**kwargs)
self.mongodb_uri = mongodb_uri
self.client_kwargs = client_kwargs
self.set_callback = set_callback
self._collection = collection
self._client = AsyncIOMotorClient(self.mongodb_uri, uuidRepresentation='standard', **self.client_kwargs)
self._database = self._client.get_database()
async def create(self, app: Quart) -> None:
pass
@_convert_key_to_uuid
async def get(self, key, app):
value = await self.collection.find_one({'_id': key}, {'data': True})
if value:
return value.get('data', {})
else:
return None
@_convert_key_to_uuid
async def set(self, key, value, expiry=None, app=None):
doc = {
'data': value,
}
# allows the document to be modified prior upsert
if callable(self.set_callback):
self.set_callback(doc)
await self.collection.update_one({
'_id': key
}, {
'$set': doc
},
upsert=True
)
@property
def collection(self):
return self._database.get_collection(self._collection)
class NullSessionInterface(SessionInterface): class NullSessionInterface(SessionInterface):
"""This class does absolutely nothing""" """This class does absolutely nothing"""
session_class = NullSession session_class = NullSession
+2 -1
View File
@@ -24,7 +24,7 @@ INSTALL_REQUIRES = [
setup( setup(
name='Quart-Session', name='Quart-Session',
version='1.0.4', version='1.0.5-dev',
url='https://github.com/sferdi0/quart-session', url='https://github.com/sferdi0/quart-session',
license='BSD', license='BSD',
author='Sander', author='Sander',
@@ -40,6 +40,7 @@ setup(
tests_require=INSTALL_REQUIRES + ["asynctest", "hypothesis", "pytest", "pytest-asyncio"], tests_require=INSTALL_REQUIRES + ["asynctest", "hypothesis", "pytest", "pytest-asyncio"],
extras_require={ extras_require={
"dotenv": ["python-dotenv"], "dotenv": ["python-dotenv"],
"mongodb": ["motor>=2.5.1"],
"redis": ["aioredis>=2.0.0"] "redis": ["aioredis>=2.0.0"]
}, },
classifiers=[ classifiers=[