Skip to content

Commit

Permalink
DynamoDB: Add table_exists parameter and extend documentation (#237)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lxstr authored Apr 18, 2024
2 parents 182bf53 + 3f893da commit bc2fe67
Show file tree
Hide file tree
Showing 8 changed files with 125 additions and 23 deletions.
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
# 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)

0 comments on commit bc2fe67

Please sign in to comment.