19 Commits

Author SHA1 Message Date
daniel156161 b3105423f8 fix that it can set the responce cookie correctly for newer werkzeug version 2025-09-18 18:16:18 +02:00
Kroket Ltd 249a7abd89 Bump to 3.0.0 2023-11-24 04:12:03 +02:00
Kroket Ltd 093d28fe62 Merge pull request #19 from jonathonfletcher/master
do not unpack initial in call to ServerSideSession's super.
2023-11-24 04:07:24 +02:00
Jonathon Fletcher 7e43c76ad0 do not unpack initial in call to ServerSideSession's super. 2023-11-04 11:50:46 -07:00
Kroket Ltd c25a62412d Merge pull request #16 from kroketio/session_cookie_name_fix
Session cookie name fix
2023-10-17 01:42:31 +03:00
Kroket Ltd 9186fae9e7 bump version to 2.1.0 2023-10-17 01:41:24 +03:00
Kroket Ltd 31a56ddd47 session_cookie_name fix due to https://github.com/pallets/flask/pull/4995/files 2023-10-17 01:40:11 +03:00
Sander 6ad2a3789d Update CHANGELOG, bump version 2023-01-16 19:31:09 +02:00
Sander d08156f193 Merge pull request #12 from degenhard/master
Change for python 3.11 aioredis to redis.
2023-01-16 12:17:46 +02:00
Peter Heise 39e5fc5c13 Updated setup.py redis requirement. 2023-01-16 10:16:33 +01:00
Peter Heise 237f2354ad Changed redis package in readme. 2023-01-15 21:42:18 +01:00
Peter Heise 5119f60434 Changed aioredis import for python 3.11 2023-01-15 21:38:47 +01:00
Kroket Ltd 2f53caa654 Bump version 2022-11-24 17:14:14 +01:00
Sander cc99a72901 Merge pull request #11 from adrienyhuel/master
Fix MemcachedSession
2022-11-24 17:09:45 +01:00
Adrien YHUEL 5301b4418f Fix MemcachedSession
Remove asyncio loop argument
Remove asyncio.coroutine annotation as it is removed in Python 3.11
2022-11-24 15:10:48 +01:00
Kroket Ltd 3e6b102b34 Transfer copyright, bump version 2022-11-13 03:36:15 +02:00
Sander 65b44db7df Merge pull request #10 from smithk86/mongodb
add session handler for mongodb: MongoDBSessionInterface
2022-03-20 12:55:20 +02:00
Kyle Smith b9f2dc0067 add session handler for mongodb: MongoDBSessionInterface 2022-03-17 15:51:45 -04:00
Sander a19f227d88 Merge pull request #9 from sanderfoobar/self-cfg-update-version
dont use app.config, bump version
2022-03-10 15:01:28 +02:00
8 changed files with 175 additions and 40 deletions
+18 -1
View File
@@ -1,7 +1,24 @@
### 2.1.0
- `session_cookie_name` fix
### 2.0.0
- Move from aioredis to redis
### 1.0.7
- Updated memcached support (removed asyncio.loop, removed coroutine decorator)
### 1.0.6
- MongoDB support
- Transfer copyright to Kroket Ltd.
### 1.0.3 2021-08-31 ### 1.0.3 2021-08-31
- Migrated to aioredis 2 - Migrated to aioredis 2
- SameSite support https://github.com/sanderfoobar/quart-session/commit/8daae3a6734e8f7da13954d5a1a5da8f5fc5a49a - SameSite support https://github.com/kroketio/quart-session/commit/8daae3a6734e8f7da13954d5a1a5da8f5fc5a49a
- Memcached stuff https://github.com/filak/quart-session/commit/004871c495a069784e57e604b69f65af1b7e645a - Memcached stuff https://github.com/filak/quart-session/commit/004871c495a069784e57e604b69f65af1b7e645a
### 1.0.0 2020-01-15 ### 1.0.0 2020-01-15
+1 -1
View File
@@ -1,5 +1,5 @@
Copyright (c) 2014 by Shipeng Feng. Copyright (c) 2014 by Shipeng Feng.
Copyright (c) 2020 by Sander. Copyright (c) 2020 by Kroket Ltd.
Some rights reserved. Some rights reserved.
+1
View File
@@ -7,3 +7,4 @@ recursive-include quart_session *.md
exclude .gitlab-ci.yml exclude .gitlab-ci.yml
exclude examples exclude examples
exclude docs exclude docs
exclude venv
+16 -3
View File
@@ -43,7 +43,7 @@ app.run()
### Redis ### Redis
via `aioredis>=2.0.0`. via `redis>=4.4.0`.
```python3 ```python3
app = Quart(__name__) app = Quart(__name__)
@@ -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`
@@ -132,9 +144,11 @@ app.session_interface.serialize = pickle
At any point you may interface with the session back-end directly: At any point you may interface with the session back-end directly:
```python3 ```python3
from quart_session.sessions import SessionInterface
@app.route("/") @app.route("/")
async def hello(): async def hello():
cache = app.session_interface cache: SessionInterface = app.session_interface
await cache.set("random_key", "val", expiry=3600) await cache.set("random_key", "val", expiry=3600)
data = await cache.get("random_key") data = await cache.get("random_key")
``` ```
@@ -174,7 +188,6 @@ by explicitly setting `SESSION_REVERSE_PROXY` to `True`.
## Future development ## Future development
- `MongoDBSessionInterface`
- `FileSystemSessionInterface` - `FileSystemSessionInterface`
- `GoogleCloudDatastoreSessionInterface` - `GoogleCloudDatastoreSessionInterface`
- Pytest - Pytest
+1 -1
View File
@@ -5,7 +5,7 @@
Quart-Session demo. Quart-Session demo.
:copyright: (c) 2020 by Sander. :copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
from quart import Quart, session from quart import Quart, session
+19 -3
View File
@@ -6,17 +6,23 @@
Adds server session support to your application. Adds server session support to your application.
:copyright: (c) 2014 by Shipeng Feng. :copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by Sander. :copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
__version__ = '1.0.4' __version__ = '3.0.0'
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(
+101 -14
View File
@@ -6,13 +6,14 @@
Server-side Sessions and SessionInterfaces. Server-side Sessions and SessionInterfaces.
:copyright: (c) 2014 by Shipeng Feng. :copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by Sander. :copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details. :license: BSD, see LICENSE for more details.
""" """
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
@@ -30,7 +31,7 @@ class ServerSideSession(SecureCookieSession):
"""Baseclass for server-side based sessions.""" """Baseclass for server-side based sessions."""
def __init__(self, initial=None, sid=None, permanent=None, addr=None): def __init__(self, initial=None, sid=None, permanent=None, addr=None):
super(ServerSideSession, self).__init__(**initial or {}) super(ServerSideSession, self).__init__(initial or {})
self.sid = sid self.sid = sid
if permanent: if permanent:
self.permanent = permanent self.permanent = permanent
@@ -60,6 +61,10 @@ class MemcachedSession(ServerSideSession):
pass pass
class MongoDBSession(ServerSideSession):
pass
class NullSession(ServerSideSession): class NullSession(ServerSideSession):
pass pass
@@ -87,7 +92,8 @@ class SessionInterface(QuartSessionInterface):
app: Quart, app: Quart,
request: BaseRequestWebsocket request: BaseRequestWebsocket
) -> Optional[SecureCookieSession]: ) -> Optional[SecureCookieSession]:
sid = request.cookies.get(app.session_cookie_name) cname = app.config.get('SESSION_COOKIE_NAME', 'session')
sid = request.cookies.get(cname)
if self._config['SESSION_REVERSE_PROXY'] is True: if self._config['SESSION_REVERSE_PROXY'] is True:
# and no, you cannot define your own incoming # and no, you cannot define your own incoming
# header, stick to standards :-) # header, stick to standards :-)
@@ -120,6 +126,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:
@@ -155,13 +164,14 @@ class SessionInterface(QuartSessionInterface):
isinstance(response.response, FileBody): isinstance(response.response, FileBody):
return return
cname = app.config.get('SESSION_COOKIE_NAME', 'session')
session_key = self.key_prefix + session.sid session_key = self.key_prefix + session.sid
domain = self.get_cookie_domain(app) domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app) path = self.get_cookie_path(app)
if not session: if not session:
if session.modified: if session.modified:
await self.delete(key=session_key, app=app) await self.delete(key=session_key, app=app)
response.delete_cookie(app.session_cookie_name, response.delete_cookie(cname,
domain=domain, path=path) domain=domain, path=path)
return return
httponly = self.get_cookie_httponly(app) httponly = self.get_cookie_httponly(app)
@@ -169,20 +179,24 @@ 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))
else: else:
session_id = session.sid session_id = session.sid
response.set_cookie(app.session_cookie_name, session_id, response.set_cookie(cname, session_id.decode('utf-8'),
expires=expires, httponly=httponly, expires=expires, httponly=httponly,
domain=domain, path=path, secure=secure, samesite=samesite) domain=domain, path=path, secure=secure, samesite=samesite)
async def create(self, app: Quart): async def create(self, app: Quart):
raise NotImplementedError() raise NotImplementedError()
async def get(self, app: Quart, key: str): async def get(self, key: str, app: Quart = None):
raise NotImplementedError() raise NotImplementedError()
async def set(self, key: str, value, expiry: int = None, async def set(self, key: str, value, expiry: int = None,
@@ -228,7 +242,7 @@ class RedisSessionInterface(SessionInterface):
for connection pool examples). for connection pool examples).
""" """
if self.backend is None: if self.backend is None:
import aioredis from redis import asyncio as aioredis
uri = self._config.get('SESSION_URI', 'redis://localhost') uri = self._config.get('SESSION_URI', 'redis://localhost')
self.backend = await aioredis.from_url( self.backend = await aioredis.from_url(
uri, encoding="utf-8", decode_responses=True uri, encoding="utf-8", decode_responses=True
@@ -315,15 +329,12 @@ class MemcachedSessionInterface(SessionInterface):
permanent=permanent, **kwargs) permanent=permanent, **kwargs)
self.backend = memcached self.backend = memcached
@asyncio.coroutine async def create(self, app: Quart) -> None:
def create(self, app: Quart) -> None:
if self.backend is None: if self.backend is None:
import aiomcache import aiomcache
loop = asyncio.get_running_loop() # self.backend = aiomcache.Client("127.0.0.1", 11211)
#self.backend = aiomcache.Client("127.0.0.1", 11211, loop=loop)
self.backend = aiomcache.Client(self._config.get('SESSION_MEMCACHED_HOST', '127.0.0.1'), self.backend = aiomcache.Client(self._config.get('SESSION_MEMCACHED_HOST', '127.0.0.1'),
self._config.get('SESSION_MEMCACHED_PORT', 11211), self._config.get('SESSION_MEMCACHED_PORT', 11211))
loop=loop)
def _get_memcache_timeout(self, timeout): def _get_memcache_timeout(self, timeout):
""" """
@@ -360,6 +371,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
+8 -7
View File
@@ -9,7 +9,7 @@ Links
````` `````
* `Github * `Github
<https://github.com/sferdi0/quart-session>`_ <https://github.com/kroketio/quart-session>`_
""" """
from setuptools import setup from setuptools import setup
@@ -19,16 +19,16 @@ with open('README.md') as f:
INSTALL_REQUIRES = [ INSTALL_REQUIRES = [
"Quart>=0.10.0" "Quart>=0.19.0"
] ]
setup( setup(
name='Quart-Session', name='Quart-Session',
version='1.0.4', version='3.0.1',
url='https://github.com/sferdi0/quart-session', url='https://github.com/kroketio/quart-session',
license='BSD', license='BSD',
author='Sander', author='Kroket Ltd.',
author_email='sander@sanderf.nl', author_email='code@kroket.io',
description='Adds server-side session support to your Quart application', description='Adds server-side session support to your Quart application',
long_description=long_description, long_description=long_description,
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
@@ -40,7 +40,8 @@ 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"],
"redis": ["aioredis>=2.0.0"] "mongodb": ["motor>=2.5.1"],
"redis": ["redis>=4.4.0"]
}, },
classifiers=[ classifiers=[
'Environment :: Web Environment', 'Environment :: Web Environment',