Skip to content

Commit bc2fe67

Browse files
authored
DynamoDB: Add table_exists parameter and extend documentation (#237)
2 parents 182bf53 + 3f893da commit bc2fe67

File tree

8 files changed

+125
-23
lines changed

8 files changed

+125
-23
lines changed

CONTRIBUTING.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Install dependencies
1616

1717
.. code-block:: bash
1818
19-
$ pip install -r requirements/dev.in
19+
$ pip install -r requirements/dev.txt
2020
$ pip install -r requirements/docs.in
2121
2222
Install the package in editable mode
@@ -44,7 +44,7 @@ or
4444
4545
$ sphinx-build -b html docs docs/_build
4646
47-
Run the tests together or individually
47+
Run the tests together or individually, requires the docker containers to be up and running (see below)
4848

4949
.. code-block:: bash
5050

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## Contributors
22

3+
- [MauriceBrg](https://github.com/MauriceBrg)
34
- [giuppep](https://github.com/giuppep)
45
- [eiriklid](https://github.com/eiriklid)
56
- [necat1](https://github.com/necat1)

docs/config_reference.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ These are specific to Flask-Session.
1818
- **cachelib**: CacheLibSessionInterface
1919
- **mongodb**: MongoDBSessionInterface
2020
- **sqlalchemy**: SqlAlchemySessionInterface
21+
- **dynamodb**: DynamoDBSessionInterface
2122

2223
.. py:data:: SESSION_PERMANENT
2324
@@ -215,6 +216,12 @@ Dynamodb
215216
216217
Default: ``'Sessions'``
217218
219+
.. py:data:: SESSION_DYNAMODB_TABLE_EXISTS
220+
221+
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.
222+
223+
Default: ``False``
224+
218225
.. deprecated:: 0.7.0
219226

220227
``SESSION_FILE_DIR``, ``SESSION_FILE_THRESHOLD``, ``SESSION_FILE_MODE``. Use ``SESSION_CACHELIB`` instead.

docs/config_serialization.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The msgspec library has speed and memory advantages over other libraries. Howeve
1414
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.
1515

1616
Casting to a supported type:
17-
~~~~~~~~~~~~~~~~~~~~~~~~~~~
17+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1818

1919
.. code-block:: python
2020

src/flask_session/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ def _get_interface(self, app):
106106
SESSION_DYNAMODB_TABLE = config.get(
107107
"SESSION_DYNAMODB_TABLE", Defaults.SESSION_DYNAMODB_TABLE
108108
)
109+
SESSION_DYNAMODB_TABLE_EXISTS = config.get(
110+
"SESSION_DYNAMODB_TABLE_EXISTS", Defaults.SESSION_DYNAMODB_TABLE_EXISTS
111+
)
109112

110113
# PostgreSQL settings
111114
SESSION_POSTGRESQL = config.get(
@@ -191,6 +194,7 @@ def _get_interface(self, app):
191194
**common_params,
192195
client=SESSION_DYNAMODB,
193196
table_name=SESSION_DYNAMODB_TABLE,
197+
table_exists=SESSION_DYNAMODB_TABLE_EXISTS,
194198
)
195199

196200
elif SESSION_TYPE == "postgresql":

src/flask_session/defaults.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,9 @@ class Defaults:
4343
# DynamoDB settings
4444
SESSION_DYNAMODB = None
4545
SESSION_DYNAMODB_TABLE = "Sessions"
46+
SESSION_DYNAMODB_TABLE_EXISTS = False
4647

4748
# PostgreSQL settings
4849
SESSION_POSTGRESQL = None
4950
SESSION_POSTGRESQL_TABLE = "flask_sessions"
50-
SESSION_POSTGRESQL_SCHEMA = "public"
51+
SESSION_POSTGRESQL_SCHEMA = "public"

src/flask_session/dynamodb/dynamodb.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
"""Provides a Session Interface to DynamoDB"""
2+
13
import warnings
24
from datetime import datetime
35
from datetime import timedelta as TimeDelta
46
from decimal import Decimal
57
from typing import Optional
68

79
import boto3
8-
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource
910
from flask import Flask
1011
from itsdangerous import want_bytes
12+
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource
1113

1214
from ..base import ServerSideSession, ServerSideSessionInterface
1315
from ..defaults import Defaults
@@ -20,12 +22,41 @@ class DynamoDBSession(ServerSideSession):
2022
class DynamoDBSessionInterface(ServerSideSessionInterface):
2123
"""A Session interface that uses dynamodb as backend. (`boto3` required)
2224
23-
:param client: A ``DynamoDBServiceResource`` instance.
25+
By default (``table_exists=False``) it will create a DynamoDB table with this configuration:
26+
27+
- Table Name: Value of ``table_name``, by default ``Sessions``
28+
- Key Schema: Simple Primary Key ``id`` of type string
29+
- Billing Mode: Pay per Request
30+
- Time to Live enabled, attribute name: ``expiration``
31+
- The following permissions are required:
32+
- ``dynamodb:CreateTable``
33+
- ``dynamodb:DescribeTable``
34+
- ``dynamodb:UpdateTimeToLive``
35+
- ``dynamodb:GetItem``
36+
- ``dynamodb:UpdateItem``
37+
- ``dynamodb:DeleteItem``
38+
39+
If you set ``table_exists`` to True, you're responsible for creating a table with this config:
40+
41+
- Table Name: Value of ``table_name``, by default ``Sessions``
42+
- Key Schema: Simple Primary Key ``id`` of type string
43+
- Time to Live enabled, attribute name: ``expiration``
44+
- The following permissions are required under these circumstances:
45+
- ``dynamodb:GetItem``
46+
- ``dynamodb:UpdateItem``
47+
- ``dynamodb:DeleteItem``
48+
49+
:param client: A ``DynamoDBServiceResource`` instance, i.e. the result
50+
of ``boto3.resource("dynamodb", ...)``.
2451
:param key_prefix: A prefix that is added to all DynamoDB store keys.
2552
:param use_signer: Whether to sign the session id cookie or not.
2653
:param permanent: Whether to use permanent session or not.
2754
:param sid_length: The length of the generated session id in bytes.
2855
:param table_name: DynamoDB table name to store the session.
56+
:param table_exists: The table already exists, don't try to create it (default=False).
57+
58+
.. versionadded:: 0.9
59+
The `table_exists` parameter was added.
2960
3061
.. versionadded:: 0.6
3162
The `sid_length` parameter was added.
@@ -46,8 +77,11 @@ def __init__(
4677
sid_length: int = Defaults.SESSION_ID_LENGTH,
4778
serialization_format: str = Defaults.SESSION_SERIALIZATION_FORMAT,
4879
table_name: str = Defaults.SESSION_DYNAMODB_TABLE,
80+
table_exists: Optional[bool] = Defaults.SESSION_DYNAMODB_TABLE_EXISTS,
4981
):
5082

83+
# NOTE: The name client is a bit misleading as we're using the resource API of boto3 as opposed to the service API
84+
# which would be instantiated as boto3.client.
5185
if client is None:
5286
warnings.warn(
5387
"No valid DynamoDBServiceResource instance provided, attempting to create a new instance on localhost:8000.",
@@ -62,44 +96,58 @@ def __init__(
6296
aws_secret_access_key="dummy",
6397
)
6498

99+
self.client = client
100+
self.table_name = table_name
101+
102+
if not table_exists:
103+
self._create_table()
104+
105+
self.store = client.Table(table_name)
106+
super().__init__(
107+
app,
108+
key_prefix,
109+
use_signer,
110+
permanent,
111+
sid_length,
112+
serialization_format,
113+
)
114+
115+
def _create_table(self):
65116
try:
66-
client.create_table(
117+
self.client.create_table(
67118
AttributeDefinitions=[
68119
{"AttributeName": "id", "AttributeType": "S"},
69120
],
70-
TableName=table_name,
121+
TableName=self.table_name,
71122
KeySchema=[
72123
{"AttributeName": "id", "KeyType": "HASH"},
73124
],
74125
BillingMode="PAY_PER_REQUEST",
75126
)
76-
client.meta.client.get_waiter("table_exists").wait(TableName=table_name)
77-
client.meta.client.update_time_to_live(
127+
self.client.meta.client.get_waiter("table_exists").wait(
128+
TableName=self.table_name
129+
)
130+
self.client.meta.client.update_time_to_live(
78131
TableName=self.table_name,
79132
TimeToLiveSpecification={
80133
"Enabled": True,
81134
"AttributeName": "expiration",
82135
},
83136
)
84-
except (AttributeError, client.meta.client.exceptions.ResourceInUseException):
137+
except (
138+
AttributeError,
139+
self.client.meta.client.exceptions.ResourceInUseException,
140+
):
85141
# TTL already exists, or table already exists
86142
pass
87143

88-
self.client = client
89-
self.store = client.Table(table_name)
90-
super().__init__(
91-
app,
92-
key_prefix,
93-
use_signer,
94-
permanent,
95-
sid_length,
96-
serialization_format,
97-
)
98-
99144
def _retrieve_session_data(self, store_id: str) -> Optional[dict]:
100145
# Get the saved session (document) from the database
101146
document = self.store.get_item(Key={"id": store_id}).get("Item")
102-
if document:
147+
session_is_not_expired = Decimal(datetime.utcnow().timestamp()) <= document.get(
148+
"expiration"
149+
)
150+
if document and session_is_not_expired:
103151
serialized_session_data = want_bytes(document.get("val").value)
104152
return self.serializer.loads(serialized_session_data)
105153
return None

tests/test_dynamodb.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import boto3
44
import flask
5+
import pytest
56
from flask_session.defaults import Defaults
67
from flask_session.dynamodb import DynamoDBSession
78

@@ -52,3 +53,43 @@ def test_dynamodb_default(self, app_utils):
5253
with app.test_request_context():
5354
assert isinstance(flask.session, DynamoDBSession)
5455
app_utils.test_session(app)
56+
57+
def test_dynamodb_with_existing_table(self, app_utils):
58+
"""
59+
Setting the SESSION_DYNAMODB_TABLE_EXISTS to True for an
60+
existing table shouldn't change anything.
61+
"""
62+
63+
with self.setup_dynamodb():
64+
app = app_utils.create_app(
65+
{
66+
"SESSION_TYPE": "dynamodb",
67+
"SESSION_DYNAMODB": self.client,
68+
"SESSION_DYNAMODB_TABLE_EXISTS": True,
69+
}
70+
)
71+
72+
with app.test_request_context():
73+
assert isinstance(flask.session, DynamoDBSession)
74+
app_utils.test_session(app)
75+
76+
def test_dynamodb_with_existing_table_fails_if_table_doesnt_exist(self, app_utils):
77+
"""Accessing a non-existent table should result in problems."""
78+
79+
app = app_utils.create_app(
80+
{
81+
"SESSION_TYPE": "dynamodb",
82+
"SESSION_DYNAMODB": boto3.resource(
83+
"dynamodb",
84+
endpoint_url="http://localhost:8000",
85+
region_name="us-west-2",
86+
aws_access_key_id="dummy",
87+
aws_secret_access_key="dummy",
88+
),
89+
"SESSION_DYNAMODB_TABLE": "non-existent-123",
90+
"SESSION_DYNAMODB_TABLE_EXISTS": True,
91+
}
92+
)
93+
with app.test_request_context(), pytest.raises(AssertionError):
94+
assert isinstance(flask.session, DynamoDBSession)
95+
app_utils.test_session(app)

0 commit comments

Comments
 (0)