diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 8be17b23..e7884931 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -21,6 +21,7 @@ - [kim-sondrup](https://github.com/kim-sondrup) - [bnjmn](https://github.com/bnjmn) - [christopherpickering](https://github.com/christopherpickering) +- [jlgoolsbee](https://github.com/jlgoolsbee) ## Original Author diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 9fef8fd5..7c19fe3c 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -175,6 +175,12 @@ SqlAlchemy Default: ``'sessions'`` +.. py:data:: SESSION_SQLALCHEMY_TABLE_EXISTS + + Whether (or not) the table for storing session data is managed by libraries (e.g. Flask-Migrate) or other means outside of Flask-Session. When set to ``True``, Flask-Session will not try to create the session table. + + Default: ``False`` + .. py:data:: SESSION_SQLALCHEMY_SEQUENCE The name of the sequence you want to use for the primary key. diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index 5d4caee9..884de9f4 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -91,6 +91,9 @@ def _get_interface(self, app): SESSION_SQLALCHEMY_TABLE = config.get( "SESSION_SQLALCHEMY_TABLE", Defaults.SESSION_SQLALCHEMY_TABLE ) + SESSION_SQLALCHEMY_TABLE_EXISTS = config.get( + "SESSION_SQLALCHEMY_TABLE_EXISTS", Defaults.SESSION_SQLALCHEMY_TABLE_EXISTS + ) SESSION_SQLALCHEMY_SEQUENCE = config.get( "SESSION_SQLALCHEMY_SEQUENCE", Defaults.SESSION_SQLALCHEMY_SEQUENCE ) @@ -182,6 +185,7 @@ def _get_interface(self, app): **common_params, client=SESSION_SQLALCHEMY, table=SESSION_SQLALCHEMY_TABLE, + table_exists=SESSION_SQLALCHEMY_TABLE_EXISTS, sequence=SESSION_SQLALCHEMY_SEQUENCE, schema=SESSION_SQLALCHEMY_SCHEMA, bind_key=SESSION_SQLALCHEMY_BIND_KEY, diff --git a/src/flask_session/defaults.py b/src/flask_session/defaults.py index f1bc1501..3eaa3970 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -39,6 +39,7 @@ class Defaults: SESSION_SQLALCHEMY_SEQUENCE = None SESSION_SQLALCHEMY_SCHEMA = None SESSION_SQLALCHEMY_BIND_KEY = None + SESSION_SQLALCHEMY_TABLE_EXISTS = False # DynamoDB settings SESSION_DYNAMODB = None diff --git a/src/flask_session/sqlalchemy/sqlalchemy.py b/src/flask_session/sqlalchemy/sqlalchemy.py index ebd5075b..fe239ad9 100644 --- a/src/flask_session/sqlalchemy/sqlalchemy.py +++ b/src/flask_session/sqlalchemy/sqlalchemy.py @@ -1,3 +1,5 @@ +"""Provides a Session Interface to SQLAlchemy""" + import warnings from datetime import datetime from datetime import timedelta as TimeDelta @@ -46,6 +48,13 @@ def __repr__(self): class SqlAlchemySessionInterface(ServerSideSessionInterface): """Uses the Flask-SQLAlchemy from a flask app as session storage. + By default (``table_exists=False``) Flask-Session itself will create the table for session storage according to the + model defined by the ``create_session_model`` method. If ``table_exists`` is set to True, you're responsible—either + manually or via other tooling (e.g., Flask-Migrate)—for creating a table that matches the model, taking into account + the values (or defaults) provided via configuration parameters to the SQLAlchemy session interface (specifically + ``SESSION_SQLALCHEMY_TABLE``, ``SESSION_SQLALCHEMY_SCHEMA``, ``SESSION_SQLALCHEMY_BIND_KEY``, and + ``SESSION_SQLALCHEMY_SEQUENCE``). + :param app: A Flask app instance. :param client: A Flask-SQLAlchemy instance. :param key_prefix: A prefix that is added to all storage keys. @@ -54,11 +63,15 @@ class SqlAlchemySessionInterface(ServerSideSessionInterface): :param sid_length: The length of the generated session id in bytes. :param serialization_format: The serialization format to use for the session data. :param table: The table name you want to use. + :param table_exists: Whether the session table is created/managed outside of Flask-Session (default=False). :param sequence: The sequence to use for the primary key if needed. :param schema: The db schema to use. :param bind_key: The db bind key to use. :param cleanup_n_requests: Delete expired sessions on average every N requests. + .. versionadded:: 0.9 + The `table_exists` parameter was added. + .. versionadded:: 0.7 db changed to client to be standard on all session interfaces. The `cleanup_n_request` parameter was added. @@ -86,6 +99,7 @@ def __init__( sequence: Optional[str] = Defaults.SESSION_SQLALCHEMY_SEQUENCE, schema: Optional[str] = Defaults.SESSION_SQLALCHEMY_SCHEMA, bind_key: Optional[str] = Defaults.SESSION_SQLALCHEMY_BIND_KEY, + table_exists: bool = Defaults.SESSION_SQLALCHEMY_TABLE_EXISTS, cleanup_n_requests: Optional[int] = Defaults.SESSION_CLEANUP_N_REQUESTS, ): self.app = app @@ -103,13 +117,15 @@ def __init__( self.sql_session_model = create_session_model( client, table, schema, bind_key, sequence ) - # Create the table if it does not exist - with app.app_context(): - if bind_key: - engine = self.client.get_engine(app, bind=bind_key) - else: - engine = self.client.engine - self.sql_session_model.__table__.create(bind=engine, checkfirst=True) + + # Optionally create the table if it does not exist + if not table_exists: + with app.app_context(): + if bind_key: + engine = self.client.get_engine(app, bind=bind_key) + else: + engine = self.client.engine + self.sql_session_model.__table__.create(bind=engine, checkfirst=True) super().__init__( app, diff --git a/tests/test_memcached.py b/tests/test_memcached.py index 84298fd9..be63f4f1 100644 --- a/tests/test_memcached.py +++ b/tests/test_memcached.py @@ -11,7 +11,7 @@ class TestMemcachedSession: @contextmanager def setup_memcached(self): - self.mc = memcache.Client(("127.0.0.1:11211")) + self.mc = memcache.Client("127.0.0.1:11211") try: self.mc.flush_all() yield diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index e8c34b9e..704315ec 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -57,3 +57,22 @@ def test_use_signer(self, app_utils): json.loads(byte_string.decode("utf-8")) if byte_string else {} ) assert stored_session.get("value") == "44" + + @pytest.mark.filterwarnings("ignore:No valid SQLAlchemy instance provided") + def test_database_not_created_automatically(self, app_utils): + app = app_utils.create_app( + { + "SESSION_TYPE": "sqlalchemy", + "SQLALCHEMY_DATABASE_URI": "sqlite:///", + "SESSION_SQLALCHEMY_TABLE_EXISTS": True, + } + ) + with app.app_context() and self.setup_sqlalchemy( + app + ) and app.test_request_context(): + assert isinstance( + flask.session, + SqlAlchemySession, + ) + with pytest.raises(AssertionError): + app_utils.test_session(app)