diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6453893..e415db2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,7 @@ jobs: python -m pip install --upgrade pip pip install setuptools wheel twine python setup.py sdist bdist_wheel + cp dist/django-dysession-${{ env.STRIP_RELEASE_VERSION }}.tar.gz dist/django-dysession.tar.gz - name: Archive wheel artifacts file uses: actions/upload-artifact@v3 diff --git a/README.md b/README.md index d07a425..3e613f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@
django-dysession is a django extension by using AWS DynamoDB as a session backend
- + +@@ -82,9 +83,12 @@ def mainpage(request): request.session["planet_have_been_to"] = ["Earth", "Jupiter", "Saturn"] return HttpResponse("Ayyy") - ``` +Enjoy! + +![](asset/dynamodb-real-demo-image.png) + ## Django Commands diff --git a/asset/banner.png b/asset/banner.png new file mode 100644 index 0000000..9c6de61 Binary files /dev/null and b/asset/banner.png differ diff --git a/asset/dynamodb-real-demo-image.png b/asset/dynamodb-real-demo-image.png new file mode 100644 index 0000000..7057b26 Binary files /dev/null and b/asset/dynamodb-real-demo-image.png differ diff --git a/dysession/aws/dynamodb.py b/dysession/aws/dynamodb.py index 20d2d66..896a959 100644 --- a/dysession/aws/dynamodb.py +++ b/dysession/aws/dynamodb.py @@ -147,6 +147,32 @@ def insert_session_item( return response +def delete_session_item( + data: SessionDataModel, + table_name: Optional[str] = None, +) -> bool: + """Delete a session key""" + + assert type(data.session_key) is str, "session_key should be string type" + + if table_name is None: + table_name = get_config()["DYNAMODB_TABLENAME"] + + resource = boto3.resource("dynamodb", region_name=get_config()["DYNAMODB_REGION"]) + table = resource.Table(table_name) + pk = get_config()["PARTITION_KEY_NAME"] + + insert_item = {pk: data.session_key} + for key in data: + insert_item[key] = data[key] + + response = table.delete_item( + Key={pk: data.session_key}, + ) + + return response + + class DynamoDB: def __init__(self, client=None) -> None: self.client = client @@ -206,3 +232,9 @@ def exists(self, session_key: str) -> bool: ) return key_exists(session_key=session_key) + + def delete(self, data: SessionDataModel, table_name: Optional[str] = None) -> bool: + try: + delete_session_item(data=data, table_name=table_name) + except AssertionError: + raise diff --git a/dysession/backends/db.py b/dysession/backends/db.py index c7bf334..8a57f46 100644 --- a/dysession/backends/db.py +++ b/dysession/backends/db.py @@ -9,6 +9,7 @@ from dysession.aws.dynamodb import DynamoDB from dysession.backends.error import ( + DeleteSessionError, SessionExpired, SessionKeyDoesNotExist, SessionKeyDuplicated, @@ -120,9 +121,13 @@ def delete(self, session_key=None): Delete the session data under this key. If the key is None, use the current session key value. """ + if session_key is None: + session_key = self._session_key + + try: - self.db.delete(session_key=self._session_key) - except: + self.db.delete(self._get_session()) + except DeleteSessionError: pass def load(self) -> SessionDataModel: diff --git a/dysession/backends/error.py b/dysession/backends/error.py index ce37bac..7f42245 100644 --- a/dysession/backends/error.py +++ b/dysession/backends/error.py @@ -8,3 +8,7 @@ class SessionKeyDoesNotExist(Exception): class SessionKeyDuplicated(Exception): ... + + +class DeleteSessionError(Exception): + ... diff --git a/dysession/backends/model.py b/dysession/backends/model.py index b6176ce..261ca6f 100644 --- a/dysession/backends/model.py +++ b/dysession/backends/model.py @@ -1,6 +1,8 @@ import json from typing import Any, Optional +from dysession.settings import get_config + class SessionDataModel: @@ -11,8 +13,8 @@ def __init__(self, session_key: Optional[str] = None) -> None: if type(session_key) is not str and session_key is not None: raise TypeError("session_key should be type str or None") - self.session_key = session_key - self.__variables_names = set(["session_key"]) + self.__variables_names = set([]) + self[get_config()["PARTITION_KEY_NAME"]] = session_key def __getitem__(self, key) -> Any: # Set SESSION_EXPIRE_AT_BROWSER_CLOSE to False @@ -27,10 +29,15 @@ def __getitem__(self, key) -> Any: raise KeyError raise - def __setitem__(self, key, value): - # if key == "session_key": - # raise ValueError() + def __get_session_key(self): + return self[get_config()["PARTITION_KEY_NAME"]] + + def __set_session_key(self, value: Any): + self[get_config()["PARTITION_KEY_NAME"]] = value + session_key = property(__get_session_key, __set_session_key) + + def __setitem__(self, key, value): setattr(self, key, value) self.__variables_names.add(key) @@ -42,7 +49,7 @@ def __iter__(self): return iter(self.__variables_names) def __is_empty(self): - return "session_key" in self.__variables_names and len(self.__variables_names) == 1 + return len(self.__variables_names) == 0 is_empty = property(__is_empty) diff --git a/dysession/version.txt b/dysession/version.txt index 7f20734..e6d5cb8 100644 --- a/dysession/version.txt +++ b/dysession/version.txt @@ -1 +1 @@ -1.0.1 \ No newline at end of file +1.0.2 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 77ef77b..091c7e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,8 @@ description = "django-dysession is a django extension which integrated AWS Dynam long_description = file: README.md long_description_content_type = text/markdown keywords = "django,session,aws,dynamodb" -url = https://github.com/MissterHao/django-dysession/releases/download/v1.0.1/django-dysession-1.0.1.tar.gz -download_url = https://github.com/MissterHao/django-dysession/releases/download/v1.0.1/django-dysession-1.0.1.tar.gz +url = https://github.com/MissterHao/django-dysession/releases/latest/download/django-dysession.tar.gz +download_url = https://github.com/MissterHao/django-dysession/releases/latest/download/django-dysession.tar.gz author = "Hao-Wei, Li" author_email = "henryliking@gmail.com" maintainer = "MissterHao" diff --git a/tests/test_aws_dynamodb.py b/tests/test_aws_dynamodb.py index ed81d8c..742b91a 100644 --- a/tests/test_aws_dynamodb.py +++ b/tests/test_aws_dynamodb.py @@ -6,6 +6,7 @@ from dysession.aws.dynamodb import ( check_dynamodb_table_exists, create_dynamodb_table, + delete_session_item, destory_dynamodb_table, get_item, insert_session_item, @@ -362,9 +363,7 @@ def test_get_item_using_not_exist_key(self): ) with self.assertRaises(DynamodbItemNotFound): - resp = get_item( - session_key="not_exist_key", table_name=options["table"] - ) + resp = get_item(session_key="not_exist_key", table_name=options["table"]) # Insert Item @parameterized.expand( @@ -407,9 +406,7 @@ def test_insert_item_with_tablename(self, session_key: str): resp = insert_session_item(data=model) self.assertEqual(resp["ResponseMetadata"]["HTTPStatusCode"], 200) - resp = get_item( - session_key=session_key, table_name=options["table"] - ) + resp = get_item(session_key=session_key, table_name=options["table"]) self.assertIsInstance(resp, SessionDataModel) @parameterized.expand( @@ -454,3 +451,48 @@ def test_insert_item_without_tablename(self, session_key: str): resp = get_item(session_key=session_key) self.assertIsInstance(resp, SessionDataModel) + + @parameterized.expand( + [ + ["aaaaaaaaa"], + ["bbbbbbbbb"], + ["ccccccccc"], + ] + ) + @mock_dynamodb + def test_delete_item_without_tablename(self, session_key: str): + + options = { + "pk": get_config()["PARTITION_KEY_NAME"], + "sk": get_config()["SORT_KEY_NAME"], + "table": "sessions", + "region": "ap-northeast-1", + } + + client = boto3.client("dynamodb", region_name=options["region"]) + try: + check_dynamodb_table_exists(table_name=options["table"], client=client) + except DynamodbTableNotFound: + create_dynamodb_table( + options={ + "pk": options["pk"], + "sk": options["sk"], + "table": options["table"], + }, + client=client, + ) + + model = SessionDataModel(session_key) + model["a"] = 1 + model["b"] = 2 + model["c"] = 3 + model["d"] = 4 + model["e"] = "qwerty" + + resp = insert_session_item(data=model) + self.assertEqual(resp["ResponseMetadata"]["HTTPStatusCode"], 200) + + resp = delete_session_item(data=model) + + with self.assertRaises(DynamodbItemNotFound): + resp = get_item(session_key=session_key) diff --git a/tests/test_backend_db.py b/tests/test_backend_db.py index ac9ba55..8d10d03 100644 --- a/tests/test_backend_db.py +++ b/tests/test_backend_db.py @@ -233,3 +233,34 @@ def test_exist_check_input_type_error_via_dynamodb_controller( with self.assertRaises(TypeError): db.exists(error_input) + + @mock_dynamodb + def test_delete_item_via_dynamodb_controller(self): + + session_key = "test_set_duplicated_datamodel_via_dynamodb_controller" + self.create_dynamodb_table() + + model = SessionDataModel(session_key) + model["a"] = 1 + model[get_config()["TTL_ATTRIBUTE_NAME"]] = int(datetime.now().timestamp()) + 50 + + db = DynamoDB(self.client) + db.set(model, get_config()["DYNAMODB_TABLENAME"]) + query_model = db.get(session_key=session_key) + self.assertEqual(model.a, query_model.a) + + db.delete(model) + + @mock_dynamodb + def test_delete_item_via_dynamodb_controller_raise_error(self): + + session_key = None + self.create_dynamodb_table() + + model = SessionDataModel(session_key) + model["a"] = 1 + model[get_config()["TTL_ATTRIBUTE_NAME"]] = int(datetime.now().timestamp()) + 50 + + db = DynamoDB(self.client) + with self.assertRaises(AssertionError): + db.delete(model) diff --git a/tests/test_backend_model.py b/tests/test_backend_model.py index 5cd60f6..6c4aead 100644 --- a/tests/test_backend_model.py +++ b/tests/test_backend_model.py @@ -89,13 +89,13 @@ def test_get_function_default_value(self): def test_is_empty(self): model = SessionDataModel() - self.assertTrue(model.is_empty) + self.assertFalse(model.is_empty) model["good_key"] = 0 self.assertFalse(model.is_empty) del model["good_key"] - self.assertTrue(model.is_empty) + self.assertFalse(model.is_empty) def test_iter(self): model = SessionDataModel() @@ -105,7 +105,7 @@ def test_iter(self): model["c"] = 1 model["d"] = 1 - self.assertEqual(set(model), set(["a", "b", "c", "d", "session_key"])) + self.assertEqual(set(model), set(["a", "b", "c", "d", "PK"])) def test_items(self): model = SessionDataModel("session_key") @@ -123,7 +123,7 @@ def test_items(self): values.append(v) - self.assertEqual(set(keys), set(["a", "b", "c", "d", "session_key"])) + self.assertEqual(set(keys), set(["a", "b", "c", "d", "PK"])) self.assertEqual(set(values), set([1, 2, 3, 4, "session_key"])) def test_str_magic_method(self): @@ -137,7 +137,7 @@ def test_str_magic_method(self): data = json.loads(str(model)) - for k in ["session_key", "a", "b", "c", "d"]: + for k in ["PK", "a", "b", "c", "d"]: self.assertIn(k, data.keys()) def test_get__session_expiry(self): @@ -152,4 +152,10 @@ def test_not_found_allow_list(self): model[k] with self.assertRaises(KeyError): - self.assertIsNone(model.get(k)) \ No newline at end of file + self.assertIsNone(model.get(k)) + + + def test_get_and_set_session_key(self): + model = SessionDataModel() + model.session_key = "key" + self.assertEqual(model.session_key, "key") \ No newline at end of file