Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 08ee20ac07 | |||
| 6a51c15dbf | |||
| df20bbff18 | |||
| 499e46c93d |
@@ -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,5 +1,5 @@
|
|||||||
Copyright (c) 2014 by Shipeng Feng.
|
Copyright (c) 2014 by Shipeng Feng.
|
||||||
Copyright (c) 2020 by Kroket Ltd.
|
Copyright (c) 2020 by dsc.
|
||||||
|
|
||||||
Some rights reserved.
|
Some rights reserved.
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ recursive-include quart_session *.md
|
|||||||
exclude .gitlab-ci.yml
|
exclude .gitlab-ci.yml
|
||||||
exclude examples
|
exclude examples
|
||||||
exclude docs
|
exclude docs
|
||||||
exclude venv
|
|
||||||
@@ -2,21 +2,22 @@
|
|||||||
|
|
||||||
 [](https://pypi.org/project/Quart-Session/) 
|
 [](https://pypi.org/project/Quart-Session/) 
|
||||||
|
|
||||||
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.
|
server-side sessions to your application.
|
||||||
|
|
||||||
Based on [flask-session](https://pypi.org/project/Flask-Session/).
|
Based on [flask-session](https://pypi.org/project/Flask-Session/).
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Quart-Session can be installed via pipenv or pip,
|
Quart-Session can be installed via pipenv or
|
||||||
|
pip,
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ pipenv install quart-session
|
$ pipenv install quart-session
|
||||||
$ pip 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
|
```python3
|
||||||
from quart import Quart, session
|
from quart import Quart, session
|
||||||
@@ -29,11 +30,7 @@ Session(app)
|
|||||||
@app.route('/')
|
@app.route('/')
|
||||||
async def hello():
|
async def hello():
|
||||||
session["foo"] = "bar"
|
session["foo"] = "bar"
|
||||||
return "session key 'foo' set"
|
return 'hello'
|
||||||
|
|
||||||
@app.route('/foo')
|
|
||||||
async def foo():
|
|
||||||
return session.get("foo", "session key 'foo' not found")
|
|
||||||
|
|
||||||
app.run()
|
app.run()
|
||||||
```
|
```
|
||||||
@@ -41,9 +38,9 @@ app.run()
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
|
||||||
### Redis
|
### Redis support
|
||||||
|
|
||||||
via `redis>=4.4.0`.
|
via `aioredis`.
|
||||||
|
|
||||||
```python3
|
```python3
|
||||||
app = Quart(__name__)
|
app = Quart(__name__)
|
||||||
@@ -51,16 +48,8 @@ app.config['SESSION_TYPE'] = 'redis'
|
|||||||
Session(app)
|
Session(app)
|
||||||
```
|
```
|
||||||
|
|
||||||
By default, Quart-session connects to Redis at `127.0.0.1:6379`. If you
|
If you already have a `aioredis.Client` instance and you'd like to share
|
||||||
have a different location, use `SESSION_URI`
|
it with the session interface,
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
```python3
|
```python3
|
||||||
app = Quart(__name__)
|
app = Quart(__name__)
|
||||||
@@ -68,17 +57,15 @@ app.config['SESSION_TYPE'] = 'redis'
|
|||||||
|
|
||||||
@app.before_serving
|
@app.before_serving
|
||||||
async def setup():
|
async def setup():
|
||||||
cache = await aioredis.Redis(
|
cache = await aioredis.create_redis_pool(...)
|
||||||
host="foobar.com",
|
|
||||||
port=6379,
|
|
||||||
password="foobar"
|
|
||||||
)
|
|
||||||
|
|
||||||
app.config['SESSION_REDIS'] = cache
|
app.config['SESSION_REDIS'] = cache
|
||||||
Session(app)
|
Session(app)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Trio
|
By default, Quart-session creates a single connection to Redis, while
|
||||||
|
the example above sets up 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.
|
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 +78,7 @@ app.config['SESSION_TYPE'] = 'redis'
|
|||||||
Session(app)
|
Session(app)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Memcached
|
### Memcached support
|
||||||
|
|
||||||
via `aiomcache`.
|
via `aiomcache`.
|
||||||
|
|
||||||
@@ -101,23 +88,11 @@ 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`
|
||||||
for session data while Quart-Session uses [a JSON serializer](https://gitlab.com/pgjones/quart/blob/37e249b9b146824a8668eaa1daa12392aeb00256/src/quart/json/tag.py#L141)
|
for session data, Quart-Session opts for a JSON serializer capable of
|
||||||
capable of serializing the usual JSON types, as well as: `Tuple`, `Bytes`,
|
(de)serializing the usual JSON types, as well as: `Tuple`, `Bytes`,
|
||||||
`Markup`, `UUID`, and `DateTime`.
|
`Markup`, `UUID`, and `DateTime`.
|
||||||
|
|
||||||
JSON as session data allows for greater interoperability with other
|
JSON as session data allows for greater interoperability with other
|
||||||
@@ -139,44 +114,52 @@ except ImportError:
|
|||||||
app.session_interface.serialize = pickle
|
app.session_interface.serialize = pickle
|
||||||
```
|
```
|
||||||
|
|
||||||
### Back-end usage
|
### Session control
|
||||||
|
|
||||||
At any point you may interface with the session back-end directly:
|
By default, [flask-session](https://pypi.org/project/Flask-Session/) sets a
|
||||||
|
|
||||||
```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
|
|
||||||
session for each incoming request, including static files. From experience,
|
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.
|
especially in high-traffic environments.
|
||||||
|
|
||||||
Quart-Session only contacts the back-end when a session changed (or created). In addition,
|
Quart-Session offers control over the session creation. For example, often you'll only need to create a session when
|
||||||
static file serves never emit a `Set-Cookie` header. If you'd like to enable
|
a user successfully logs in.
|
||||||
this though, set `SESSION_STATIC_FILE` to `True`.
|
|
||||||
|
To enable this behaviour, set `SESSION_EXPLICIT` to `True`.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
app = Quart(__name__)
|
||||||
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
|
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 pinning
|
### Session pinning
|
||||||
|
|
||||||
Associates an user's session to his/her IP address. This mitigates cookie stealing via XSS etc, and is handy
|
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.
|
for paranoid web applications.
|
||||||
|
|
||||||
```python3
|
```python3
|
||||||
app = Quart(__name__)
|
app = Quart(__name__)
|
||||||
app.config['SESSION_TYPE'] = 'redis'
|
app.config['SESSION_TYPE'] = 'redis'
|
||||||
app.config['SESSION_PROTECTION'] = True
|
app.config['SESSION_HIJACK_PROTECTION'] = True
|
||||||
Session(app)
|
Session(app)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -184,25 +167,32 @@ Session reuse from a different IP will now result in the creation of a new sessi
|
|||||||
|
|
||||||
**Important:** If your application is behind a reverse proxy, it most
|
**Important:** If your application is behind a reverse proxy, it most
|
||||||
likely provides the `X-Forwarded-For` header which you **must** make use of
|
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
|
## Future development
|
||||||
|
|
||||||
|
The following session interfaces would be nice to have:
|
||||||
|
|
||||||
|
- `MongoDBSessionInterface`
|
||||||
- `FileSystemSessionInterface`
|
- `FileSystemSessionInterface`
|
||||||
- `GoogleCloudDatastoreSessionInterface`
|
- `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/).
|
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 `Set-Cookie` on (static) files by default.
|
||||||
- Quart-Session does not emit a `Set-Cookie` on static file serves.
|
- 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 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 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
|
## Help
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
Quart-Session demo.
|
Quart-Session demo.
|
||||||
|
|
||||||
:copyright: (c) 2020 by Kroket Ltd.
|
:copyright: (c) 2020 by dsc.
|
||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
from quart import Quart, session
|
from quart import Quart, session
|
||||||
@@ -21,13 +21,13 @@ Session(app)
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/set/')
|
@app.route('/set/')
|
||||||
async def set():
|
def set():
|
||||||
session['key'] = 'value'
|
session['key'] = 'value'
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
|
|
||||||
@app.route('/get/')
|
@app.route('/get/')
|
||||||
async def get():
|
def get():
|
||||||
return session.get('key', 'not set')
|
return session.get('key', 'not set')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
[project]
|
|
||||||
name = "quart-session"
|
|
||||||
version = "3.0.1"
|
|
||||||
description = "Add your description here"
|
|
||||||
readme = "README.md"
|
|
||||||
requires-python = ">=3.13"
|
|
||||||
dependencies = []
|
|
||||||
@@ -6,23 +6,18 @@
|
|||||||
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 Kroket Ltd.
|
:copyright: (c) 2020 by dsc.
|
||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = '3.0.0'
|
__version__ = '0.0.1'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from quart import Quart
|
from quart import Quart
|
||||||
|
|
||||||
from .sessions import (
|
from .sessions import RedisSessionInterface, RedisTrioSessionInterface, MemcachedSessionInterface, NullSessionInterface
|
||||||
RedisSessionInterface,
|
|
||||||
RedisTrioSessionInterface,
|
|
||||||
MemcachedSessionInterface,
|
|
||||||
MongoDBSessionInterface,
|
|
||||||
NullSessionInterface
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Session(object):
|
class Session(object):
|
||||||
@@ -84,8 +79,8 @@ class Session(object):
|
|||||||
config.setdefault('SESSION_PERMANENT', True)
|
config.setdefault('SESSION_PERMANENT', True)
|
||||||
config.setdefault('SESSION_USE_SIGNER', False)
|
config.setdefault('SESSION_USE_SIGNER', False)
|
||||||
config.setdefault('SESSION_KEY_PREFIX', 'session:')
|
config.setdefault('SESSION_KEY_PREFIX', 'session:')
|
||||||
config.setdefault('SESSION_PROTECTION', False)
|
config.setdefault('SESSION_HIJACK_PROTECTION', False)
|
||||||
config.setdefault('SESSION_REVERSE_PROXY', False)
|
config.setdefault('SESSION_HIJACK_REVERSE_PROXY', False)
|
||||||
config.setdefault('SESSION_STATIC_FILE', False)
|
config.setdefault('SESSION_STATIC_FILE', False)
|
||||||
config.setdefault('SESSION_EXPLICIT', False)
|
config.setdefault('SESSION_EXPLICIT', False)
|
||||||
config.setdefault('SESSION_REDIS', None)
|
config.setdefault('SESSION_REDIS', None)
|
||||||
@@ -96,18 +91,6 @@ class Session(object):
|
|||||||
config.setdefault('SESSION_FILE_MODE', 384)
|
config.setdefault('SESSION_FILE_MODE', 384)
|
||||||
config = {k: v for k, v in config.items() if k.startswith('SESSION_')}
|
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':
|
if config['SESSION_TYPE'] == 'redis':
|
||||||
options = {
|
options = {
|
||||||
"redis": config['SESSION_REDIS'],
|
"redis": config['SESSION_REDIS'],
|
||||||
@@ -139,25 +122,7 @@ 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':
|
|
||||||
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:
|
else:
|
||||||
raise NotImplementedError(f"No such session interface "
|
session_interface = NullSessionInterface()
|
||||||
f"\"{config['SESSION_TYPE']}\". {backend_warning}")
|
|
||||||
|
|
||||||
return session_interface
|
return session_interface
|
||||||
|
|||||||
+93
-200
@@ -6,16 +6,15 @@
|
|||||||
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 Kroket Ltd.
|
:copyright: (c) 2020 by dsc.
|
||||||
: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 Any, Callable, Optional, TYPE_CHECKING
|
||||||
from uuid import uuid4, UUID
|
from uuid import uuid4
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
|
||||||
|
|
||||||
from quart import Quart, current_app
|
from quart import Quart
|
||||||
from quart.wrappers import BaseRequestWebsocket, Response
|
from quart.wrappers import BaseRequestWebsocket, Response
|
||||||
from quart.wrappers.response import FileBody
|
from quart.wrappers.response import FileBody
|
||||||
from quart.sessions import SessionInterface as QuartSessionInterface, SecureCookieSession
|
from quart.sessions import SessionInterface as QuartSessionInterface, SecureCookieSession
|
||||||
@@ -31,18 +30,13 @@ 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
|
||||||
if addr:
|
if addr:
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
self.modified = False
|
self._dirty = False
|
||||||
|
|
||||||
def dirty(self):
|
|
||||||
current_app.logger.warning("Deprecation: `dirty()` has "
|
|
||||||
"been made obsolete. Will be "
|
|
||||||
"removed soon^tm.")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def addr(self) -> str:
|
def addr(self) -> str:
|
||||||
@@ -52,6 +46,28 @@ class ServerSideSession(SecureCookieSession):
|
|||||||
def addr(self, value: str) -> None:
|
def addr(self, value: str) -> None:
|
||||||
self['_addr'] = value # type: ignore
|
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):
|
class RedisSession(ServerSideSession):
|
||||||
pass
|
pass
|
||||||
@@ -61,10 +77,6 @@ class MemcachedSession(ServerSideSession):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MongoDBSession(ServerSideSession):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NullSession(ServerSideSession):
|
class NullSession(ServerSideSession):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -92,17 +104,9 @@ class SessionInterface(QuartSessionInterface):
|
|||||||
app: Quart,
|
app: Quart,
|
||||||
request: BaseRequestWebsocket
|
request: BaseRequestWebsocket
|
||||||
) -> Optional[SecureCookieSession]:
|
) -> Optional[SecureCookieSession]:
|
||||||
cname = app.config.get('SESSION_COOKIE_NAME', 'session')
|
sid = request.cookies.get(app.session_cookie_name)
|
||||||
sid = request.cookies.get(cname)
|
addr = request.headers.get('X-Forwarded-For', request.remote_addr) if \
|
||||||
if self._config['SESSION_REVERSE_PROXY'] is True:
|
self._config['SESSION_HIJACK_PROTECTION'] else None
|
||||||
# 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
|
|
||||||
options = {"sid": sid, "permanent": self.permanent, "addr": addr}
|
options = {"sid": sid, "permanent": self.permanent, "addr": addr}
|
||||||
|
|
||||||
if not sid:
|
if not sid:
|
||||||
@@ -121,27 +125,27 @@ class SessionInterface(QuartSessionInterface):
|
|||||||
options['sid'] = self._generate_sid()
|
options['sid'] = self._generate_sid()
|
||||||
return self.session_class(**options)
|
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:
|
if val is None:
|
||||||
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:
|
||||||
app.logger.warning(f"Failed to deserialize session "
|
app.logger.warning(f"Failed to deserialize session "
|
||||||
f"data for sid: {sid}. Generating new sid.")
|
f"data for sid: {sid}. Generating new sid.")
|
||||||
app.logger.debug(f"data: {val}")
|
|
||||||
options['sid'] = self._generate_sid()
|
options['sid'] = self._generate_sid()
|
||||||
return self.session_class(**options)
|
return self.session_class(**options)
|
||||||
|
|
||||||
protection = self._config['SESSION_PROTECTION']
|
prevent_hijack = self._config['SESSION_HIJACK_PROTECTION']
|
||||||
if protection is True and addr is not None and \
|
if prevent_hijack is True:
|
||||||
data.get('_addr', addr) != addr:
|
if self._config['SESSION_HIJACK_REVERSE_PROXY'] is True:
|
||||||
await self.delete(key=self.key_prefix + sid, app=app)
|
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()
|
options['sid'] = self._generate_sid()
|
||||||
return self.session_class(**options)
|
return self.session_class(**options)
|
||||||
|
|
||||||
@@ -154,8 +158,9 @@ class SessionInterface(QuartSessionInterface):
|
|||||||
session: SecureCookieSession,
|
session: SecureCookieSession,
|
||||||
response: Response
|
response: Response
|
||||||
) -> None:
|
) -> None:
|
||||||
# prevent set-cookie on unmodified session objects
|
# prevent set-cookie
|
||||||
if not session.modified:
|
if self._config['SESSION_EXPLICIT'] is True and \
|
||||||
|
not session._dirty:
|
||||||
return
|
return
|
||||||
|
|
||||||
# prevent set-cookie on (static) file responses
|
# prevent set-cookie on (static) file responses
|
||||||
@@ -163,47 +168,39 @@ class SessionInterface(QuartSessionInterface):
|
|||||||
if self._config['SESSION_STATIC_FILE'] is False and \
|
if self._config['SESSION_STATIC_FILE'] is False and \
|
||||||
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._backend_delete(app=app, key=session_key)
|
||||||
response.delete_cookie(cname,
|
response.delete_cookie(app.session_cookie_name,
|
||||||
domain=domain, path=path)
|
domain=domain, path=path)
|
||||||
return
|
return
|
||||||
httponly = self.get_cookie_httponly(app)
|
httponly = self.get_cookie_httponly(app)
|
||||||
samesite = self.get_cookie_samesite(app)
|
|
||||||
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._backend_set(app=app, key=session_key, value=val)
|
||||||
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(cname, session_id.decode('utf-8'),
|
response.set_cookie(app.session_cookie_name, session_id,
|
||||||
expires=expires, httponly=httponly,
|
expires=expires, httponly=httponly,
|
||||||
domain=domain, path=path, secure=secure, samesite=samesite)
|
domain=domain, path=path, secure=secure)
|
||||||
|
|
||||||
async def create(self, app: Quart):
|
async def create(self, app: Quart):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get(self, key: str, app: Quart = None):
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def set(self, key: str, value, expiry: int = None,
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
app: Quart = None):
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def delete(self, key: str, app: Quart = None):
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _generate_sid(self) -> str:
|
def _generate_sid(self) -> str:
|
||||||
@@ -230,7 +227,7 @@ class RedisSessionInterface(SessionInterface):
|
|||||||
|
|
||||||
def __init__(self, redis, **kwargs):
|
def __init__(self, redis, **kwargs):
|
||||||
super(RedisSessionInterface, self).__init__(**kwargs)
|
super(RedisSessionInterface, self).__init__(**kwargs)
|
||||||
self.backend = redis
|
self.redis = redis
|
||||||
|
|
||||||
async def create(self, app: Quart) -> None:
|
async def create(self, app: Quart) -> None:
|
||||||
"""Creates ``aioredis.Redis`` instance.
|
"""Creates ``aioredis.Redis`` instance.
|
||||||
@@ -238,29 +235,22 @@ class RedisSessionInterface(SessionInterface):
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Creates a single Redis connection, you might prefer
|
Creates a single Redis connection, you might prefer
|
||||||
pooling instead (see the `aioredis` documentation
|
pooling instead (see ``aioredis.Redis.create_redis_pool``)
|
||||||
for connection pool examples).
|
|
||||||
"""
|
"""
|
||||||
if self.backend is None:
|
if self.redis is None:
|
||||||
from redis import asyncio as aioredis
|
import aioredis
|
||||||
uri = self._config.get('SESSION_URI', 'redis://localhost')
|
self.redis = await aioredis.create_redis("redis://localhost")
|
||||||
self.backend = await aioredis.from_url(
|
|
||||||
uri, encoding="utf-8", decode_responses=True
|
|
||||||
)
|
|
||||||
|
|
||||||
async def get(self, key: str, app: Quart = None):
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
return await self.backend.get(key)
|
return await self.redis.get(key)
|
||||||
|
|
||||||
async def set(self, key: str, value, expiry: int = None,
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
app: Quart = None):
|
return await self.redis.setex(
|
||||||
if app and not expiry:
|
key=key, value=value,
|
||||||
expiry = total_seconds(app.permanent_session_lifetime)
|
seconds=total_seconds(app.permanent_session_lifetime))
|
||||||
return await self.backend.setex(
|
|
||||||
name=key, value=value,
|
|
||||||
time=expiry)
|
|
||||||
|
|
||||||
async def delete(self, key: str, app: Quart = None):
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
return await self.backend.delete(key)
|
return await self.redis.delete(key)
|
||||||
|
|
||||||
|
|
||||||
class RedisTrioSessionInterface(SessionInterface):
|
class RedisTrioSessionInterface(SessionInterface):
|
||||||
@@ -277,7 +267,7 @@ class RedisTrioSessionInterface(SessionInterface):
|
|||||||
|
|
||||||
def __init__(self, redis, **kwargs):
|
def __init__(self, redis, **kwargs):
|
||||||
super(RedisTrioSessionInterface, self).__init__(**kwargs)
|
super(RedisTrioSessionInterface, self).__init__(**kwargs)
|
||||||
self.backend = redis
|
self.redis_trio = redis
|
||||||
|
|
||||||
async def create(self, app: Quart) -> None:
|
async def create(self, app: Quart) -> None:
|
||||||
"""Creates ``aioredis.Redis`` instance.
|
"""Creates ``aioredis.Redis`` instance.
|
||||||
@@ -287,26 +277,23 @@ class RedisTrioSessionInterface(SessionInterface):
|
|||||||
Creates a single Redis connection. Pooling not
|
Creates a single Redis connection. Pooling not
|
||||||
supported yet for ``RedisTrio``.
|
supported yet for ``RedisTrio``.
|
||||||
"""
|
"""
|
||||||
if self.backend is None:
|
if self.redis_trio is None:
|
||||||
from quart_session.redis_trio import RedisTrio
|
from quart_session.redis_trio import RedisTrio
|
||||||
self.backend = RedisTrio()
|
self.redis_trio = RedisTrio()
|
||||||
await self.backend.connect()
|
await self.redis_trio.connect()
|
||||||
|
|
||||||
async def get(self, key: str, app: Quart = None):
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
data = await self.backend.get(key)
|
data = await self.redis_trio.get(key)
|
||||||
if data:
|
if data:
|
||||||
return data.decode()
|
return data.decode()
|
||||||
|
|
||||||
async def set(self, key: str, value, expiry: int = None,
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
app: Quart = None):
|
return await self.redis_trio.setex(
|
||||||
if app and not expiry:
|
|
||||||
expiry = total_seconds(app.permanent_session_lifetime)
|
|
||||||
return await self.backend.setex(
|
|
||||||
key=key, value=value,
|
key=key, value=value,
|
||||||
seconds=expiry)
|
seconds=total_seconds(app.permanent_session_lifetime))
|
||||||
|
|
||||||
async def delete(self, key: str, app: Quart = None):
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
return await self.backend.delete(key)
|
return await self.redis_trio.delete(key)
|
||||||
|
|
||||||
|
|
||||||
class MemcachedSessionInterface(SessionInterface):
|
class MemcachedSessionInterface(SessionInterface):
|
||||||
@@ -327,14 +314,14 @@ class MemcachedSessionInterface(SessionInterface):
|
|||||||
super(MemcachedSessionInterface, self).__init__(
|
super(MemcachedSessionInterface, self).__init__(
|
||||||
key_prefix=key_prefix, use_signer=use_signer,
|
key_prefix=key_prefix, use_signer=use_signer,
|
||||||
permanent=permanent, **kwargs)
|
permanent=permanent, **kwargs)
|
||||||
self.backend = memcached
|
self.memcached = memcached
|
||||||
|
|
||||||
async def create(self, app: Quart) -> None:
|
@asyncio.coroutine
|
||||||
if self.backend is None:
|
def create(self, app: Quart) -> None:
|
||||||
|
if self.memcached is None:
|
||||||
import aiomcache
|
import aiomcache
|
||||||
# self.backend = aiomcache.Client("127.0.0.1", 11211)
|
loop = asyncio.get_running_loop()
|
||||||
self.backend = aiomcache.Client(self._config.get('SESSION_MEMCACHED_HOST', '127.0.0.1'),
|
self.memcached = aiomcache.Client("127.0.0.1", 11211, loop=loop)
|
||||||
self._config.get('SESSION_MEMCACHED_PORT', 11211))
|
|
||||||
|
|
||||||
def _get_memcache_timeout(self, timeout):
|
def _get_memcache_timeout(self, timeout):
|
||||||
"""
|
"""
|
||||||
@@ -351,120 +338,26 @@ class MemcachedSessionInterface(SessionInterface):
|
|||||||
timeout += int(time.time())
|
timeout += int(time.time())
|
||||||
return timeout
|
return timeout
|
||||||
|
|
||||||
async def get(self, key: str, app: Quart = None):
|
async def _backend_get(self, app: Quart, key: str):
|
||||||
key = key.encode()
|
key = key.encode()
|
||||||
return await self.backend.get(key)
|
return await self.memcached.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))
|
|
||||||
|
|
||||||
|
async def _backend_set(self, app: Quart, key: str, value):
|
||||||
key = key.encode()
|
key = key.encode()
|
||||||
value = value.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)
|
exptime=expiry)
|
||||||
|
|
||||||
async def delete(self, key: str, app: Quart = None):
|
async def _backend_delete(self, app: Quart, key: str):
|
||||||
key = key.encode()
|
key = key.encode()
|
||||||
return await self.backend.delete(key)
|
return await self.memcached.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"""
|
"""Used to open a :class:`quart.sessions.NullSession` instance.
|
||||||
session_class = NullSession
|
"""
|
||||||
|
|
||||||
def __init__(
|
def open_session(self, app: Quart, request: BaseRequestWebsocket):
|
||||||
self, key_prefix: str, use_signer: bool = False,
|
return None
|
||||||
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
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Links
|
|||||||
`````
|
`````
|
||||||
|
|
||||||
* `Github
|
* `Github
|
||||||
<https://github.com/kroketio/quart-session>`_
|
<https://github.com/xmrdsc/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.19.0"
|
"Quart>=0.10.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Quart-Session',
|
name='Quart-Session',
|
||||||
version='3.0.1',
|
version='0.0.1',
|
||||||
url='https://github.com/kroketio/quart-session',
|
url='https://github.com/xmrdsc/quart-session',
|
||||||
license='BSD',
|
license='BSD',
|
||||||
author='Kroket Ltd.',
|
author='dsc',
|
||||||
author_email='code@kroket.io',
|
author_email='dsc@xmr.pm',
|
||||||
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',
|
||||||
@@ -38,11 +38,7 @@ setup(
|
|||||||
platforms='any',
|
platforms='any',
|
||||||
install_requires=INSTALL_REQUIRES,
|
install_requires=INSTALL_REQUIRES,
|
||||||
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": ["redis>=4.4.0"]
|
|
||||||
},
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
|||||||
Reference in New Issue
Block a user