From 5d197daaf59362a138b132a619849cdac109dc99 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 27 Mar 2024 15:09:10 -0300 Subject: [PATCH 01/34] add psycopg2 dependency --- pyproject.toml | 15 +++++++-------- requirements/dev.txt | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1d3e2326..b14321d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,11 @@ name = "Flask-Session" description = "Server-side session support for Flask" readme = "README.md" -license = {text = "BSD-3-Clause"} -maintainers = [{name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com"}] -authors = [{name = "Shipeng Feng", email = "fsp261@gmail.com"}] +license = { text = "BSD-3-Clause" } +maintainers = [ + { name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com" }, +] +authors = [{ name = "Shipeng Feng", email = "fsp261@gmail.com" }] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", @@ -19,11 +21,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Application Frameworks", ] requires-python = ">=3.8" -dependencies = [ - "flask>=2.2", - "msgspec>=0.18.6", - "cachelib", -] +dependencies = ["flask>=2.2", "msgspec>=0.18.6", "cachelib"] dynamic = ["version"] [project.urls] @@ -88,4 +86,5 @@ dev-dependencies = [ "boto3>=1.34.68", "mypy_boto3_dynamodb>=1.34.67", "pymemcache>=4.0.0", + "psycopg2-binary>=2", ] diff --git a/requirements/dev.txt b/requirements/dev.txt index 14205475..868d1b06 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -17,4 +17,5 @@ Flask-SQLAlchemy pymongo boto3 mypy_boto3_dynamodb +psycopg2-binary From a56e68cc1d187f0e45a10d9bcadcdb1c323336f1 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 27 Mar 2024 15:25:03 -0300 Subject: [PATCH 02/34] setup basic structure --- src/flask_session/postgres/__init__.py | 0 src/flask_session/postgres/postgres.py | 87 ++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/flask_session/postgres/__init__.py create mode 100644 src/flask_session/postgres/postgres.py diff --git a/src/flask_session/postgres/__init__.py b/src/flask_session/postgres/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py new file mode 100644 index 00000000..c2c38d85 --- /dev/null +++ b/src/flask_session/postgres/postgres.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from typing import Optional + +from flask import Flask +from psycopg2.pool import ThreadedConnectionPool +from datetime import timedelta as TimeDelta + +from ..base import ServerSideSession, ServerSideSessionInterface +from ..defaults import Defaults + +DEFAULT_TABLE_NAME = "flask_sessions" +DEFAULT_SCHEMA_NAME = "public" +DEFAULT_PG_MAX_DB_CONN = 10 + +class PostgreSqlSession(ServerSideSession): + pass + + +class PostgreSqlSessionInterface(ServerSideSessionInterface): + pass + + session_class = PostgreSqlSession + ttl = True + + + def __init__( + self, + uri: str, + *, + app: Flask, + key_prefix: str = Defaults.SESSION_KEY_PREFIX, + use_signer: bool = Defaults.SESSION_USE_SIGNER, + permanent: bool = Defaults.SESSION_PERMANENT, + sid_length: int = Defaults.SESSION_ID_LENGTH, + serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, + cleanup_n_requests: Optional[int] = Defaults.SESSION_CLEANUP_N_REQUESTS, + table_name: str = DEFAULT_TABLE_NAME, + schema_name: str = DEFAULT_SCHEMA_NAME, + max_db_conn: int = DEFAULT_PG_MAX_DB_CONN, + ) -> None: + """Initialize a new Flask-PgSession instance. + + Args: + uri (str): The database URI to connect to. + table_name (str, optional): The name of the table to store sessions in. + Defaults to "flask_sessions". + schema_name (str, optional): The name of the schema to store sessions in. + Defaults to "public". + key_prefix (str, optional): The prefix to prepend to the session ID when + storing it in the database. Defaults to "". + use_signer (bool, optional): Whether to use a signer to sign the session. + Defaults to False. + permanent (bool, optional): Whether the session should be permanent. + Defaults to True. + autodelete_expired_sessions (bool, optional): Whether to automatically + delete expired sessions. Defaults to True. + max_db_conn (int, optional): The maximum number of database connections to + keep open. Defaults to 10. + """ + self.pool = ThreadedConnectionPool(1, max_db_conn, uri) + + self._table = table_name + self._schema = schema_name + + super().__init__( + app, + key_prefix, + use_signer, + permanent, + sid_length, + serialization_format, + cleanup_n_requests, + ) + + def _delete_expired_sessions(self) -> None: + raise NotImplementedError + + def _delete_session(self, store_id:str) -> None: + raise NotImplementedError + + def _upsert_session(self, session_lifetime:TimeDelta, session: ServerSideSession, store_id: str) -> None: + raise NotImplementedError + + def _retrieve_session_data(self, store_id: str)-> Optional[dict]: + raise NotImplementedError + From 6cdc30f6491761487680f2dca392153db9bfa20d Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 27 Mar 2024 15:26:05 -0300 Subject: [PATCH 03/34] copy queries from flask-pg-sessions repo --- src/flask_session/postgres/_queries.py | 78 ++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/flask_session/postgres/_queries.py diff --git a/src/flask_session/postgres/_queries.py b/src/flask_session/postgres/_queries.py new file mode 100644 index 00000000..ae47aa93 --- /dev/null +++ b/src/flask_session/postgres/_queries.py @@ -0,0 +1,78 @@ +from psycopg2 import sql + + +class Queries: + def __init__(self, schema: str, table: str) -> None: + """Class to hold all the queries used by the session interface. + + Args: + schema (str): The name of the schema to use for the session data. + table (str): The name of the table to use for the session data. + """ + self.schema = schema + self.table = table + + @property + def create_schema(self) -> str: + return sql.SQL("CREATE SCHEMA IF NOT EXISTS {schema};").format( + schema=sql.Identifier(self.schema) + ) + + @property + def create_table(self) -> str: + uq_idx = sql.Identifier(f"uq_{self.table}_session_id") + expiry_idx = sql.Identifier(f"{self.table}_expiry_idx") + return sql.SQL( + """CREATE TABLE IF NOT EXISTS {schema}.{table} ( + session_id VARCHAR(255) NOT NULL PRIMARY KEY, + created TIMESTAMP WITHOUT TIME ZONE DEFAULT (NOW() AT TIME ZONE 'utc'), + data BYTEA, + expiry TIMESTAMP WITHOUT TIME ZONE + ); + + --- Unique session_id + CREATE UNIQUE INDEX IF NOT EXISTS + {uq_idx} ON {schema}.{table} (session_id); + + --- Index for expiry timestamp + CREATE INDEX IF NOT EXISTS + {expiry_idx} ON {schema}.{table} (expiry);""" + ).format( + schema=sql.Identifier(self.schema), + table=sql.Identifier(self.table), + uq_idx=uq_idx, + expiry_idx=expiry_idx, + ) + + @property + def retrieve_session_data(self) -> str: + return sql.SQL( + """--- If the current sessions is expired, delete it + DELETE FROM {schema}.{table} + WHERE session_id = %(session_id)s AND expiry < NOW(); + --- Else retrieve it + SELECT data FROM {schema}.{table} WHERE session_id = %(session_id)s; + """ + ).format(schema=sql.Identifier(self.schema), table=sql.Identifier(self.table)) + + @property + def upsert_session(self) -> str: + return sql.SQL( + """INSERT INTO {schema}.{table} (session_id, data, expiry) + VALUES (%(session_id)s, %(data)s, %(expiry)s) + ON CONFLICT (session_id) + DO UPDATE SET data = %(data)s, expiry = %(expiry)s; + """ + ).format(schema=sql.Identifier(self.schema), table=sql.Identifier(self.table)) + + @property + def delete_expired_sessions(self) -> str: + return sql.SQL("DELETE FROM {schema}.{table} WHERE expiry < NOW();").format( + schema=sql.Identifier(self.schema), table=sql.Identifier(self.table) + ) + + @property + def delete_session(self) -> str: + return sql.SQL( + "DELETE FROM {schema}.{table} WHERE session_id = %(session_id)s;" + ).format(schema=sql.Identifier(self.schema), table=sql.Identifier(self.table)) From 77046f7217b1c73685c1fbbd8b779c4f3bf81ddb Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:30:36 -0300 Subject: [PATCH 04/34] implement abc methods --- src/flask_session/postgres/postgres.py | 93 ++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 12 deletions(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index c2c38d85..7860bdf8 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -1,18 +1,28 @@ from __future__ import annotations from typing import Optional +from contextlib import contextmanager from flask import Flask from psycopg2.pool import ThreadedConnectionPool +from psycopg2.extensions import connection as PsycoPg2Connection +from psycopg2.extensions import cursor as PsycoPg2Cursor from datetime import timedelta as TimeDelta +from datetime import datetime +from typing import Any, Generator, Optional +from itsdangerous import want_bytes from ..base import ServerSideSession, ServerSideSessionInterface from ..defaults import Defaults +from ._queries import Queries +from .._utils import retry_query + DEFAULT_TABLE_NAME = "flask_sessions" DEFAULT_SCHEMA_NAME = "public" DEFAULT_PG_MAX_DB_CONN = 10 + class PostgreSqlSession(ServerSideSession): pass @@ -23,7 +33,6 @@ class PostgreSqlSessionInterface(ServerSideSessionInterface): session_class = PostgreSqlSession ttl = True - def __init__( self, uri: str, @@ -63,6 +72,8 @@ def __init__( self._table = table_name self._schema = schema_name + self._queries = Queries(schema=schema_name, table=table_name) + super().__init__( app, key_prefix, @@ -73,15 +84,73 @@ def __init__( cleanup_n_requests, ) - def _delete_expired_sessions(self) -> None: - raise NotImplementedError - - def _delete_session(self, store_id:str) -> None: - raise NotImplementedError - - def _upsert_session(self, session_lifetime:TimeDelta, session: ServerSideSession, store_id: str) -> None: - raise NotImplementedError - - def _retrieve_session_data(self, store_id: str)-> Optional[dict]: - raise NotImplementedError + # QUERY HELPERS + + @contextmanager + def _get_cursor( + self, conn: Optional[PsycoPg2Connection] = None + ) -> Generator[PsycoPg2Cursor, None, None]: + _conn: PsycoPg2Connection = conn or self.pool.getconn() + + assert isinstance(_conn, PsycoPg2Connection) + try: + with _conn: + with _conn.cursor() as cur: + yield cur + except Exception: + raise + finally: + self.pool.putconn(_conn) + + @retry_query(max_attempts=3) + def _create_schema_and_table(self) -> None: + with self._get_cursor() as cur: + cur.execute(self._queries.create_schema) + cur.execute(self._queries.create_table) + def _delete_expired_sessions(self) -> None: + """Delete all expired sessions from the database.""" + with self._get_cursor() as cur: + cur.execute(self._queries.delete_expired_sessions) + + @retry_query(max_attempts=3) + def _delete_session(self, store_id: str) -> None: + with self._get_cursor() as cur: + cur.execute( + self._queries.delete_session, + dict(session_id=self._get_store_id(store_id)), + ) + + @retry_query(max_attempts=3) + def _retrieve_session_data(self, store_id: str) -> bytes | None: + with self._get_cursor() as cur: + cur.execute( + self._queries.retrieve_session_data, + dict(session_id=self._get_store_id(store_id)), + ) + session_data = cur.fetchone() + + if session_data is not None: + serialized_session_data = want_bytes(session_data) + return self.serializer.decode(serialized_session_data) + return None + + @retry_query(max_attempts=3) + def _upsert_session( + self, session_lifetime: TimeDelta, session: ServerSideSession, store_id: str + ) -> None: + + serialized_session_data = self.serializer.encode(session) + + # TODO: use database's NOW() rather than datetime.utcnow() + expiry = datetime.utcnow() + session_lifetime + + with self._get_cursor() as cur: + cur.execute( + self._queries.upsert_session, + dict( + session_id=session.sid, + data=serialized_session_data, + expiry=expiry, + ), + ) From 161b2c954d06c3a27042563e7c013691f6dd6486 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:32:03 -0300 Subject: [PATCH 05/34] add imports in __init__.py --- src/flask_session/postgres/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/flask_session/postgres/__init__.py b/src/flask_session/postgres/__init__.py index e69de29b..f78d9a79 100644 --- a/src/flask_session/postgres/__init__.py +++ b/src/flask_session/postgres/__init__.py @@ -0,0 +1 @@ +from .postgres import PostgreSqlSession, PostgreSqlSessionInterface # noqa: F401 From eef255d20c064170085f7b971515fface20af3f5 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:35:32 -0300 Subject: [PATCH 06/34] pass ttl to upsert query and compute expiry time based on database time --- src/flask_session/postgres/_queries.py | 6 +++--- src/flask_session/postgres/postgres.py | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/flask_session/postgres/_queries.py b/src/flask_session/postgres/_queries.py index ae47aa93..af38a2df 100644 --- a/src/flask_session/postgres/_queries.py +++ b/src/flask_session/postgres/_queries.py @@ -58,10 +58,10 @@ def retrieve_session_data(self) -> str: @property def upsert_session(self) -> str: return sql.SQL( - """INSERT INTO {schema}.{table} (session_id, data, expiry) - VALUES (%(session_id)s, %(data)s, %(expiry)s) + """INSERT INTO {schema}.{table} (session_id, data, ttl) + VALUES (%(session_id)s, %(data)s, NOW() + %(ttl)s) ON CONFLICT (session_id) - DO UPDATE SET data = %(data)s, expiry = %(expiry)s; + DO UPDATE SET data = %(data)s, expiry = NOW() + %(ttl)s; """ ).format(schema=sql.Identifier(self.schema), table=sql.Identifier(self.table)) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 7860bdf8..1ae49f33 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -8,7 +8,6 @@ from psycopg2.extensions import connection as PsycoPg2Connection from psycopg2.extensions import cursor as PsycoPg2Cursor from datetime import timedelta as TimeDelta -from datetime import datetime from typing import Any, Generator, Optional from itsdangerous import want_bytes @@ -142,15 +141,12 @@ def _upsert_session( serialized_session_data = self.serializer.encode(session) - # TODO: use database's NOW() rather than datetime.utcnow() - expiry = datetime.utcnow() + session_lifetime - with self._get_cursor() as cur: cur.execute( self._queries.upsert_session, dict( session_id=session.sid, data=serialized_session_data, - expiry=expiry, + ttl=session_lifetime, ), ) From 4441ca6e8c45622be8b362892e9d0d8d3ef4d347 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:35:49 -0300 Subject: [PATCH 07/34] sort imports --- src/flask_session/postgres/postgres.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 1ae49f33..f65af349 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -1,21 +1,19 @@ from __future__ import annotations -from typing import Optional from contextlib import contextmanager +from datetime import timedelta as TimeDelta +from typing import Any, Generator, Optional from flask import Flask -from psycopg2.pool import ThreadedConnectionPool +from itsdangerous import want_bytes from psycopg2.extensions import connection as PsycoPg2Connection from psycopg2.extensions import cursor as PsycoPg2Cursor -from datetime import timedelta as TimeDelta -from typing import Any, Generator, Optional -from itsdangerous import want_bytes +from psycopg2.pool import ThreadedConnectionPool +from .._utils import retry_query from ..base import ServerSideSession, ServerSideSessionInterface from ..defaults import Defaults - from ._queries import Queries -from .._utils import retry_query DEFAULT_TABLE_NAME = "flask_sessions" DEFAULT_SCHEMA_NAME = "public" From a3a4e7b53ca08740bfd4403c4811e53c2db39d92 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:38:55 -0300 Subject: [PATCH 08/34] linting fixes --- src/flask_session/postgres/postgres.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index f65af349..630579cc 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from datetime import timedelta as TimeDelta -from typing import Any, Generator, Optional +from typing import Generator from flask import Flask from itsdangerous import want_bytes @@ -40,7 +40,7 @@ def __init__( permanent: bool = Defaults.SESSION_PERMANENT, sid_length: int = Defaults.SESSION_ID_LENGTH, serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, - cleanup_n_requests: Optional[int] = Defaults.SESSION_CLEANUP_N_REQUESTS, + cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, table_name: str = DEFAULT_TABLE_NAME, schema_name: str = DEFAULT_SCHEMA_NAME, max_db_conn: int = DEFAULT_PG_MAX_DB_CONN, @@ -85,15 +85,14 @@ def __init__( @contextmanager def _get_cursor( - self, conn: Optional[PsycoPg2Connection] = None + self, conn: PsycoPg2Connection | None = None ) -> Generator[PsycoPg2Cursor, None, None]: _conn: PsycoPg2Connection = conn or self.pool.getconn() assert isinstance(_conn, PsycoPg2Connection) try: - with _conn: - with _conn.cursor() as cur: - yield cur + with _conn, _conn.cursor() as cur: + yield cur except Exception: raise finally: @@ -119,7 +118,7 @@ def _delete_session(self, store_id: str) -> None: ) @retry_query(max_attempts=3) - def _retrieve_session_data(self, store_id: str) -> bytes | None: + def _retrieve_session_data(self, store_id: str) -> dict | None: with self._get_cursor() as cur: cur.execute( self._queries.retrieve_session_data, From 58b445adab348269ffa361c7c2dd37f178131672 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:41:51 -0300 Subject: [PATCH 09/34] move defaults to dedicated module --- src/flask_session/defaults.py | 5 +++++ src/flask_session/postgres/postgres.py | 10 +++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/flask_session/defaults.py b/src/flask_session/defaults.py index 7f890d6e..4a341eb0 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -43,3 +43,8 @@ class Defaults: # DynamoDB settings SESSION_DYNAMODB = None SESSION_DYNAMODB_TABLE = "Sessions" + + # PostgreSQL settings + SESSION_POSTGRESQL_TABLE = "flask_sessions" + SESSION_POSTGRESQL_SCHEMA = "public" + SESSION_POSTGRESQL_MAX_DB_CONN = 10 diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 630579cc..a46670c7 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -15,10 +15,6 @@ from ..defaults import Defaults from ._queries import Queries -DEFAULT_TABLE_NAME = "flask_sessions" -DEFAULT_SCHEMA_NAME = "public" -DEFAULT_PG_MAX_DB_CONN = 10 - class PostgreSqlSession(ServerSideSession): pass @@ -41,9 +37,9 @@ def __init__( sid_length: int = Defaults.SESSION_ID_LENGTH, serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, - table_name: str = DEFAULT_TABLE_NAME, - schema_name: str = DEFAULT_SCHEMA_NAME, - max_db_conn: int = DEFAULT_PG_MAX_DB_CONN, + table_name: str = Defaults.SESSION_POSTGRESQL_TABLE, + schema_name: str = Defaults.SESSION_POSTGRESQL_SCHEMA, + max_db_conn: int = Defaults.SESSION_POSTGRESQL_MAX_DB_CONN, ) -> None: """Initialize a new Flask-PgSession instance. From a9e65e37dbfce3f938b7a246183f9debbaff8f64 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:42:29 -0300 Subject: [PATCH 10/34] tidy up --- src/flask_session/postgres/postgres.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index a46670c7..d9ad9c81 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -37,8 +37,8 @@ def __init__( sid_length: int = Defaults.SESSION_ID_LENGTH, serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, - table_name: str = Defaults.SESSION_POSTGRESQL_TABLE, - schema_name: str = Defaults.SESSION_POSTGRESQL_SCHEMA, + table: str = Defaults.SESSION_POSTGRESQL_TABLE, + schema: str = Defaults.SESSION_POSTGRESQL_SCHEMA, max_db_conn: int = Defaults.SESSION_POSTGRESQL_MAX_DB_CONN, ) -> None: """Initialize a new Flask-PgSession instance. @@ -62,10 +62,10 @@ def __init__( """ self.pool = ThreadedConnectionPool(1, max_db_conn, uri) - self._table = table_name - self._schema = schema_name + self._table = table + self._schema = schema - self._queries = Queries(schema=schema_name, table=table_name) + self._queries = Queries(schema=self._schema, table=self._table) super().__init__( app, @@ -77,8 +77,6 @@ def __init__( cleanup_n_requests, ) - # QUERY HELPERS - @contextmanager def _get_cursor( self, conn: PsycoPg2Connection | None = None From 3c3dbff6c9bca3765b023aa71040b42e2f101c38 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:44:34 -0300 Subject: [PATCH 11/34] fix indentation --- src/flask_session/postgres/postgres.py | 128 ++++++++++++------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index d9ad9c81..e5817a74 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -77,67 +77,67 @@ def __init__( cleanup_n_requests, ) - @contextmanager - def _get_cursor( - self, conn: PsycoPg2Connection | None = None - ) -> Generator[PsycoPg2Cursor, None, None]: - _conn: PsycoPg2Connection = conn or self.pool.getconn() - - assert isinstance(_conn, PsycoPg2Connection) - try: - with _conn, _conn.cursor() as cur: - yield cur - except Exception: - raise - finally: - self.pool.putconn(_conn) - - @retry_query(max_attempts=3) - def _create_schema_and_table(self) -> None: - with self._get_cursor() as cur: - cur.execute(self._queries.create_schema) - cur.execute(self._queries.create_table) - - def _delete_expired_sessions(self) -> None: - """Delete all expired sessions from the database.""" - with self._get_cursor() as cur: - cur.execute(self._queries.delete_expired_sessions) - - @retry_query(max_attempts=3) - def _delete_session(self, store_id: str) -> None: - with self._get_cursor() as cur: - cur.execute( - self._queries.delete_session, - dict(session_id=self._get_store_id(store_id)), - ) - - @retry_query(max_attempts=3) - def _retrieve_session_data(self, store_id: str) -> dict | None: - with self._get_cursor() as cur: - cur.execute( - self._queries.retrieve_session_data, - dict(session_id=self._get_store_id(store_id)), - ) - session_data = cur.fetchone() - - if session_data is not None: - serialized_session_data = want_bytes(session_data) - return self.serializer.decode(serialized_session_data) - return None - - @retry_query(max_attempts=3) - def _upsert_session( - self, session_lifetime: TimeDelta, session: ServerSideSession, store_id: str - ) -> None: - - serialized_session_data = self.serializer.encode(session) - - with self._get_cursor() as cur: - cur.execute( - self._queries.upsert_session, - dict( - session_id=session.sid, - data=serialized_session_data, - ttl=session_lifetime, - ), - ) + @contextmanager + def _get_cursor( + self, conn: PsycoPg2Connection | None = None + ) -> Generator[PsycoPg2Cursor, None, None]: + _conn: PsycoPg2Connection = conn or self.pool.getconn() + + assert isinstance(_conn, PsycoPg2Connection) + try: + with _conn, _conn.cursor() as cur: + yield cur + except Exception: + raise + finally: + self.pool.putconn(_conn) + + @retry_query(max_attempts=3) + def _create_schema_and_table(self) -> None: + with self._get_cursor() as cur: + cur.execute(self._queries.create_schema) + cur.execute(self._queries.create_table) + + def _delete_expired_sessions(self) -> None: + """Delete all expired sessions from the database.""" + with self._get_cursor() as cur: + cur.execute(self._queries.delete_expired_sessions) + + @retry_query(max_attempts=3) + def _delete_session(self, store_id: str) -> None: + with self._get_cursor() as cur: + cur.execute( + self._queries.delete_session, + dict(session_id=self._get_store_id(store_id)), + ) + + @retry_query(max_attempts=3) + def _retrieve_session_data(self, store_id: str) -> dict | None: + with self._get_cursor() as cur: + cur.execute( + self._queries.retrieve_session_data, + dict(session_id=self._get_store_id(store_id)), + ) + session_data = cur.fetchone() + + if session_data is not None: + serialized_session_data = want_bytes(session_data) + return self.serializer.decode(serialized_session_data) + return None + + @retry_query(max_attempts=3) + def _upsert_session( + self, session_lifetime: TimeDelta, session: ServerSideSession, store_id: str + ) -> None: + + serialized_session_data = self.serializer.encode(session) + + with self._get_cursor() as cur: + cur.execute( + self._queries.upsert_session, + dict( + session_id=session.sid, + data=serialized_session_data, + ttl=session_lifetime, + ), + ) From e79befe213948a97cf9ec99da5bc6b1d86a04232 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:45:10 -0300 Subject: [PATCH 12/34] create schema and table if they don't exist --- src/flask_session/postgres/postgres.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index e5817a74..1748fe1a 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -67,6 +67,8 @@ def __init__( self._queries = Queries(schema=self._schema, table=self._table) + self._create_schema_and_table() + super().__init__( app, key_prefix, From 6d0ece68a473f5f56be23f178cc5d6b9ba633551 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 09:47:05 -0300 Subject: [PATCH 13/34] use store_id arg in upsert_session --- src/flask_session/postgres/postgres.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 1748fe1a..06b6bddc 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -134,11 +134,14 @@ def _upsert_session( serialized_session_data = self.serializer.encode(session) + if session.sid is not None: + assert session.sid == store_id + with self._get_cursor() as cur: cur.execute( self._queries.upsert_session, dict( - session_id=session.sid, + session_id=store_id, data=serialized_session_data, ttl=session_lifetime, ), From ae93eaf5221a458d25febfe971003128fd8e0fb4 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Thu, 28 Mar 2024 12:20:51 -0300 Subject: [PATCH 14/34] fix: misunderstood ttl flag --- src/flask_session/postgres/postgres.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 06b6bddc..02d3a891 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -24,7 +24,7 @@ class PostgreSqlSessionInterface(ServerSideSessionInterface): pass session_class = PostgreSqlSession - ttl = True + ttl = False def __init__( self, From 24d3732254043991c3f94f23d4dd07fe25e1e0ee Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 09:36:53 -0300 Subject: [PATCH 15/34] pass threadedconnectionpool to constructor rather than connection parameters --- src/flask_session/defaults.py | 1 - src/flask_session/postgres/postgres.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flask_session/defaults.py b/src/flask_session/defaults.py index 4a341eb0..eab71c70 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -47,4 +47,3 @@ class Defaults: # PostgreSQL settings SESSION_POSTGRESQL_TABLE = "flask_sessions" SESSION_POSTGRESQL_SCHEMA = "public" - SESSION_POSTGRESQL_MAX_DB_CONN = 10 diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 02d3a891..137482b2 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -28,9 +28,8 @@ class PostgreSqlSessionInterface(ServerSideSessionInterface): def __init__( self, - uri: str, - *, app: Flask, + pool: ThreadedConnectionPool, key_prefix: str = Defaults.SESSION_KEY_PREFIX, use_signer: bool = Defaults.SESSION_USE_SIGNER, permanent: bool = Defaults.SESSION_PERMANENT, @@ -39,7 +38,6 @@ def __init__( cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, table: str = Defaults.SESSION_POSTGRESQL_TABLE, schema: str = Defaults.SESSION_POSTGRESQL_SCHEMA, - max_db_conn: int = Defaults.SESSION_POSTGRESQL_MAX_DB_CONN, ) -> None: """Initialize a new Flask-PgSession instance. @@ -60,7 +58,10 @@ def __init__( max_db_conn (int, optional): The maximum number of database connections to keep open. Defaults to 10. """ - self.pool = ThreadedConnectionPool(1, max_db_conn, uri) + if not isinstance(pool, ThreadedConnectionPool): + raise TypeError("No valid ThreadedConnectionPool instance provided.") + + self.pool = pool self._table = table self._schema = schema From bc3ee172caf13a1f1e25b5370c708f4f6863e479 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 09:37:27 -0300 Subject: [PATCH 16/34] Remove leftover 'pass' --- src/flask_session/postgres/postgres.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 137482b2..6391d871 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -21,7 +21,6 @@ class PostgreSqlSession(ServerSideSession): class PostgreSqlSessionInterface(ServerSideSessionInterface): - pass session_class = PostgreSqlSession ttl = False From 1ede44801e2762d5b72dd555223eca32367f68e7 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 09:41:46 -0300 Subject: [PATCH 17/34] docstring --- src/flask_session/postgres/postgres.py | 33 ++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 6391d871..30133f20 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -21,6 +21,18 @@ class PostgreSqlSession(ServerSideSession): class PostgreSqlSessionInterface(ServerSideSessionInterface): + """A Session interface that uses PostgreSQL as a session storage. (`psycopg2` required) + + :param pool: A ``psycopg2.pool.ThreadedConnectionPool`` instance. + :param key_prefix: A prefix that is added to all storage keys. + :param use_signer: Whether to sign the session id cookie or not. + :param permanent: Whether to use permanent session or not. + :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 schema: The db schema to use. + :param cleanup_n_requests: Delete expired sessions on average every N requests. + """ session_class = PostgreSqlSession ttl = False @@ -34,29 +46,10 @@ def __init__( permanent: bool = Defaults.SESSION_PERMANENT, sid_length: int = Defaults.SESSION_ID_LENGTH, serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, - cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, table: str = Defaults.SESSION_POSTGRESQL_TABLE, schema: str = Defaults.SESSION_POSTGRESQL_SCHEMA, + cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, ) -> None: - """Initialize a new Flask-PgSession instance. - - Args: - uri (str): The database URI to connect to. - table_name (str, optional): The name of the table to store sessions in. - Defaults to "flask_sessions". - schema_name (str, optional): The name of the schema to store sessions in. - Defaults to "public". - key_prefix (str, optional): The prefix to prepend to the session ID when - storing it in the database. Defaults to "". - use_signer (bool, optional): Whether to use a signer to sign the session. - Defaults to False. - permanent (bool, optional): Whether the session should be permanent. - Defaults to True. - autodelete_expired_sessions (bool, optional): Whether to automatically - delete expired sessions. Defaults to True. - max_db_conn (int, optional): The maximum number of database connections to - keep open. Defaults to 10. - """ if not isinstance(pool, ThreadedConnectionPool): raise TypeError("No valid ThreadedConnectionPool instance provided.") From b21760e3b27f1cdc36783918c96eda0601a8537f Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 09:43:16 -0300 Subject: [PATCH 18/34] add postgres session to api docs --- docs/api.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index b31eceb5..31e1e2b1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -20,4 +20,5 @@ Anything documented here is part of the public API that Flask-Session provides, .. autoclass:: flask_session.cachelib.CacheLibSessionInterface .. autoclass:: flask_session.mongodb.MongoDBSessionInterface .. autoclass:: flask_session.sqlalchemy.SqlAlchemySessionInterface -.. autoclass:: flask_session.dynamodb.DynamoDBSessionInterface \ No newline at end of file +.. autoclass:: flask_session.dynamodb.DynamoDBSessionInterface +.. autoclass:: flask_session.postgres.PostgreSqlSessionInterface \ No newline at end of file From b8ece7faf03d412a72e044ecfc9fdea79780382e Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 10:08:25 -0300 Subject: [PATCH 19/34] initialisation of postrges session from flask config --- src/flask_session/__init__.py | 30 +++++++++++++++++++++++--- src/flask_session/defaults.py | 1 + src/flask_session/postgres/postgres.py | 2 +- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index b95a0b05..25dff6ba 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -100,9 +100,6 @@ def _get_interface(self, app): SESSION_SQLALCHEMY_BIND_KEY = config.get( "SESSION_SQLALCHEMY_BIND_KEY", Defaults.SESSION_SQLALCHEMY_BIND_KEY ) - SESSION_CLEANUP_N_REQUESTS = config.get( - "SESSION_CLEANUP_N_REQUESTS", Defaults.SESSION_CLEANUP_N_REQUESTS - ) # DynamoDB settings SESSION_DYNAMODB = config.get("SESSION_DYNAMODB", Defaults.SESSION_DYNAMODB) @@ -110,6 +107,22 @@ def _get_interface(self, app): "SESSION_DYNAMODB_TABLE", Defaults.SESSION_DYNAMODB_TABLE ) + # PostgreSQL settings + SESSION_POSTGRESQL = config.get( + "SESSION_POSTGRESQL", Defaults.SESSION_POSTGRESQL + ) + SESSION_POSTGRESQL_TABLE = config.get( + "SESSION_POSTGRESQL_TABLE", Defaults.SESSION_POSTGRESQL_TABLE + ) + SESSION_POSTGRESQL_SCHEMA = config.get( + "SESSION_POSTGRESQL_SCHEMA", Defaults.SESSION_POSTGRESQL_SCHEMA + ) + + # Shared settings + SESSION_CLEANUP_N_REQUESTS = config.get( + "SESSION_CLEANUP_N_REQUESTS", Defaults.SESSION_CLEANUP_N_REQUESTS + ) + common_params = { "app": app, "key_prefix": SESSION_KEY_PREFIX, @@ -180,6 +193,17 @@ def _get_interface(self, app): table_name=SESSION_DYNAMODB_TABLE, ) + elif SESSION_TYPE == "postgresql": + from .postgres import PostgreSqlSessionInterface + + session_interface = PostgreSqlSessionInterface( + **common_params, + pool=SESSION_POSTGRESQL, + table=SESSION_POSTGRESQL_TABLE, + schema=SESSION_POSTGRESQL_SCHEMA, + cleanup_n_requests=SESSION_CLEANUP_N_REQUESTS, + ) + else: raise ValueError(f"Unrecognized value for SESSION_TYPE: {SESSION_TYPE}") diff --git a/src/flask_session/defaults.py b/src/flask_session/defaults.py index eab71c70..0467db46 100644 --- a/src/flask_session/defaults.py +++ b/src/flask_session/defaults.py @@ -45,5 +45,6 @@ class Defaults: SESSION_DYNAMODB_TABLE = "Sessions" # PostgreSQL settings + SESSION_POSTGRESQL = None SESSION_POSTGRESQL_TABLE = "flask_sessions" SESSION_POSTGRESQL_SCHEMA = "public" diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 30133f20..3ebbbbcb 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -40,7 +40,7 @@ class PostgreSqlSessionInterface(ServerSideSessionInterface): def __init__( self, app: Flask, - pool: ThreadedConnectionPool, + pool: ThreadedConnectionPool | None = Defaults.SESSION_POSTGRESQL, key_prefix: str = Defaults.SESSION_KEY_PREFIX, use_signer: bool = Defaults.SESSION_USE_SIGNER, permanent: bool = Defaults.SESSION_PERMANENT, From 4b19f7fd0f568f13c5b6eb70c772ebc128bd81b6 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 10:15:33 -0300 Subject: [PATCH 20/34] update docs requirements --- requirements/docs.in | 3 ++- requirements/docs.txt | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/requirements/docs.in b/requirements/docs.in index 211cd708..b8088dd3 100644 --- a/requirements/docs.in +++ b/requirements/docs.in @@ -9,4 +9,5 @@ pymongo flask_sqlalchemy pymemcache boto3 -mypy_boto3_dynamodb \ No newline at end of file +mypy_boto3_dynamodb +psycopg2-binary \ No newline at end of file diff --git a/requirements/docs.txt b/requirements/docs.txt index 81adad9c..0d61012b 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -6,6 +6,8 @@ # alabaster==0.7.13 # via sphinx +async-timeout==4.0.3 + # via redis babel==2.12.1 # via sphinx beautifulsoup4==4.12.3 @@ -36,6 +38,8 @@ flask-sqlalchemy==3.1.1 # via -r requirements/docs.in furo==2024.1.29 # via -r requirements/docs.in +greenlet==3.0.3 + # via sqlalchemy idna==3.4 # via requests imagesize==1.4.1 @@ -58,6 +62,8 @@ mypy-boto3-dynamodb==1.34.67 # via -r requirements/docs.in packaging==23.1 # via sphinx +psycopg2-binary==2.9.9 + # via -r requirements/docs.in pygments==2.15.1 # via # furo From d8ec548083fe5392ebfb11b488e391d9602e5551 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 10:49:37 -0300 Subject: [PATCH 21/34] query for dropping sessions table - useful for tests --- src/flask_session/postgres/_queries.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/flask_session/postgres/_queries.py b/src/flask_session/postgres/_queries.py index af38a2df..26745764 100644 --- a/src/flask_session/postgres/_queries.py +++ b/src/flask_session/postgres/_queries.py @@ -76,3 +76,9 @@ def delete_session(self) -> str: return sql.SQL( "DELETE FROM {schema}.{table} WHERE session_id = %(session_id)s;" ).format(schema=sql.Identifier(self.schema), table=sql.Identifier(self.table)) + + @property + def drop_sessions_table(self) -> None: + sql.SQL("DROP TABLE IF EXISTS {schema}.{table};").format( + schema=sql.Identifier(self.schema), table=sql.Identifier(self.table) + ) From 86b5b9c015ecaf9f5ff69fa2607847e13d20976f Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 12:12:40 -0300 Subject: [PATCH 22/34] add postgres to docker compose --- docker-compose.yml | 71 ++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7864e9d7..543ec039 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,48 @@ -version: '3.8' +version: "3.8" services: - dynamodb-local: - image: "amazon/dynamodb-local:latest" - container_name: dynamodb-local - ports: - - "8000:8000" - environment: - - AWS_ACCESS_KEY_ID=dummy - - AWS_SECRET_ACCESS_KEY=dummy - - AWS_DEFAULT_REGION=us-west-2 + dynamodb-local: + image: "amazon/dynamodb-local:latest" + container_name: dynamodb-local + ports: + - "8000:8000" + environment: + - AWS_ACCESS_KEY_ID=dummy + - AWS_SECRET_ACCESS_KEY=dummy + - AWS_DEFAULT_REGION=us-west-2 - mongo: - image: mongo:latest - ports: - - "27017:27017" - volumes: - - mongo_data:/data/db + mongo: + image: mongo:latest + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db - redis: - image: redis:latest - ports: - - "6379:6379" - volumes: - - redis_data:/data + redis: + image: redis:latest + ports: + - "6379:6379" + volumes: + - redis_data:/data - memcached: - image: memcached:latest - ports: - - "11211:11211" + memcached: + image: memcached:latest + ports: + - "11211:11211" + + postgres: + image: postgres:latest + environment: + - POSTGRES_USER=root + - POSTGRES_PASSWORD=pwd + - POSTGRES_DB=dummy + ports: + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data volumes: - postgres_data: - mongo_data: - redis_data: - dynamodb_data: \ No newline at end of file + postgres_data: + mongo_data: + redis_data: + dynamodb_data: From 7cbfd26affb618312e40dfdb93ac3629f71a6e87 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 12:12:52 -0300 Subject: [PATCH 23/34] basic tests for postgres sessions --- tests/test_postgresql.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/test_postgresql.py diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py new file mode 100644 index 00000000..bc6cebf3 --- /dev/null +++ b/tests/test_postgresql.py @@ -0,0 +1,30 @@ +import json +from contextlib import contextmanager + +import flask +import pytest +from psycopg2.pool import ThreadedConnectionPool +from sqlalchemy import text + +from flask_session.postgres import PostgreSqlSession + +TEST_DB = "postgresql://root:pwd@localhost:5433/dummy" + + +class TestPostgreSql: + """This requires package: sqlalchemy""" + + @contextmanager + def setup_postgresql(self, app_utils): + self.pool = ThreadedConnectionPool(1, 5, TEST_DB) + self.app = app_utils.create_app( + {"SESSION_TYPE": "postgresql", "SESSION_POSTGRESQL": self.pool} + ) + + yield + self.app.session_interface._drop_table() + + def test_postgres(self, app_utils): + with self.setup_postgresql(app_utils), self.app.test_request_context(): + assert isinstance(flask.session, PostgreSqlSession) + app_utils.test_session(self.app) From 7d68f456c83345ccfffee3b1f852ccd28b127458 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 12:13:02 -0300 Subject: [PATCH 24/34] bug fixes --- src/flask_session/postgres/_queries.py | 6 +++--- src/flask_session/postgres/postgres.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/flask_session/postgres/_queries.py b/src/flask_session/postgres/_queries.py index 26745764..23fb0101 100644 --- a/src/flask_session/postgres/_queries.py +++ b/src/flask_session/postgres/_queries.py @@ -58,7 +58,7 @@ def retrieve_session_data(self) -> str: @property def upsert_session(self) -> str: return sql.SQL( - """INSERT INTO {schema}.{table} (session_id, data, ttl) + """INSERT INTO {schema}.{table} (session_id, data, expiry) VALUES (%(session_id)s, %(data)s, NOW() + %(ttl)s) ON CONFLICT (session_id) DO UPDATE SET data = %(data)s, expiry = NOW() + %(ttl)s; @@ -78,7 +78,7 @@ def delete_session(self) -> str: ).format(schema=sql.Identifier(self.schema), table=sql.Identifier(self.table)) @property - def drop_sessions_table(self) -> None: - sql.SQL("DROP TABLE IF EXISTS {schema}.{table};").format( + def drop_sessions_table(self) -> str: + return sql.SQL("DROP TABLE IF EXISTS {schema}.{table};").format( schema=sql.Identifier(self.schema), table=sql.Identifier(self.table) ) diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgres/postgres.py index 3ebbbbcb..48ed3340 100644 --- a/src/flask_session/postgres/postgres.py +++ b/src/flask_session/postgres/postgres.py @@ -103,7 +103,7 @@ def _delete_session(self, store_id: str) -> None: with self._get_cursor() as cur: cur.execute( self._queries.delete_session, - dict(session_id=self._get_store_id(store_id)), + dict(session_id=store_id), ) @retry_query(max_attempts=3) @@ -111,12 +111,12 @@ def _retrieve_session_data(self, store_id: str) -> dict | None: with self._get_cursor() as cur: cur.execute( self._queries.retrieve_session_data, - dict(session_id=self._get_store_id(store_id)), + dict(session_id=store_id), ) session_data = cur.fetchone() if session_data is not None: - serialized_session_data = want_bytes(session_data) + serialized_session_data = want_bytes(session_data[0]) return self.serializer.decode(serialized_session_data) return None @@ -128,7 +128,7 @@ def _upsert_session( serialized_session_data = self.serializer.encode(session) if session.sid is not None: - assert session.sid == store_id + assert session.sid == store_id.removeprefix(self.key_prefix) with self._get_cursor() as cur: cur.execute( @@ -139,3 +139,8 @@ def _upsert_session( ttl=session_lifetime, ), ) + + def _drop_table(self): + with self._get_cursor() as cur: + print(self._queries.drop_sessions_table) + cur.execute(self._queries.drop_sessions_table) From 297eaa175b527e4b7dd6829d328feebe95ec287b Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 12:25:01 -0300 Subject: [PATCH 25/34] add cookie test --- tests/test_postgresql.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index bc6cebf3..0d52e84a 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -2,9 +2,8 @@ from contextlib import contextmanager import flask -import pytest +from itsdangerous import want_bytes from psycopg2.pool import ThreadedConnectionPool -from sqlalchemy import text from flask_session.postgres import PostgreSqlSession @@ -24,7 +23,28 @@ def setup_postgresql(self, app_utils): yield self.app.session_interface._drop_table() + def retrieve_stored_session(self, key): + with self.app.session_interface._get_cursor() as cur: + cur.execute( + self.app.session_interface._queries.retrieve_session_data, + dict(session_id=key), + ) + + session_data = cur.fetchone() + if session_data is not None: + return want_bytes(session_data[0].tobytes()) + return None + def test_postgres(self, app_utils): with self.setup_postgresql(app_utils), self.app.test_request_context(): assert isinstance(flask.session, PostgreSqlSession) app_utils.test_session(self.app) + + # Check if the session is stored in MongoDB + cookie = app_utils.test_session_with_cookie(self.app) + session_id = cookie.split(";")[0].split("=")[1] + byte_string = self.retrieve_stored_session(f"session:{session_id}") + stored_session = ( + json.loads(byte_string.decode("utf-8")) if byte_string else {} + ) + assert stored_session.get("value") == "44" From b4f488d9ef217ff4456b5dea8f1d9907f3a7018c Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 15:40:18 -0300 Subject: [PATCH 26/34] add postgres service to test gh actions --- .github/workflows/test.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3b57e6e9..2351b7d4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,6 +15,21 @@ jobs: image: amazon/dynamodb-local ports: - 8000:8000 + + postgresql: + image: postgres:latest + ports: + - 5433:5432 + env: + POSTGRES_PASSWORD: pwd + POSTGRES_USER: root + POSTGRES_DB: dummy + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - uses: actions/checkout@v4 - uses: supercharge/redis-github-action@1.5.0 From 616edd06d2e9f9f84e591bad2603170e12a5eeef Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 16:33:21 -0300 Subject: [PATCH 27/34] undo formatting changes --- docker-compose.yml | 80 +++++++++++++++++++++++----------------------- pyproject.toml | 10 +++--- 2 files changed, 46 insertions(+), 44 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 543ec039..3463412d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,48 +1,48 @@ -version: "3.8" +version: '3.8' services: - dynamodb-local: - image: "amazon/dynamodb-local:latest" - container_name: dynamodb-local - ports: - - "8000:8000" - environment: - - AWS_ACCESS_KEY_ID=dummy - - AWS_SECRET_ACCESS_KEY=dummy - - AWS_DEFAULT_REGION=us-west-2 + dynamodb-local: + image: "amazon/dynamodb-local:latest" + container_name: dynamodb-local + ports: + - "8000:8000" + environment: + - AWS_ACCESS_KEY_ID=dummy + - AWS_SECRET_ACCESS_KEY=dummy + - AWS_DEFAULT_REGION=us-west-2 - mongo: - image: mongo:latest - ports: - - "27017:27017" - volumes: - - mongo_data:/data/db + mongo: + image: mongo:latest + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db - redis: - image: redis:latest - ports: - - "6379:6379" - volumes: - - redis_data:/data + redis: + image: redis:latest + ports: + - "6379:6379" + volumes: + - redis_data:/data - memcached: - image: memcached:latest - ports: - - "11211:11211" + memcached: + image: memcached:latest + ports: + - "11211:11211" - postgres: - image: postgres:latest - environment: - - POSTGRES_USER=root - - POSTGRES_PASSWORD=pwd - - POSTGRES_DB=dummy - ports: - - "5433:5432" - volumes: - - postgres_data:/var/lib/postgresql/data + postgres: + image: postgres:latest + environment: + - POSTGRES_USER=root + - POSTGRES_PASSWORD=pwd + - POSTGRES_DB=dummy + ports: + - "5433:5432" + volumes: + - postgres_data:/var/lib/postgresql/data volumes: - postgres_data: - mongo_data: - redis_data: - dynamodb_data: + postgres_data: + mongo_data: + redis_data: + dynamodb_data: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b14321d4..b66f1cc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,7 @@ name = "Flask-Session" description = "Server-side session support for Flask" readme = "README.md" license = { text = "BSD-3-Clause" } -maintainers = [ - { name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com" }, -] +maintainers = [{ name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com" }] authors = [{ name = "Shipeng Feng", email = "fsp261@gmail.com" }] classifiers = [ "Development Status :: 4 - Beta", @@ -21,7 +19,11 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Application Frameworks", ] requires-python = ">=3.8" -dependencies = ["flask>=2.2", "msgspec>=0.18.6", "cachelib"] +dependencies = [ + "flask>=2.2", + "msgspec>=0.18.6", + "cachelib" +] dynamic = ["version"] [project.urls] From 563d5a4f566874225820250e881fb9a38a497201 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 16:34:55 -0300 Subject: [PATCH 28/34] rename postgres -> postgresql --- docs/api.rst | 2 +- src/flask_session/{postgres => postgresql}/__init__.py | 0 src/flask_session/{postgres => postgresql}/_queries.py | 0 .../{postgres/postgres.py => postgresql/postgresql.py} | 0 tests/test_postgresql.py | 4 ++-- 5 files changed, 3 insertions(+), 3 deletions(-) rename src/flask_session/{postgres => postgresql}/__init__.py (100%) rename src/flask_session/{postgres => postgresql}/_queries.py (100%) rename src/flask_session/{postgres/postgres.py => postgresql/postgresql.py} (100%) diff --git a/docs/api.rst b/docs/api.rst index 31e1e2b1..faae610d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -21,4 +21,4 @@ Anything documented here is part of the public API that Flask-Session provides, .. autoclass:: flask_session.mongodb.MongoDBSessionInterface .. autoclass:: flask_session.sqlalchemy.SqlAlchemySessionInterface .. autoclass:: flask_session.dynamodb.DynamoDBSessionInterface -.. autoclass:: flask_session.postgres.PostgreSqlSessionInterface \ No newline at end of file +.. autoclass:: flask_session.postgresql.PostgreSqlSessionInterface \ No newline at end of file diff --git a/src/flask_session/postgres/__init__.py b/src/flask_session/postgresql/__init__.py similarity index 100% rename from src/flask_session/postgres/__init__.py rename to src/flask_session/postgresql/__init__.py diff --git a/src/flask_session/postgres/_queries.py b/src/flask_session/postgresql/_queries.py similarity index 100% rename from src/flask_session/postgres/_queries.py rename to src/flask_session/postgresql/_queries.py diff --git a/src/flask_session/postgres/postgres.py b/src/flask_session/postgresql/postgresql.py similarity index 100% rename from src/flask_session/postgres/postgres.py rename to src/flask_session/postgresql/postgresql.py diff --git a/tests/test_postgresql.py b/tests/test_postgresql.py index 0d52e84a..05a9e555 100644 --- a/tests/test_postgresql.py +++ b/tests/test_postgresql.py @@ -5,7 +5,7 @@ from itsdangerous import want_bytes from psycopg2.pool import ThreadedConnectionPool -from flask_session.postgres import PostgreSqlSession +from flask_session.postgresql import PostgreSqlSession TEST_DB = "postgresql://root:pwd@localhost:5433/dummy" @@ -35,7 +35,7 @@ def retrieve_stored_session(self, key): return want_bytes(session_data[0].tobytes()) return None - def test_postgres(self, app_utils): + def test_postgresql(self, app_utils): with self.setup_postgresql(app_utils), self.app.test_request_context(): assert isinstance(flask.session, PostgreSqlSession) app_utils.test_session(self.app) From db586abcc84a3a1e63c10abf62163b713e68f61a Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 16:36:25 -0300 Subject: [PATCH 29/34] undo formatting changes --- pyproject.toml | 6 +++--- src/flask_session/__init__.py | 2 +- src/flask_session/postgresql/__init__.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b66f1cc3..5db4a0e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,9 +2,9 @@ name = "Flask-Session" description = "Server-side session support for Flask" readme = "README.md" -license = { text = "BSD-3-Clause" } -maintainers = [{ name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com" }] -authors = [{ name = "Shipeng Feng", email = "fsp261@gmail.com" }] +license = {text = "BSD-3-Clause"} +maintainers = [{name = "Pallets Community Ecosystem", email = "contact@palletsprojects.com"}] +authors = [{name = "Shipeng Feng", email = "fsp261@gmail.com"}] classifiers = [ "Development Status :: 4 - Beta", "Environment :: Web Environment", diff --git a/src/flask_session/__init__.py b/src/flask_session/__init__.py index 25dff6ba..f527beb5 100644 --- a/src/flask_session/__init__.py +++ b/src/flask_session/__init__.py @@ -194,7 +194,7 @@ def _get_interface(self, app): ) elif SESSION_TYPE == "postgresql": - from .postgres import PostgreSqlSessionInterface + from .postgresql import PostgreSqlSessionInterface session_interface = PostgreSqlSessionInterface( **common_params, diff --git a/src/flask_session/postgresql/__init__.py b/src/flask_session/postgresql/__init__.py index f78d9a79..0d51b3a8 100644 --- a/src/flask_session/postgresql/__init__.py +++ b/src/flask_session/postgresql/__init__.py @@ -1 +1 @@ -from .postgres import PostgreSqlSession, PostgreSqlSessionInterface # noqa: F401 +from .postgresql import PostgreSqlSession, PostgreSqlSessionInterface # noqa: F401 From 38c1d8165eb8739f19e2566a11448719f3dcb4ba Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 16:36:56 -0300 Subject: [PATCH 30/34] undo formatting changes --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5db4a0e5..f4aaed36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,8 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "flask>=2.2", - "msgspec>=0.18.6", + "flask>=2.2", + "msgspec>=0.18.6", "cachelib" ] dynamic = ["version"] From ab21ea1a620c54671c0c7b96b5a80e4508ddc155 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 16:39:37 -0300 Subject: [PATCH 31/34] undo formatting changes --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f4aaed36..da444405 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ requires-python = ">=3.8" dependencies = [ "flask>=2.2", "msgspec>=0.18.6", - "cachelib" + "cachelib", ] dynamic = ["version"] From bd4623c2eb9df9ffd9a6812f09926a97cdef3615 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Wed, 3 Apr 2024 16:40:00 -0300 Subject: [PATCH 32/34] remove debug print statement --- src/flask_session/postgresql/postgresql.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flask_session/postgresql/postgresql.py b/src/flask_session/postgresql/postgresql.py index 48ed3340..e85bd537 100644 --- a/src/flask_session/postgresql/postgresql.py +++ b/src/flask_session/postgresql/postgresql.py @@ -142,5 +142,4 @@ def _upsert_session( def _drop_table(self): with self._get_cursor() as cur: - print(self._queries.drop_sessions_table) cur.execute(self._queries.drop_sessions_table) From bb4259bf24cd020ff3d247d105c2115375af5234 Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Tue, 16 Apr 2024 08:34:24 -0300 Subject: [PATCH 33/34] replace pipes in type annotations with Optional --- src/flask_session/postgresql/postgresql.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/flask_session/postgresql/postgresql.py b/src/flask_session/postgresql/postgresql.py index e85bd537..b5f1e8d6 100644 --- a/src/flask_session/postgresql/postgresql.py +++ b/src/flask_session/postgresql/postgresql.py @@ -2,7 +2,7 @@ from contextlib import contextmanager from datetime import timedelta as TimeDelta -from typing import Generator +from typing import Generator, Optional from flask import Flask from itsdangerous import want_bytes @@ -40,7 +40,7 @@ class PostgreSqlSessionInterface(ServerSideSessionInterface): def __init__( self, app: Flask, - pool: ThreadedConnectionPool | None = Defaults.SESSION_POSTGRESQL, + pool: Optional[ThreadedConnectionPool] = Defaults.SESSION_POSTGRESQL, key_prefix: str = Defaults.SESSION_KEY_PREFIX, use_signer: bool = Defaults.SESSION_USE_SIGNER, permanent: bool = Defaults.SESSION_PERMANENT, @@ -48,7 +48,7 @@ def __init__( serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT, table: str = Defaults.SESSION_POSTGRESQL_TABLE, schema: str = Defaults.SESSION_POSTGRESQL_SCHEMA, - cleanup_n_requests: int | None = Defaults.SESSION_CLEANUP_N_REQUESTS, + cleanup_n_requests: Optional[int] = Defaults.SESSION_CLEANUP_N_REQUESTS, ) -> None: if not isinstance(pool, ThreadedConnectionPool): raise TypeError("No valid ThreadedConnectionPool instance provided.") @@ -74,7 +74,7 @@ def __init__( @contextmanager def _get_cursor( - self, conn: PsycoPg2Connection | None = None + self, conn: Optional[PsycoPg2Connection] = None ) -> Generator[PsycoPg2Cursor, None, None]: _conn: PsycoPg2Connection = conn or self.pool.getconn() @@ -107,7 +107,7 @@ def _delete_session(self, store_id: str) -> None: ) @retry_query(max_attempts=3) - def _retrieve_session_data(self, store_id: str) -> dict | None: + def _retrieve_session_data(self, store_id: str) -> Optional[dict]: with self._get_cursor() as cur: cur.execute( self._queries.retrieve_session_data, From 1108c74c5103438d1ed8c9edacd23ea15408139d Mon Sep 17 00:00:00 2001 From: Giuseppe Papallo Date: Tue, 16 Apr 2024 08:35:02 -0300 Subject: [PATCH 34/34] fix missing return type --- src/flask_session/postgresql/postgresql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flask_session/postgresql/postgresql.py b/src/flask_session/postgresql/postgresql.py index b5f1e8d6..9ad198bb 100644 --- a/src/flask_session/postgresql/postgresql.py +++ b/src/flask_session/postgresql/postgresql.py @@ -140,6 +140,6 @@ def _upsert_session( ), ) - def _drop_table(self): + def _drop_table(self) -> None: with self._get_cursor() as cur: cur.execute(self._queries.drop_sessions_table)