Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DynamoDB: Add table_exists parameter and extend documentation #237

Merged
merged 5 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Install dependencies

.. code-block:: bash

$ pip install -r requirements/dev.in
$ pip install -r requirements/dev.txt
$ pip install -r requirements/docs.in

Install the package in editable mode
Expand Down Expand Up @@ -44,7 +44,7 @@ or

$ sphinx-build -b html docs docs/_build

Run the tests together or individually
Run the tests together or individually, requires the docker containers to be up and running (see below)

.. code-block:: bash

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Contributors

- [MauriceBrg](https://github.com/MauriceBrg)
- [giuppep](https://github.com/giuppep)
- [eiriklid](https://github.com/eiriklid)
- [necat1](https://github.com/necat1)
Expand Down
7 changes: 7 additions & 0 deletions docs/config_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ These are specific to Flask-Session.
- **cachelib**: CacheLibSessionInterface
- **mongodb**: MongoDBSessionInterface
- **sqlalchemy**: SqlAlchemySessionInterface
- **dynamodb**: DynamoDBSessionInterface

.. py:data:: SESSION_PERMANENT

Expand Down Expand Up @@ -215,6 +216,12 @@ Dynamodb

Default: ``'Sessions'``

.. py:data:: SESSION_DYNAMODB_TABLE_EXISTS

By default it will create a new table with the TTL setting activated unless you set this parameter to ``True``, then it assumes that the table already exists.

Default: ``False``

.. deprecated:: 0.7.0

``SESSION_FILE_DIR``, ``SESSION_FILE_THRESHOLD``, ``SESSION_FILE_MODE``. Use ``SESSION_CACHELIB`` instead.
Expand Down
2 changes: 1 addition & 1 deletion docs/config_serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The msgspec library has speed and memory advantages over other libraries. Howeve
If you encounter a TypeError such as: "Encoding objects of type <type> is unsupported", you may be attempting to serialize an unsupported type. In this case, you can either convert the object to a supported type or use a different serializer.

Casting to a supported type:
~~~~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: python

Expand Down
4 changes: 4 additions & 0 deletions src/flask_session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ def _get_interface(self, app):
SESSION_DYNAMODB_TABLE = config.get(
"SESSION_DYNAMODB_TABLE", Defaults.SESSION_DYNAMODB_TABLE
)
SESSION_DYNAMODB_TABLE_EXISTS = config.get(
"SESSION_DYNAMODB_TABLE_EXISTS", Defaults.SESSION_DYNAMODB_TABLE_EXISTS
)

# PostgreSQL settings
SESSION_POSTGRESQL = config.get(
Expand Down Expand Up @@ -191,6 +194,7 @@ def _get_interface(self, app):
**common_params,
client=SESSION_DYNAMODB,
table_name=SESSION_DYNAMODB_TABLE,
table_exists=SESSION_DYNAMODB_TABLE_EXISTS,
)

elif SESSION_TYPE == "postgresql":
Expand Down
3 changes: 2 additions & 1 deletion src/flask_session/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ class Defaults:
# DynamoDB settings
SESSION_DYNAMODB = None
SESSION_DYNAMODB_TABLE = "Sessions"
SESSION_DYNAMODB_TABLE_EXISTS = False

# PostgreSQL settings
SESSION_POSTGRESQL = None
SESSION_POSTGRESQL_TABLE = "flask_sessions"
SESSION_POSTGRESQL_SCHEMA = "public"
SESSION_POSTGRESQL_SCHEMA = "public"
86 changes: 67 additions & 19 deletions src/flask_session/dynamodb/dynamodb.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Provides a Session Interface to DynamoDB"""

import warnings
from datetime import datetime
from datetime import timedelta as TimeDelta
from decimal import Decimal
from typing import Optional

import boto3
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource
from flask import Flask
from itsdangerous import want_bytes
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource

from ..base import ServerSideSession, ServerSideSessionInterface
from ..defaults import Defaults
Expand All @@ -20,12 +22,41 @@ class DynamoDBSession(ServerSideSession):
class DynamoDBSessionInterface(ServerSideSessionInterface):
"""A Session interface that uses dynamodb as backend. (`boto3` required)

:param client: A ``DynamoDBServiceResource`` instance.
By default (``table_exists=False``) it will create a DynamoDB table with this configuration:

- Table Name: Value of ``table_name``, by default ``Sessions``
- Key Schema: Simple Primary Key ``id`` of type string
- Billing Mode: Pay per Request
- Time to Live enabled, attribute name: ``expiration``
- The following permissions are required:
- ``dynamodb:CreateTable``
- ``dynamodb:DescribeTable``
- ``dynamodb:UpdateTimeToLive``
- ``dynamodb:GetItem``
- ``dynamodb:UpdateItem``
- ``dynamodb:DeleteItem``

If you set ``table_exists`` to True, you're responsible for creating a table with this config:

- Table Name: Value of ``table_name``, by default ``Sessions``
- Key Schema: Simple Primary Key ``id`` of type string
- Time to Live enabled, attribute name: ``expiration``
- The following permissions are required under these circumstances:
- ``dynamodb:GetItem``
- ``dynamodb:UpdateItem``
- ``dynamodb:DeleteItem``

:param client: A ``DynamoDBServiceResource`` instance, i.e. the result
of ``boto3.resource("dynamodb", ...)``.
:param key_prefix: A prefix that is added to all DynamoDB store 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 table_name: DynamoDB table name to store the session.
:param table_exists: The table already exists, don't try to create it (default=False).

.. versionadded:: 0.9
The `table_exists` parameter was added.

.. versionadded:: 0.6
The `sid_length` parameter was added.
Expand All @@ -46,8 +77,11 @@ def __init__(
sid_length: int = Defaults.SESSION_ID_LENGTH,
serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT,
table_name: str = Defaults.SESSION_DYNAMODB_TABLE,
table_exists: Optional[bool] = Defaults.SESSION_DYNAMODB_TABLE_EXISTS,
):

# NOTE: The name client is a bit misleading as we're using the resource API of boto3 as opposed to the service API
MauriceBrg marked this conversation as resolved.
Show resolved Hide resolved
# which would be instantiated as boto3.client.
if client is None:
warnings.warn(
"No valid DynamoDBServiceResource instance provided, attempting to create a new instance on localhost:8000.",
Expand All @@ -62,44 +96,58 @@ def __init__(
aws_secret_access_key="dummy",
)

self.client = client
self.table_name = table_name

if not table_exists:
self._create_table()

self.store = client.Table(table_name)
super().__init__(
app,
key_prefix,
use_signer,
permanent,
sid_length,
serialization_format,
)

def _create_table(self):
try:
client.create_table(
self.client.create_table(
AttributeDefinitions=[
{"AttributeName": "id", "AttributeType": "S"},
],
TableName=table_name,
TableName=self.table_name,
KeySchema=[
{"AttributeName": "id", "KeyType": "HASH"},
],
BillingMode="PAY_PER_REQUEST",
)
client.meta.client.get_waiter("table_exists").wait(TableName=table_name)
client.meta.client.update_time_to_live(
self.client.meta.client.get_waiter("table_exists").wait(
TableName=self.table_name
)
self.client.meta.client.update_time_to_live(
TableName=self.table_name,
TimeToLiveSpecification={
"Enabled": True,
"AttributeName": "expiration",
},
)
except (AttributeError, client.meta.client.exceptions.ResourceInUseException):
except (
AttributeError,
self.client.meta.client.exceptions.ResourceInUseException,
):
# TTL already exists, or table already exists
pass

self.client = client
self.store = client.Table(table_name)
super().__init__(
app,
key_prefix,
use_signer,
permanent,
sid_length,
serialization_format,
)

def _retrieve_session_data(self, store_id: str) -> Optional[dict]:
# Get the saved session (document) from the database
document = self.store.get_item(Key={"id": store_id}).get("Item")
if document:
session_is_not_expired = Decimal(datetime.utcnow().timestamp()) <= document.get(
"expiration"
)
if document and session_is_not_expired:
serialized_session_data = want_bytes(document.get("val").value)
return self.serializer.loads(serialized_session_data)
return None
Expand Down
41 changes: 41 additions & 0 deletions tests/test_dynamodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import boto3
import flask
import pytest
from flask_session.defaults import Defaults
from flask_session.dynamodb import DynamoDBSession

Expand Down Expand Up @@ -52,3 +53,43 @@ def test_dynamodb_default(self, app_utils):
with app.test_request_context():
assert isinstance(flask.session, DynamoDBSession)
app_utils.test_session(app)

def test_dynamodb_with_existing_table(self, app_utils):
"""
Setting the SESSION_DYNAMODB_TABLE_EXISTS to True for an
existing table shouldn't change anything.
"""

with self.setup_dynamodb():
app = app_utils.create_app(
{
"SESSION_TYPE": "dynamodb",
"SESSION_DYNAMODB": self.client,
"SESSION_DYNAMODB_TABLE_EXISTS": True,
}
)

with app.test_request_context():
assert isinstance(flask.session, DynamoDBSession)
app_utils.test_session(app)

def test_dynamodb_with_existing_table_fails_if_table_doesnt_exist(self, app_utils):
"""Accessing a non-existent table should result in problems."""

app = app_utils.create_app(
{
"SESSION_TYPE": "dynamodb",
"SESSION_DYNAMODB": boto3.resource(
"dynamodb",
endpoint_url="http://localhost:8000",
region_name="us-west-2",
aws_access_key_id="dummy",
aws_secret_access_key="dummy",
),
"SESSION_DYNAMODB_TABLE": "non-existent-123",
"SESSION_DYNAMODB_TABLE_EXISTS": True,
}
)
with app.test_request_context(), pytest.raises(AssertionError):
assert isinstance(flask.session, DynamoDBSession)
app_utils.test_session(app)
Loading