From c76d50ea76654e5d6af45cbf70468cb0d077e30a Mon Sep 17 00:00:00 2001 From: Lee Goolsbee Date: Tue, 16 Apr 2024 10:34:15 -0500 Subject: [PATCH 1/6] Add option to defer table creation; allows management of table and schema by other tooling (like Flask-Migrate) --- src/flask_session/__init__.py | 4 ++++ src/flask_session/defaults.py | 1 + src/flask_session/sqlalchemy/sqlalchemy.py | 17 ++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index 5d4caee9..c7d6fd9c 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_CREATE_TABLE = config.get( + "SESSION_SQLALCHEMY_CREATE_TABLE", Defaults.SESSION_SQLALCHEMY_CREATE_TABLE + ) 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, + create_table=SESSION_SQLALCHEMY_CREATE_TABLE, 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..f9d0a0d4 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_CREATE_TABLE = True # DynamoDB settings SESSION_DYNAMODB = None diff --git a/src/flask_session/sqlalchemy/sqlalchemy.py b/src/flask_session/sqlalchemy/sqlalchemy.py index ebd5075b..ac23e303 100644 --- a/src/flask_session/sqlalchemy/sqlalchemy.py +++ b/src/flask_session/sqlalchemy/sqlalchemy.py @@ -86,6 +86,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, + create_table: bool = Defaults.SESSION_SQLALCHEMY_CREATE_TABLE, cleanup_n_requests: Optional[int] = Defaults.SESSION_CLEANUP_N_REQUESTS, ): self.app = app @@ -103,13 +104,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 create_table: + 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, From 261c4919fa4ac8ec72e7b54453573c357c7b5dc3 Mon Sep 17 00:00:00 2001 From: Lee Goolsbee Date: Tue, 16 Apr 2024 10:35:41 -0500 Subject: [PATCH 2/6] Update docs and add test for new config key SESSION_SQLALCHEMY_CREATE_TABLE --- docs/config_reference.rst | 6 ++++++ tests/test_sqlalchemy.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 9fef8fd5..1dbaaedc 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -175,6 +175,12 @@ SqlAlchemy Default: ``'sessions'`` +.. py:data:: SESSION_SQLALCHEMY_CREATE_TABLE + + Whether (or not) Flask-Session should manage creation of the table for storing session data. + + Default: ``True`` + .. py:data:: SESSION_SQLALCHEMY_SEQUENCE The name of the sequence you want to use for the primary key. diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index e8c34b9e..036d24a5 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_CREATE_TABLE": False, + } + ) + 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) From b89de7fed67310b171a5c29db239060a81a9aadf Mon Sep 17 00:00:00 2001 From: Lee Goolsbee Date: Tue, 16 Apr 2024 10:38:35 -0500 Subject: [PATCH 3/6] Modified by ruff --- tests/test_memcached.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e4c599d4089c5e6591cfc68b73b15ab308bf317c Mon Sep 17 00:00:00 2001 From: Lee Goolsbee Date: Wed, 24 Apr 2024 15:40:05 -0500 Subject: [PATCH 4/6] add myself as a contributor --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) 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 From beb9f34025d326e37e7d08e3ccd9e7b66ecbd9a9 Mon Sep 17 00:00:00 2001 From: Lee Goolsbee Date: Wed, 24 Apr 2024 17:24:09 -0500 Subject: [PATCH 5/6] incorporate PR feedback; create_table -> table_exists --- docs/config_reference.rst | 6 +++--- src/flask_session/__init__.py | 6 +++--- src/flask_session/defaults.py | 2 +- src/flask_session/sqlalchemy/sqlalchemy.py | 12 +++++++++--- tests/test_sqlalchemy.py | 2 +- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 1dbaaedc..7c19fe3c 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -175,11 +175,11 @@ SqlAlchemy Default: ``'sessions'`` -.. py:data:: SESSION_SQLALCHEMY_CREATE_TABLE +.. py:data:: SESSION_SQLALCHEMY_TABLE_EXISTS - Whether (or not) Flask-Session should manage creation of the table for storing session data. + 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: ``True`` + Default: ``False`` .. py:data:: SESSION_SQLALCHEMY_SEQUENCE diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index c7d6fd9c..884de9f4 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -91,8 +91,8 @@ def _get_interface(self, app): SESSION_SQLALCHEMY_TABLE = config.get( "SESSION_SQLALCHEMY_TABLE", Defaults.SESSION_SQLALCHEMY_TABLE ) - SESSION_SQLALCHEMY_CREATE_TABLE = config.get( - "SESSION_SQLALCHEMY_CREATE_TABLE", Defaults.SESSION_SQLALCHEMY_CREATE_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 @@ -185,7 +185,7 @@ def _get_interface(self, app): **common_params, client=SESSION_SQLALCHEMY, table=SESSION_SQLALCHEMY_TABLE, - create_table=SESSION_SQLALCHEMY_CREATE_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 f9d0a0d4..3eaa3970 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -39,7 +39,7 @@ class Defaults: SESSION_SQLALCHEMY_SEQUENCE = None SESSION_SQLALCHEMY_SCHEMA = None SESSION_SQLALCHEMY_BIND_KEY = None - SESSION_SQLALCHEMY_CREATE_TABLE = True + 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 ac23e303..012f906e 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 @@ -54,11 +56,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,7 +92,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, - create_table: bool = Defaults.SESSION_SQLALCHEMY_CREATE_TABLE, + table_exists: bool = Defaults.SESSION_SQLALCHEMY_TABLE_EXISTS, cleanup_n_requests: Optional[int] = Defaults.SESSION_CLEANUP_N_REQUESTS, ): self.app = app @@ -104,9 +110,9 @@ def __init__( self.sql_session_model = create_session_model( client, table, schema, bind_key, sequence ) - + # Optionally create the table if it does not exist - if create_table: + if not table_exists: with app.app_context(): if bind_key: engine = self.client.get_engine(app, bind=bind_key) diff --git a/tests/test_sqlalchemy.py b/tests/test_sqlalchemy.py index 036d24a5..704315ec 100644 --- a/tests/test_sqlalchemy.py +++ b/tests/test_sqlalchemy.py @@ -64,7 +64,7 @@ def test_database_not_created_automatically(self, app_utils): { "SESSION_TYPE": "sqlalchemy", "SQLALCHEMY_DATABASE_URI": "sqlite:///", - "SESSION_SQLALCHEMY_CREATE_TABLE": False, + "SESSION_SQLALCHEMY_TABLE_EXISTS": True, } ) with app.app_context() and self.setup_sqlalchemy( From 8a122438dd80b8cad9597708f6262046c8484bb9 Mon Sep 17 00:00:00 2001 From: Lee Goolsbee Date: Tue, 30 Apr 2024 16:27:01 -0500 Subject: [PATCH 6/6] add blurb to SqlAlchemySessionInterface docstring about table management options --- src/flask_session/sqlalchemy/sqlalchemy.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/flask_session/sqlalchemy/sqlalchemy.py b/src/flask_session/sqlalchemy/sqlalchemy.py index 012f906e..fe239ad9 100644 --- a/src/flask_session/sqlalchemy/sqlalchemy.py +++ b/src/flask_session/sqlalchemy/sqlalchemy.py @@ -48,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.