Skip to content

Commit

Permalink
Refactor structure for maintainability
Browse files Browse the repository at this point in the history
Refactor, add cachelib
  • Loading branch information
Lxstr committed Feb 23, 2024
1 parent 519f754 commit 2bc7df1
Show file tree
Hide file tree
Showing 32 changed files with 1,183 additions and 887 deletions.
20 changes: 13 additions & 7 deletions CHANGES.rst
@@ -1,20 +1,26 @@
Version 0.7.0
------------------

Added
- Use msgpack for serialization, along with ``SESSION_SERIALIZATION_FORMAT`` to choose between ``json`` and ``msgpack``.
- Deprecated pickle. It is still available to read existing sessions, but will be removed in 1.0.0. All sessions will transfer to msgspec upon first interaction with 0.7.0.
- Prevent sid reuse on storage miss.
- Add time-to-live expiration for MongoDB.
- Add retry for SQL based storage.
- Abstraction to improve consistency between backends.
- Enforce ``PERMANENT_SESSION_LIFETIME`` as expiration consistently for all backends.
- Add logo and additional documentation.
- Add ``flask session_cleanup`` command and alternatively, ``SESSION_CLEANUP_N_REQUESTS`` for SQLAlchemy or future non-TTL backends.
- Use Vary cookie header.
- Type hints.
- Add logo and additional documentation.

Deprecated
- Deprecated pickle. It is still available to read existing sessions, but will be removed in 1.0.0. All sessions will transfer to msgspec upon first interaction with 0.7.0.
- Remove null session in favour of specific exception messages.
- Deprecate ``SESSION_USE_SIGNER``.
- Remove backend session interfaces from public API and semver.
- Deprecate FileSystemSessionInterface in favor of the broader CacheLibSessionInterface.

Fixed
- Prevent sid reuse on storage miss.
- Abstraction to improve consistency between backends.
- Enforce ``PERMANENT_SESSION_LIFETIME`` as expiration consistently for all backends.
- Use Vary cookie header as per Flask.
- Specifically include backend session interfaces in public API and document usage.


Version 0.6.0
Expand Down
9 changes: 8 additions & 1 deletion docs/api.rst
Expand Up @@ -9,4 +9,11 @@ Anything documented here is part of the public API that Flask-Session provides,
.. autoclass:: Session
:members: init_app

.. autoclass:: flask_session.sessions.ServerSideSession
.. autoclass:: flask_session.base.ServerSideSession

.. autoclass:: flask_session.redis.RedisSessionInterface
.. autoclass:: flask_session.memcached.MemcachedSessionInterface
.. autoclass:: flask_session.filesystem.FileSystemSessionInterface
.. autoclass:: flask_session.cachelib.CacheLibSessionInterface
.. autoclass:: flask_session.mongodb.MongoDBSessionInterface
.. autoclass:: flask_session.sqlalchemy.SqlAlchemySessionInterface
8 changes: 2 additions & 6 deletions docs/config_exceptions.rst
@@ -1,12 +1,7 @@
Storage exceptions
===================

For various reasons, database operations can fail. When a database operation fails, the database client will raise an Exception.

Retries
--------

Upon an Exception, Flask-Session will retry with backoff up to 3 times for SQL based storage. If the operation still fails after 3 retries, the Exception will be raised.
Only for SQL based storage, upon an exception, Flask-Session will retry with backoff up to 3 times. If the operation still fails after 3 retries, the exception will be raised.

For other storage types, the retry logic is either included or can be configured in the client setup. Refer to the client's documentation for more information.

Expand All @@ -22,6 +17,7 @@ Redis example with retries on certain errors:
ConnectionError,
TimeoutError
)
...
retry = Retry(ExponentialBackoff(), 3)
SESSION_REDIS = Redis(host='localhost', port=6379, retry=retry, retry_on_error=[BusyLoadingError, ConnectionError, TimeoutError])
Expand Down
3 changes: 2 additions & 1 deletion docs/config_flask_session.rst
Expand Up @@ -10,7 +10,8 @@ These are specific to Flask-Session.

- **redis**: RedisSessionInterface
- **memcached**: MemcachedSessionInterface
- **filesystem**: FileSystemSessionInterface
- **filesystem**: FileSystemSessionInterface (Deprecated in 0.7.0, will be removed in 1.0.0 in favor of CacheLibSessionInterface)
- **cachelib**: CacheLibSessionInterface
- **mongodb**: MongoDBSessionInterface
- **sqlalchemy**: SqlAlchemySessionInterface

Expand Down
16 changes: 16 additions & 0 deletions docs/config_storage.rst
Expand Up @@ -43,6 +43,22 @@ FileSystem

Default: ``0600``

.. deprecated:: 0.7.0
``SESSION_FILE_MODE``, ``SESSION_FILE_THRESHOLD`` and ``SESSION_FILE_DIR``. Use ``SESSION_CACHELIB`` instead.

Cachelib
~~~~~~~~~~~~~~~~~~~~~~~
.. py:data:: SESSION_CACHELIB
Any valid `cachelib backend <https://cachelib.readthedocs.io/en/stable/>`_. This allows you maximum flexibility in choosing the cache backend and its configuration.

The following would set a cache directory called "flask_session" and a threshold of 500 items before it starts deleting some.

.. code-block:: python
app.config['SESSION_CACHELIB'] = FileSystemCache(cache_dir='flask_session', threshold=500)
Default: ``FileSystemCache`` in ``./flask_session`` directory.

MongoDB
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Expand Up @@ -6,7 +6,7 @@ Table of Contents

introduction
installation
quickstart
usage
config
api
contributing
Expand Down
51 changes: 46 additions & 5 deletions docs/installation.rst
Expand Up @@ -12,7 +12,7 @@ Flask-Session's only required dependency is msgspec for serialization, which has

.. note::

You need to choose a storage type and install an appropriate client library, unless you are using the FileSystemCache.
You need to choose a storage type and install an appropriate client library.

For example, if you want to use Redis as your storage, you will need to install the redis-py client library:

Expand All @@ -22,8 +22,10 @@ For example, if you want to use Redis as your storage, you will need to install
Redis is the recommended storage type for Flask-Session, as it has the most complete support for the features of Flask-Session with minimal configuration.

Supported storage and client libraries:
Support
--------

Directly supported storage and client libraries:

.. list-table::
:header-rows: 1
Expand All @@ -33,17 +35,56 @@ Supported storage and client libraries:
* - Redis
- redis-py_
* - Memcached
- pylibmc_, python-memcached_, pymemcache_
- pylibmc_, python-memcached_ or pymemcache_
* - MongoDB
- pymongo_
* - SQL Alchemy
- flask-sqlalchemy_

Other clients may work if they use the same commands as the ones listed above.

Cachelib
--------

Flask-Session also indirectly supports storage and client libraries via cachelib_, which is a wrapper around various cache libraries and subject to change. You must also install cachelib_ itself to use these.

.. warning::

As of writing, cachelib_ still use pickle_ as the default serializer, which may have security implications.

Using cachlib :class:`FileSystemCache`` or :class:`SimpleCache` may be useful for development.

.. list-table::
:header-rows: 1

* - Storage
- Client Library
* - File System
- Not required
* - Simple Memory
- Not required
* - UWSGI
- uwsgi_
* - Redis
- redis-py_
* - Memcached
- pylibmc_, memcached, libmc_ or `google.appengine.api.memcached`_
* - MongoDB
- pymongo_
* - DynamoDB
- boto3_



.. _redis-py: https://github.com/andymccurdy/redis-py
.. _pylibmc: http://sendapatch.se/projects/pylibmc/
.. _python-memcached: https://github.com/linsomniac/python-memcached
.. _pymemcache: https://github.com/pinterest/pymemcache
.. _pymongo: http://api.mongodb.org/python/current/index.html
.. _Flask-SQLAlchemy: https://github.com/pallets-eco/flask-sqlalchemy
.. _pymongo: https://pymongo.readthedocs.io/en/stable
.. _Flask-SQLAlchemy: https://github.com/pallets-eco/flask-sqlalchemy
.. _cachelib: https://cachelib.readthedocs.io/en/stable/
.. _google.appengine.api.memcached: https://cloud.google.com/appengine/docs/legacy/standard/python/memcache
.. _boto3: https://boto3.amazonaws.com/v1/documentation/api/latest/index.html
.. _libmc: https://github.com/douban/libmc
.. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html
.. _pickle: https://docs.python.org/3/library/pickle
37 changes: 31 additions & 6 deletions docs/quickstart.rst → docs/usage.rst
@@ -1,6 +1,9 @@
Quick Start
Usage
===========

Quickstart
-----------

.. currentmodule:: flask_session


Expand All @@ -11,15 +14,15 @@ then create the :class:`Session` object by passing it the application.

You can not use ``Session`` instance directly, what ``Session`` does
is just change the :attr:`~flask.Flask.session_interface` attribute on
your Flask applications. You should always use :class:`flask.session`.
your Flask applications. You should always use :class:`flask.session` when accessing the current session.

.. code-block:: python
from flask import Flask, session
from flask_session import Session
app = Flask(__name__)
# Check Configuration section for more details
SESSION_TYPE = 'redis'
app.config.from_object(__name__)
Session(app)
Expand All @@ -33,10 +36,32 @@ then create the :class:`Session` object by passing it the application.
def get():
return session.get('key', 'not set')
You may also set up your application later using :meth:`~Session.init_app`
method.
This would automatically setup a redis client connected to `localhost:6379` and use it to store the session data.

.. code-block:: python
See the `configuration section <config.rst>`_ for more details.

Alternative initialization
---------------------------

Rather than calling `Session(app)`, you may initialize later using :meth:`~Session.init_app`.

.. code-block:: python
sess = Session()
sess.init_app(app)
Or, if you prefer to directly set parameters rather than using the configuration constants, you can initialize by setting the interface constructor directly to the :attr:`session_interface`.

.. code-block:: python
from flask_session.implementations.redis import RedisSessionInterface
...
redis = Redis(
host='localhost',
port=6379,
)
app.session_interface = RedisSessionInterface(
client=redis,
)
38 changes: 27 additions & 11 deletions src/flask_session/__init__.py
@@ -1,11 +1,4 @@
from .defaults import Defaults
from .sessions import (
FileSystemSessionInterface,
MemcachedSessionInterface,
MongoDBSessionInterface,
RedisSessionInterface,
SqlAlchemySessionInterface,
)

__version__ = "0.6.0rc1"

Expand All @@ -14,6 +7,8 @@ class Session:
"""This class is used to add Server-side Session to one or more Flask
applications.
:param app: A Flask app instance.
For a typical setup use the following initialization::
app = Flask(__name__)
Expand Down Expand Up @@ -50,10 +45,11 @@ def _get_interface(self, app):

# Flask-session specific settings
SESSION_TYPE = config.get("SESSION_TYPE", Defaults.SESSION_TYPE)

SESSION_PERMANENT = config.get("SESSION_PERMANENT", Defaults.SESSION_PERMANENT)
SESSION_USE_SIGNER = config.get(
"SESSION_USE_SIGNER", Defaults.SESSION_USE_SIGNER
)
) # TODO: remove in 1.0
SESSION_KEY_PREFIX = config.get(
"SESSION_KEY_PREFIX", Defaults.SESSION_KEY_PREFIX
)
Expand All @@ -70,7 +66,11 @@ def _get_interface(self, app):
# Memcached settings
SESSION_MEMCACHED = config.get("SESSION_MEMCACHED", Defaults.SESSION_MEMCACHED)

# CacheLib settings
SESSION_CACHELIB = config.get("SESSION_CACHELIB", Defaults.SESSION_CACHELIB)

# Filesystem settings
# TODO: remove in 1.0
SESSION_FILE_DIR = config.get("SESSION_FILE_DIR", Defaults.SESSION_FILE_DIR)
SESSION_FILE_THRESHOLD = config.get(
"SESSION_FILE_THRESHOLD", Defaults.SESSION_FILE_THRESHOLD
Expand Down Expand Up @@ -107,7 +107,6 @@ def _get_interface(self, app):
)

common_params = {
"app": app,
"key_prefix": SESSION_KEY_PREFIX,
"use_signer": SESSION_USE_SIGNER,
"permanent": SESSION_PERMANENT,
Expand All @@ -116,33 +115,50 @@ def _get_interface(self, app):
}

if SESSION_TYPE == "redis":
from .redis import RedisSessionInterface

session_interface = RedisSessionInterface(
**common_params,
redis=SESSION_REDIS,
client=SESSION_REDIS,
)
elif SESSION_TYPE == "memcached":
from .memcached import MemcachedSessionInterface

session_interface = MemcachedSessionInterface(
**common_params,
client=SESSION_MEMCACHED,
)
elif SESSION_TYPE == "filesystem":
from .filesystem import FileSystemSessionInterface

session_interface = FileSystemSessionInterface(
**common_params,
cache_dir=SESSION_FILE_DIR,
threshold=SESSION_FILE_THRESHOLD,
mode=SESSION_FILE_MODE,
)
elif SESSION_TYPE == "cachelib":
from .cachelib import CacheLibSessionInterface

session_interface = CacheLibSessionInterface(
**common_params, client=SESSION_CACHELIB
)
elif SESSION_TYPE == "mongodb":
from .mongodb import MongoDBSessionInterface

session_interface = MongoDBSessionInterface(
**common_params,
client=SESSION_MONGODB,
db=SESSION_MONGODB_DB,
collection=SESSION_MONGODB_COLLECT,
)
elif SESSION_TYPE == "sqlalchemy":
from .sqlalchemy import SqlAlchemySessionInterface

session_interface = SqlAlchemySessionInterface(
app=app,
**common_params,
db=SESSION_SQLALCHEMY,
client=SESSION_SQLALCHEMY,
table=SESSION_SQLALCHEMY_TABLE,
sequence=SESSION_SQLALCHEMY_SEQUENCE,
schema=SESSION_SQLALCHEMY_SCHEMA,
Expand Down
5 changes: 5 additions & 0 deletions src/flask_session/_utils.py
Expand Up @@ -21,13 +21,18 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import time
from functools import wraps
from typing import Any, Callable

from flask import current_app


def total_seconds(timedelta):
return int(timedelta.total_seconds())


def retry_query(
*, max_attempts: int = 3, delay: float = 0.3, backoff: int = 2
) -> Callable[..., Any]:
Expand Down

0 comments on commit 2bc7df1

Please sign in to comment.