22 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
Sander 53a82fda3f Bump version 2022-03-10 15:00:32 +02:00
Sander a82799d358 use self._config instead of app.config 2022-03-10 14:59:32 +02:00
Sander 80f39ec79b Merge pull request #8 from sanderfoobar/session-uri-docs
Change README to support new config option `SESSION_URI`
2022-03-10 14:55:03 +02:00
8 changed files with 176 additions and 41 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
- 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
### 1.0.0 2020-01-15
+1 -1
View File
@@ -1,5 +1,5 @@
Copyright (c) 2014 by Shipeng Feng.
Copyright (c) 2020 by Sander.
Copyright (c) 2020 by Kroket Ltd.
Some rights reserved.
+1
View File
@@ -7,3 +7,4 @@ recursive-include quart_session *.md
exclude .gitlab-ci.yml
exclude examples
exclude docs
exclude venv
+16 -3
View File
@@ -43,7 +43,7 @@ app.run()
### Redis
via `aioredis>=2.0.0`.
via `redis>=4.4.0`.
```python3
app = Quart(__name__)
@@ -101,6 +101,18 @@ app.config['SESSION_TYPE'] = 'memcached'
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
[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:
```python3
from quart_session.sessions import SessionInterface
@app.route("/")
async def hello():
cache = app.session_interface
cache: SessionInterface = app.session_interface
await cache.set("random_key", "val", expiry=3600)
data = await cache.get("random_key")
```
@@ -174,7 +188,6 @@ by explicitly setting `SESSION_REVERSE_PROXY` to `True`.
## Future development
- `MongoDBSessionInterface`
- `FileSystemSessionInterface`
- `GoogleCloudDatastoreSessionInterface`
- Pytest
+1 -1
View File
@@ -5,7 +5,7 @@
Quart-Session demo.
:copyright: (c) 2020 by Sander.
:copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details.
"""
from quart import Quart, session
+19 -3
View File
@@ -6,17 +6,23 @@
Adds server session support to your application.
:copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by Sander.
:copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details.
"""
__version__ = '1.0.3'
__version__ = '3.0.0'
import os
from quart import Quart
from .sessions import RedisSessionInterface, RedisTrioSessionInterface, MemcachedSessionInterface, NullSessionInterface
from .sessions import (
RedisSessionInterface,
RedisTrioSessionInterface,
MemcachedSessionInterface,
MongoDBSessionInterface,
NullSessionInterface
)
class Session(object):
@@ -133,6 +139,16 @@ class Session(object):
use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'],
**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':
app.logger.warning(f"{backend_warning}. Currently using: null")
session_interface = NullSessionInterface(
+111 -24
View File
@@ -6,13 +6,14 @@
Server-side Sessions and SessionInterfaces.
:copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by Sander.
:copyright: (c) 2020 by Kroket Ltd.
:license: BSD, see LICENSE for more details.
"""
import time
from typing import Optional
from uuid import uuid4
from uuid import uuid4, UUID
import asyncio
import functools
from quart import Quart, current_app
from quart.wrappers import BaseRequestWebsocket, Response
@@ -30,7 +31,7 @@ class ServerSideSession(SecureCookieSession):
"""Baseclass for server-side based sessions."""
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
if permanent:
self.permanent = permanent
@@ -60,6 +61,10 @@ class MemcachedSession(ServerSideSession):
pass
class MongoDBSession(ServerSideSession):
pass
class NullSession(ServerSideSession):
pass
@@ -87,7 +92,8 @@ class SessionInterface(QuartSessionInterface):
app: Quart,
request: BaseRequestWebsocket
) -> 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:
# and no, you cannot define your own incoming
# header, stick to standards :-)
@@ -120,14 +126,17 @@ class SessionInterface(QuartSessionInterface):
options['sid'] = self._generate_sid()
return self.session_class(**options)
try:
data = self.serializer.loads(val)
except:
app.logger.warning(f"Failed to deserialize session "
f"data for sid: {sid}. Generating new sid.")
app.logger.debug(f"data: {val}")
options['sid'] = self._generate_sid()
return self.session_class(**options)
if self.serializer is None:
data = val
else:
try:
data = self.serializer.loads(val)
except:
app.logger.warning(f"Failed to deserialize session "
f"data for sid: {sid}. Generating new sid.")
app.logger.debug(f"data: {val}")
options['sid'] = self._generate_sid()
return self.session_class(**options)
protection = self._config['SESSION_PROTECTION']
if protection is True and addr is not None and \
@@ -155,13 +164,14 @@ class SessionInterface(QuartSessionInterface):
isinstance(response.response, FileBody):
return
cname = app.config.get('SESSION_COOKIE_NAME', 'session')
session_key = self.key_prefix + session.sid
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
if not session:
if session.modified:
await self.delete(key=session_key, app=app)
response.delete_cookie(app.session_cookie_name,
response.delete_cookie(cname,
domain=domain, path=path)
return
httponly = self.get_cookie_httponly(app)
@@ -169,20 +179,24 @@ class SessionInterface(QuartSessionInterface):
secure = self.get_cookie_secure(app)
expires = self.get_expiration_time(app, session)
val = self.serializer.dumps(dict(session))
if self.serializer is None:
val = dict(session)
else:
val = self.serializer.dumps(dict(session))
await self.set(key=session_key, value=val, app=app)
if self.use_signer:
session_id = self._get_signer(app).sign(want_bytes(session.sid))
else:
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,
domain=domain, path=path, secure=secure, samesite=samesite)
async def create(self, app: Quart):
raise NotImplementedError()
async def get(self, app: Quart, key: str):
async def get(self, key: str, app: Quart = None):
raise NotImplementedError()
async def set(self, key: str, value, expiry: int = None,
@@ -228,8 +242,8 @@ class RedisSessionInterface(SessionInterface):
for connection pool examples).
"""
if self.backend is None:
import aioredis
uri = app.config.get('SESSION_URI', 'redis://localhost')
from redis import asyncio as aioredis
uri = self._config.get('SESSION_URI', 'redis://localhost')
self.backend = await aioredis.from_url(
uri, encoding="utf-8", decode_responses=True
)
@@ -315,15 +329,12 @@ class MemcachedSessionInterface(SessionInterface):
permanent=permanent, **kwargs)
self.backend = memcached
@asyncio.coroutine
def create(self, app: Quart) -> None:
async def create(self, app: Quart) -> None:
if self.backend is None:
import aiomcache
loop = asyncio.get_running_loop()
#self.backend = aiomcache.Client("127.0.0.1", 11211, loop=loop)
# self.backend = aiomcache.Client("127.0.0.1", 11211)
self.backend = aiomcache.Client(self._config.get('SESSION_MEMCACHED_HOST', '127.0.0.1'),
self._config.get('SESSION_MEMCACHED_PORT', 11211),
loop=loop)
self._config.get('SESSION_MEMCACHED_PORT', 11211))
def _get_memcache_timeout(self, timeout):
"""
@@ -360,6 +371,82 @@ class MemcachedSessionInterface(SessionInterface):
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):
"""This class does absolutely nothing"""
session_class = NullSession
+8 -7
View File
@@ -9,7 +9,7 @@ Links
`````
* `Github
<https://github.com/sferdi0/quart-session>`_
<https://github.com/kroketio/quart-session>`_
"""
from setuptools import setup
@@ -19,16 +19,16 @@ with open('README.md') as f:
INSTALL_REQUIRES = [
"Quart>=0.10.0"
"Quart>=0.19.0"
]
setup(
name='Quart-Session',
version='1.0.3',
url='https://github.com/sferdi0/quart-session',
version='3.0.1',
url='https://github.com/kroketio/quart-session',
license='BSD',
author='Sander',
author_email='sander@sanderf.nl',
author='Kroket Ltd.',
author_email='code@kroket.io',
description='Adds server-side session support to your Quart application',
long_description=long_description,
long_description_content_type='text/markdown',
@@ -40,7 +40,8 @@ setup(
tests_require=INSTALL_REQUIRES + ["asynctest", "hypothesis", "pytest", "pytest-asyncio"],
extras_require={
"dotenv": ["python-dotenv"],
"redis": ["aioredis>=2.0.0"]
"mongodb": ["motor>=2.5.1"],
"redis": ["redis>=4.4.0"]
},
classifiers=[
'Environment :: Web Environment',