3 Commits

Author SHA1 Message Date
dsc 121f8a5f8d Update README 2020-01-05 15:19:21 +01:00
dsc ce58ecb1df Redis+Trio support 2020-01-05 15:10:16 +01:00
dsc 499e46c93d Initial commit 2020-01-05 01:41:30 +01:00
8 changed files with 194 additions and 400 deletions
-36
View File
@@ -1,36 +0,0 @@
### 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/kroketio/quart-session/commit/8daae3a6734e8f7da13954d5a1a5da8f5fc5a49a
- Memcached stuff https://github.com/filak/quart-session/commit/004871c495a069784e57e604b69f65af1b7e645a
### 1.0.0 2020-01-15
- Added support for arbitrary usage of caching backends.
- Exposed `get`, `set`, `delete` on the session interface for direct usage.
- Renamed `SESSION_HIJACK_REVERSE_PROXY` to `SESSION_REVERSE_PROXY`.
- Renamed `SESSION_HIJACK_PROTECTION` to `SESSION_PROTECTION`.
- Removed fallback when `X-Forwarded-For` is not present whilst USING `SESSION_REVERSE_PROXY`, emit error instead.
- Fixed a bug where session timeouts would default to 600 seconds.
- Deprecated/disabled the `dirty()` method.
### 0.0.1 2020-01-04
- Released initial pre alpha version.
+1 -1
View File
@@ -1,5 +1,5 @@
Copyright (c) 2014 by Shipeng Feng.
Copyright (c) 2020 by Kroket Ltd.
Copyright (c) 2020 by dsc.
Some rights reserved.
-10
View File
@@ -1,10 +0,0 @@
include LICENSE
include CHANGELOG.md
include README.md
include setup.cfg
recursive-include quart_session *.py
recursive-include quart_session *.md
exclude .gitlab-ci.yml
exclude examples
exclude docs
exclude venv
+72 -83
View File
@@ -1,22 +1,21 @@
# Quart-Session
# Quart-session
![pyversions](https://img.shields.io/pypi/pyversions/Quart-Session.svg) [![pypiversion](https://badge.fury.io/py/Quart-Session.svg)](https://pypi.org/project/Quart-Session/) ![PyPI license](https://img.shields.io/pypi/l/Quart-Session.svg)
Quart-Session is an extension for [Quart](https://gitlab.com/pgjones/quart/blob/master/README.rst) that adds support for
Quart-Session is an extension for Quart that adds support for
server-side sessions to your application.
Based on [flask-session](https://pypi.org/project/Flask-Session/).
## Quick start
Quart-Session can be installed via pipenv or pip,
Quart-Session can be installed via pipenv or
pip,
```bash
$ pipenv install quart-session
$ pip install quart-session
```
and requires Python 3.7.0 or higher. A minimal Quart-Session example is:
and requires Python 3.7.0 or higher. A fairly minimal Quart-Session example is,
```python3
from quart import Quart, session
@@ -29,11 +28,7 @@ Session(app)
@app.route('/')
async def hello():
session["foo"] = "bar"
return "session key 'foo' set"
@app.route('/foo')
async def foo():
return session.get("foo", "session key 'foo' not found")
return 'hello'
app.run()
```
@@ -41,9 +36,9 @@ app.run()
## Features
### Redis
### Redis support
via `redis>=4.4.0`.
via `aioredis`.
```python3
app = Quart(__name__)
@@ -51,16 +46,8 @@ app.config['SESSION_TYPE'] = 'redis'
Session(app)
```
By default, Quart-session connects to Redis at `127.0.0.1:6379`. If you
have a different location, use `SESSION_URI`
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_URI'] = 'redis://:password@localhost:6379'
```
Alternatively, for extra control, you may provide your own `aioredis.Client` instance altogether.
If you already have a `aioredis.Client` instance and you'd like to share
it with the session interface,
```python3
app = Quart(__name__)
@@ -68,17 +55,15 @@ app.config['SESSION_TYPE'] = 'redis'
@app.before_serving
async def setup():
cache = await aioredis.Redis(
host="foobar.com",
port=6379,
password="foobar"
)
cache = await aioredis.create_redis_pool({"address": "..."})
app.config['SESSION_REDIS'] = cache
Session(app)
```
#### Trio
By default, Quart-session creates a single connection to Redis, while
the example above creates a connection pool.
#### Trio support
Quart-Session comes with [an (experimental) Redis client](quart_session/redis_trio) for use with the [Trio](https://trio.readthedocs.io/en/stable/) eventloop.
@@ -91,7 +76,7 @@ app.config['SESSION_TYPE'] = 'redis'
Session(app)
```
### Memcached
### Memcached support
via `aiomcache`.
@@ -101,23 +86,11 @@ 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`
for session data while Quart-Session uses [a JSON serializer](https://gitlab.com/pgjones/quart/blob/37e249b9b146824a8668eaa1daa12392aeb00256/src/quart/json/tag.py#L141)
capable of serializing the usual JSON types, as well as: `Tuple`, `Bytes`,
for session data, Quart-Session opts for a JSON serializer capable of
(de)serializing the usual JSON types, as well as: `Tuple`, `Bytes`,
`Markup`, `UUID`, and `DateTime`.
JSON as session data allows for greater interoperability with other
@@ -139,70 +112,86 @@ except ImportError:
app.session_interface.serialize = pickle
```
### Back-end usage
### Session control
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: SessionInterface = app.session_interface
await cache.set("random_key", "val", expiry=3600)
data = await cache.get("random_key")
```
The interface will have the `get`, `set`, and `delete` methods available (regardless of
back-end - similar to how [aiocache](https://github.com/argaen/aiocache) works).
### Performance
[flask-session](https://pypi.org/project/Flask-Session/) sets a
By default, [flask-session](https://pypi.org/project/Flask-Session/) sets a
session for each incoming request, including static files. From experience,
this often puts unneeded load on underlying session infrastructure,
this approach can put unneeded load on underlying session infrastructure,
especially in high-traffic environments.
Quart-Session only contacts the back-end when a session changed (or created). In addition,
static file serves never emit a `Set-Cookie` header. If you'd like to enable
this though, set `SESSION_STATIC_FILE` to `True`.
Quart-Session offers control over the session creation. For example, often you'll only need to create a session when
a user successfully logs in.
### Session pinning
Associates an user's session to his/her IP address. This mitigates cookie stealing via XSS etc, and is handy
for web applications that require extra security.
To enable this behaviour, set `SESSION_EXPLICIT` to `True`.
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_PROTECTION'] = True
app.config['SESSION_EXPLICIT'] = True
Session(app)
@app.route('/')
async def root():
if session.get('authenticated'):
return "Welcome back!"
return "Welcome anonymous!"
@app.route('/login')
async def login():
session["authenticated"] = True
session.dirty() # mark session for saving
return 'Logged in!'
app.run()
```
To re-gain the old behaviour of always emitting a `Set-Cookie` header on static file serves,
set `SESSION_STATIC_FILE` to `True`.
### Session hijack prevention
(Optionally) pins an user's session to his/her IP address. This mitigates cookie stealing via XSS etc, and is handy
for paranoid web applications.
```python3
app = Quart(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_HIJACK_PROTECTION'] = True
Session(app)
```
Session reuse from a different IP will now result in the creation of a new session, and the deletion of the old.
With this option, session reuse from a different IP will result in the
creation of a new session, and the deletion of the old.
**Important:** If your application is behind a reverse proxy, it most
likely provides the `X-Forwarded-For` header which you **must** make use of
by explicitly setting `SESSION_REVERSE_PROXY` to `True`.
by explicitly setting `SESSION_HIJACK_REVERSE_PROXY` to `True`.
## Future development
The following session interfaces would be nice to have:
- `MongoDBSessionInterface`
- `FileSystemSessionInterface`
- `GoogleCloudDatastoreSessionInterface`
- Pytest
## Flask-Session
Other to-do's:
- Unit testing
- Documentation (Sphinx)
## Migrating from Flask
This library works very similarly to [flask-session](https://pypi.org/project/Flask-Session/).
The changes are specified below:
The `quart_session.sessions` APIs are not 100% the same, but unless you
are embedded in Flask-Session's internals, a migration should be fairly
straightforward. The distinct changes are specified below:
- Quart-Session does not emit a `Set-Cookie` on every request.
- Quart-Session does not emit a `Set-Cookie` on static file serves.
- Quart-Session does not `Set-Cookie` on (static) files by default.
- Quart-Session might not have all the back-end interfaces implemented (yet), such as "filesystem".
- Quart-Session uses a different serializer: `quart.json.tag.TaggedJSONSerializer` instead of `pickle`.
- Quart-Session disallows the client to supply their own made up `sid` cookie value.
- Quart-Session can do session protection.
- Quart-Session might not have all the back-end interfaces implemented (yet), such as "filesystem".
## Help
+3 -3
View File
@@ -5,7 +5,7 @@
Quart-Session demo.
:copyright: (c) 2020 by Kroket Ltd.
:copyright: (c) 2020 by dsc.
:license: BSD, see LICENSE for more details.
"""
from quart import Quart, session
@@ -21,13 +21,13 @@ Session(app)
@app.route('/set/')
async def set():
def set():
session['key'] = 'value'
return 'ok'
@app.route('/get/')
async def get():
def get():
return session.get('key', 'not set')
+8 -42
View File
@@ -6,23 +6,19 @@
Adds server session support to your application.
:copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by Kroket Ltd.
:copyright: (c) 2020 by dsc.
:license: BSD, see LICENSE for more details.
"""
__version__ = '2.1.0'
__version__ = '0.0.1'
import os
import sniffio
from typing import Optional
from quart import Quart
from .sessions import (
RedisSessionInterface,
RedisTrioSessionInterface,
MemcachedSessionInterface,
MongoDBSessionInterface,
NullSessionInterface
)
from .sessions import RedisSessionInterface, RedisTrioSessionInterface, MemcachedSessionInterface, NullSessionInterface
class Session(object):
@@ -84,8 +80,8 @@ class Session(object):
config.setdefault('SESSION_PERMANENT', True)
config.setdefault('SESSION_USE_SIGNER', False)
config.setdefault('SESSION_KEY_PREFIX', 'session:')
config.setdefault('SESSION_PROTECTION', False)
config.setdefault('SESSION_REVERSE_PROXY', False)
config.setdefault('SESSION_HIJACK_PROTECTION', False)
config.setdefault('SESSION_HIJACK_REVERSE_PROXY', False)
config.setdefault('SESSION_STATIC_FILE', False)
config.setdefault('SESSION_EXPLICIT', False)
config.setdefault('SESSION_REDIS', None)
@@ -96,18 +92,6 @@ class Session(object):
config.setdefault('SESSION_FILE_MODE', 384)
config = {k: v for k, v in config.items() if k.startswith('SESSION_')}
if isinstance(config.get("SESSION_HIJACK_PROTECTION"), bool):
app.logger.warning("Deprecation: `SESSION_HIJACK_PROTECTION` "
"has been renamed to `SESSION_PROTECTION`")
if isinstance(config.get("SESSION_HIJACK_REVERSE_PROXY"), str):
app.logger.warning("Deprecation: `SESSION_HIJACK_REVERSE_PROXY` "
"has been renamed to `SESSION_REVERSE_PROXY`")
backend_warning = f"Please specify a session backend. " \
f"Available interfaces: redis, redis+trio, " \
f"memcached, null. e.g: app.config['SESSION_TYPE'] = 'redis'"
if config['SESSION_TYPE'] == 'redis':
options = {
"redis": config['SESSION_REDIS'],
@@ -139,25 +123,7 @@ 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(
key_prefix=config['SESSION_KEY_PREFIX'],
use_signer=config['SESSION_USE_SIGNER'],
permanent=config['SESSION_PERMANENT'],
**config)
else:
raise NotImplementedError(f"No such session interface "
f"\"{config['SESSION_TYPE']}\". {backend_warning}")
session_interface = NullSessionInterface()
return session_interface
+92 -199
View File
@@ -6,16 +6,15 @@
Server-side Sessions and SessionInterfaces.
:copyright: (c) 2014 by Shipeng Feng.
:copyright: (c) 2020 by Kroket Ltd.
:copyright: (c) 2020 by dsc.
:license: BSD, see LICENSE for more details.
"""
import time
from typing import Optional
from uuid import uuid4, UUID
from typing import Any, Callable, Optional, TYPE_CHECKING
from uuid import uuid4
import asyncio
import functools
from quart import Quart, current_app
from quart import Quart
from quart.wrappers import BaseRequestWebsocket, Response
from quart.wrappers.response import FileBody
from quart.sessions import SessionInterface as QuartSessionInterface, SecureCookieSession
@@ -37,12 +36,7 @@ class ServerSideSession(SecureCookieSession):
self.permanent = permanent
if addr:
self.addr = addr
self.modified = False
def dirty(self):
current_app.logger.warning("Deprecation: `dirty()` has "
"been made obsolete. Will be "
"removed soon^tm.")
self._dirty = False
@property
def addr(self) -> str:
@@ -52,6 +46,28 @@ class ServerSideSession(SecureCookieSession):
def addr(self, value: str) -> None:
self['_addr'] = value # type: ignore
def dirty(self):
"""Marks the session to be written/saved.
.. note::
This feature only works if you have set ``SESSION_EXPLICIT``
to ``True``, at which point you'll have to explicitly mark
each session before they'll get processed and saved.
Example::
app.config['SESSION_EXPLICIT'] = True
Session(app)
@app.route('/')
def root():
session['foo'] = 'bar'
session.dirty()
return "Hello World!"
"""
self._dirty = True
class RedisSession(ServerSideSession):
pass
@@ -61,10 +77,6 @@ class MemcachedSession(ServerSideSession):
pass
class MongoDBSession(ServerSideSession):
pass
class NullSession(ServerSideSession):
pass
@@ -92,17 +104,9 @@ class SessionInterface(QuartSessionInterface):
app: Quart,
request: BaseRequestWebsocket
) -> Optional[SecureCookieSession]:
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 :-)
addr = request.headers.get('X-Forwarded-For')
if not addr:
app.logger.error("Could not grab IP from reverse proxy, "
"session protection is DISABLED!")
else:
addr = request.remote_addr
sid = request.cookies.get(app.session_cookie_name)
addr = request.headers.get('X-Forwarded-For', request.remote_addr) if \
self._config['SESSION_HIJACK_PROTECTION'] else None
options = {"sid": sid, "permanent": self.permanent, "addr": addr}
if not sid:
@@ -121,27 +125,27 @@ class SessionInterface(QuartSessionInterface):
options['sid'] = self._generate_sid()
return self.session_class(**options)
val = await self.get(key=self.key_prefix + sid, app=app)
val = await self._backend_get(app, self.key_prefix + sid)
if val is None:
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 \
data.get('_addr', addr) != addr:
await self.delete(key=self.key_prefix + sid, app=app)
prevent_hijack = self._config['SESSION_HIJACK_PROTECTION']
if prevent_hijack is True:
if self._config['SESSION_HIJACK_REVERSE_PROXY'] is True:
addr = request.headers.get('X-Forwarded-For', request.remote_addr)
else:
addr = request.remote_addr
if data.get('_addr', addr) != addr:
await self._backend_delete(app, self.key_prefix + sid)
options['sid'] = self._generate_sid()
return self.session_class(**options)
@@ -154,8 +158,9 @@ class SessionInterface(QuartSessionInterface):
session: SecureCookieSession,
response: Response
) -> None:
# prevent set-cookie on unmodified session objects
if not session.modified:
# prevent set-cookie
if self._config['SESSION_EXPLICIT'] is True and \
not session._dirty:
return
# prevent set-cookie on (static) file responses
@@ -163,47 +168,39 @@ class SessionInterface(QuartSessionInterface):
if self._config['SESSION_STATIC_FILE'] is False and \
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(cname,
await self._backend_delete(app=app, key=session_key)
response.delete_cookie(app.session_cookie_name,
domain=domain, path=path)
return
httponly = self.get_cookie_httponly(app)
samesite = self.get_cookie_samesite(app)
secure = self.get_cookie_secure(app)
expires = self.get_expiration_time(app, 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)
await self._backend_set(app=app, key=session_key, value=val)
if self.use_signer:
session_id = self._get_signer(app).sign(want_bytes(session.sid))
else:
session_id = session.sid
response.set_cookie(cname, session_id,
response.set_cookie(app.session_cookie_name, session_id,
expires=expires, httponly=httponly,
domain=domain, path=path, secure=secure, samesite=samesite)
domain=domain, path=path, secure=secure)
async def create(self, app: Quart):
raise NotImplementedError()
async def get(self, key: str, app: Quart = None):
async def _backend_get(self, app: Quart, key: str):
raise NotImplementedError()
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
async def _backend_set(self, app: Quart, key: str, value):
raise NotImplementedError()
async def delete(self, key: str, app: Quart = None):
async def _backend_delete(self, app: Quart, key: str):
raise NotImplementedError()
def _generate_sid(self) -> str:
@@ -230,7 +227,7 @@ class RedisSessionInterface(SessionInterface):
def __init__(self, redis, **kwargs):
super(RedisSessionInterface, self).__init__(**kwargs)
self.backend = redis
self.redis = redis
async def create(self, app: Quart) -> None:
"""Creates ``aioredis.Redis`` instance.
@@ -238,29 +235,22 @@ class RedisSessionInterface(SessionInterface):
.. note::
Creates a single Redis connection, you might prefer
pooling instead (see the `aioredis` documentation
for connection pool examples).
pooling instead (see ``aioredis.Redis.create_redis_pool``)
"""
if self.backend is None:
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
)
if self.redis is None:
import aioredis
self.redis = await aioredis.create_redis("redis://localhost")
async def get(self, key: str, app: Quart = None):
return await self.backend.get(key)
async def _backend_get(self, app: Quart, key: str):
return await self.redis.get(key)
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
if app and not expiry:
expiry = total_seconds(app.permanent_session_lifetime)
return await self.backend.setex(
name=key, value=value,
time=expiry)
async def _backend_set(self, app: Quart, key: str, value):
return await self.redis.setex(
key=key, value=value,
seconds=total_seconds(app.permanent_session_lifetime))
async def delete(self, key: str, app: Quart = None):
return await self.backend.delete(key)
async def _backend_delete(self, app: Quart, key: str):
return await self.redis.delete(key)
class RedisTrioSessionInterface(SessionInterface):
@@ -277,7 +267,7 @@ class RedisTrioSessionInterface(SessionInterface):
def __init__(self, redis, **kwargs):
super(RedisTrioSessionInterface, self).__init__(**kwargs)
self.backend = redis
self.redis_trio = redis
async def create(self, app: Quart) -> None:
"""Creates ``aioredis.Redis`` instance.
@@ -287,26 +277,23 @@ class RedisTrioSessionInterface(SessionInterface):
Creates a single Redis connection. Pooling not
supported yet for ``RedisTrio``.
"""
if self.backend is None:
if self.redis_trio is None:
from quart_session.redis_trio import RedisTrio
self.backend = RedisTrio()
await self.backend.connect()
self.redis_trio = RedisTrio()
await self.redis_trio.connect()
async def get(self, key: str, app: Quart = None):
data = await self.backend.get(key)
async def _backend_get(self, app: Quart, key: str):
data = await self.redis_trio.get(key)
if data:
return data.decode()
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
if app and not expiry:
expiry = total_seconds(app.permanent_session_lifetime)
return await self.backend.setex(
async def _backend_set(self, app: Quart, key: str, value):
return await self.redis_trio.setex(
key=key, value=value,
seconds=expiry)
seconds=total_seconds(app.permanent_session_lifetime))
async def delete(self, key: str, app: Quart = None):
return await self.backend.delete(key)
async def _backend_delete(self, app: Quart, key: str):
return await self.redis_trio.delete(key)
class MemcachedSessionInterface(SessionInterface):
@@ -327,14 +314,14 @@ class MemcachedSessionInterface(SessionInterface):
super(MemcachedSessionInterface, self).__init__(
key_prefix=key_prefix, use_signer=use_signer,
permanent=permanent, **kwargs)
self.backend = memcached
self.memcached = memcached
async def create(self, app: Quart) -> None:
if self.backend is None:
@asyncio.coroutine
def create(self, app: Quart) -> None:
if self.memcached is None:
import aiomcache
# 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 = asyncio.get_running_loop()
self.memcached = aiomcache.Client("127.0.0.1", 11211, loop=loop)
def _get_memcache_timeout(self, timeout):
"""
@@ -351,120 +338,26 @@ class MemcachedSessionInterface(SessionInterface):
timeout += int(time.time())
return timeout
async def get(self, key: str, app: Quart = None):
async def _backend_get(self, app: Quart, key: str):
key = key.encode()
return await self.backend.get(key)
async def set(self, key: str, value, expiry: int = None,
app: Quart = None):
if app and not expiry:
expiry = self._get_memcache_timeout(
total_seconds(app.permanent_session_lifetime))
return await self.memcached.get(key)
async def _backend_set(self, app: Quart, key: str, value):
key = key.encode()
value = value.encode()
return await self.backend.set(key=key, value=value,
expiry = self._get_memcache_timeout(total_seconds(
app.permanent_session_lifetime))
return await self.memcached.set(key=key, value=value,
exptime=expiry)
async def delete(self, key: str, app: Quart = None):
async def _backend_delete(self, app: Quart, key: str):
key = key.encode()
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)
return await self.memcached.delete(key)
class NullSessionInterface(SessionInterface):
"""This class does absolutely nothing"""
session_class = NullSession
"""Used to open a :class:`quart.sessions.NullSession` instance.
"""
def __init__(
self, key_prefix: str, use_signer: bool = False,
permanent: bool = True, **kwargs):
super(NullSessionInterface, self).__init__(
key_prefix=key_prefix, use_signer=use_signer,
permanent=permanent, **kwargs)
self.backend = None
async def create(self, app: Quart) -> None:
pass
async def get(self, key: str, app: Quart = None) -> None:
pass
async def set(self, key: str, value, expiry: int = None,
app: Quart = None) -> None:
pass
def open_session(self, app: Quart, request: BaseRequestWebsocket):
return None
+7 -15
View File
@@ -9,14 +9,11 @@ Links
`````
* `Github
<https://github.com/kroketio/quart-session>`_
<https://github.com/xmrdsc/quart-session>`_
"""
from setuptools import setup
with open('README.md') as f:
long_description = f.read()
INSTALL_REQUIRES = [
"Quart>=0.10.0"
@@ -24,25 +21,20 @@ INSTALL_REQUIRES = [
setup(
name='Quart-Session',
version='2.1.0',
url='https://github.com/kroketio/quart-session',
version='0.0.1',
url='https://github.com/xmrdsc/quart-session',
license='BSD',
author='Kroket Ltd.',
author_email='code@kroket.io',
author='dsc',
author_email='dsc@xmr.pm',
description='Adds server-side session support to your Quart application',
long_description=long_description,
long_description_content_type='text/markdown',
long_description=__doc__,
packages=['quart_session'],
zip_safe=False,
include_package_data=True,
platforms='any',
install_requires=INSTALL_REQUIRES,
tests_require=INSTALL_REQUIRES + ["asynctest", "hypothesis", "pytest", "pytest-asyncio"],
extras_require={
"dotenv": ["python-dotenv"],
"mongodb": ["motor>=2.5.1"],
"redis": ["redis>=4.4.0"]
},
extras_require={"dotenv": ["python-dotenv"]},
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',