├── tests ├── __init__.py ├── test_settings.py └── tests.py ├── session_cleanup ├── __init__.py ├── models.py ├── tasks.py └── settings.py ├── requirements.txt ├── .gitignore ├── MANIFEST.in ├── runtests.py ├── README.rst ├── CHANGELOG.rst ├── setup.py ├── LICENSE └── .circleci └── config.yml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /session_cleanup/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | celery>=4.4.0 2 | -------------------------------------------------------------------------------- /session_cleanup/models.py: -------------------------------------------------------------------------------- 1 | # Create your models here. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[c|o] 2 | .DS_Store 3 | *~ 4 | *.egg-info 5 | build/* 6 | dist/* 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst requirements.txt 2 | recursive-include session_cleanup * 3 | -------------------------------------------------------------------------------- /session_cleanup/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from django.core import management 3 | 4 | 5 | @shared_task 6 | def cleanup(): 7 | """Cleanup expired sessions by using Django management command.""" 8 | management.call_command("clearsessions", verbosity=0) 9 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | DATABASES = { 4 | "default": { 5 | "ENGINE": "django.db.backends.sqlite3", 6 | "NAME": "testdatabase", 7 | } 8 | } 9 | INSTALLED_APPS = [ 10 | "django.contrib.sessions", 11 | 12 | "tests", 13 | ] 14 | SECRET_KEY = "4a1f63ba6b00968e7c5ae3da0996a61d784ec913fa8e7d760740a5129975728d" 15 | -------------------------------------------------------------------------------- /session_cleanup/settings.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from celery.schedules import crontab 4 | 5 | weekly_schedule = { 6 | 'task': 'session_cleanup.tasks.cleanup', 7 | 'schedule': crontab(hour=0, minute=0, day_of_week="sunday"), 8 | } 9 | 10 | nightly_schedule = { 11 | 'task': 'session_cleanup.tasks.cleanup', 12 | 'schedule': crontab(hour=0, minute=0), 13 | } 14 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | import django 6 | from django.conf import settings 7 | from django.test.utils import get_runner 8 | 9 | 10 | if __name__ == "__main__": 11 | os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings" 12 | django.setup() 13 | TestRunner = get_runner(settings) 14 | test_runner = TestRunner() 15 | failures = test_runner.run_tests(["tests"]) 16 | sys.exit(bool(failures)) 17 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Django Session Cleanup 3 | ====================== 4 | 5 | For projects that use the ``cached_db`` or ``db`` session engines, the 6 | ``django_session`` table can get quite large after a while. 7 | 8 | Django provides the 'cleanup' management command for deleting expired sessions 9 | from this table but you have to either run this command manually or 10 | set-up a cron job. 11 | 12 | Django Session Cleanup provides a periodic task for 13 | `Celery `_ that will delete expired sessions. 14 | 15 | Usage 16 | ----- 17 | 18 | 1. Run ``pip install django-session-cleanup``. 19 | 20 | 2. Add ``session_cleanup`` to ``INSTALLED_APPS`` in your project's settings. 21 | 22 | 3. Edit or create ``CELERYBEAT_SCHEDULE`` in your project's settings:: 23 | 24 | from session_cleanup.settings import weekly_schedule 25 | CELERYBEAT_SCHEDULE = { 26 | ... 27 | 'session_cleanup': weekly_schedule 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | Version 4.0.0 (2021-01-20) 6 | ========================== 7 | - Add support for Celery 5. 8 | - Change minimum package requirements to be more accurate. 9 | - Add support for Python 3.9 and Django 3.1. 10 | - Remove support for Django 1.11, 2.0, and 2.1. 11 | 12 | Version 3.0.0 (2020-02-03) 13 | ========================== 14 | - Add support for Python 3.8 and Django 3.0. 15 | - Remove support for Python 2.7. 16 | 17 | Version 2.0.0 (2019-11-24) 18 | ========================== 19 | - Add support for Django 2.1 and 2.2. 20 | - Remove explicit support for Python 3.4 and Django <1.11. 21 | 22 | Version 1.0.0 (2018-04-09) 23 | ========================== 24 | - Add support for Django 1.11 and 2.0. 25 | - Remove explicit support for Django <1.5. 26 | - Use the Django management command "clearsessions" to remove sessions. 27 | - Add continuous automated testing. 28 | 29 | Version 0.0.1 (2012-05-25) 30 | ========================== 31 | - Initial release. 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | with open('README.rst') as f: 7 | readme = f.read() 8 | 9 | setup( 10 | name='django-session-cleanup', 11 | version='4.0.0', 12 | description=('A periodic task for removing expired Django sessions ' 13 | 'with Celery.'), 14 | long_description=readme, 15 | author='Elijah Rutschman', 16 | author_email='elijahr+django-session-cleanup@gmail.com', 17 | maintainer='Martey Dodoo', 18 | maintainer_email='martey+django-session-cleanup@mobolic.com', 19 | url='https://github.com/mobolic/django-session-cleanup', 20 | classifiers=[ 21 | 'Framework :: Django :: 2.2', 22 | 'Framework :: Django :: 3.0', 23 | 'Framework :: Django :: 3.1', 24 | 'License :: OSI Approved :: BSD License', 25 | 'Programming Language :: Python :: 3.5', 26 | 'Programming Language :: Python :: 3.6', 27 | 'Programming Language :: Python :: 3.7', 28 | 'Programming Language :: Python :: 3.8', 29 | 'Programming Language :: Python :: 3.9', 30 | ], 31 | packages=find_packages(exclude=('tests',)) 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Elijah Rutschman 2 | Copyright (c) 2018-2021, Mobolic 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from importlib import import_module 3 | 4 | from django.conf import settings 5 | from django.test import TestCase 6 | from django.test.utils import override_settings 7 | from django.utils import timezone 8 | 9 | from session_cleanup.tasks import cleanup 10 | 11 | 12 | class CleanupTest(TestCase): 13 | @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file") 14 | @override_settings(SESSION_SERIALIZER="django.contrib.sessions.serializers.PickleSerializer") # noqa: E501 15 | def test_session_cleanup(self): 16 | """ 17 | Tests that sessions are deleted by the task 18 | """ 19 | engine = import_module(settings.SESSION_ENGINE) 20 | SessionStore = engine.SessionStore 21 | 22 | now = timezone.now() 23 | last_week = now - datetime.timedelta(days=7) 24 | stores = [] 25 | unexpired_stores = [] 26 | expired_stores = [] 27 | 28 | # create unexpired sessions 29 | for i in range(20): 30 | store = SessionStore() 31 | store.save() 32 | stores.append(store) 33 | 34 | for store in stores: 35 | self.assertEqual(store.exists(store.session_key), True, 36 | "Session store could not be created.") 37 | 38 | unexpired_stores = stores[:10] 39 | expired_stores = stores[10:] 40 | 41 | # expire some sessions 42 | for store in expired_stores: 43 | store.set_expiry(last_week) 44 | store.save() 45 | 46 | cleanup() 47 | 48 | for store in unexpired_stores: 49 | self.assertEqual(store.exists(store.session_key), True, 50 | "Unexpired session was deleted by cleanup.") 51 | 52 | for store in expired_stores: 53 | self.assertEqual(store.exists(store.session_key), False, 54 | "Expired session was not deleted by cleanup.") 55 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | commands: 3 | install-dependencies: 4 | description: "Install Python dependencies into a virtualenv." 5 | parameters: 6 | django_version: 7 | type: string 8 | steps: 9 | - checkout 10 | - run: 11 | name: install Python virtualenv 12 | command: | 13 | mkdir -p ~/venv 14 | if [ $(python -c "import platform; print(platform.python_version_tuple()[0])") == "2" ]; then 15 | virtualenv ~/venv; 16 | else 17 | python -m venv ~/venv; 18 | fi; 19 | - run: 20 | name: activate Python virtualenv 21 | command: echo "source ~/venv/bin/activate" >> $BASH_ENV 22 | - run: pip install coverage 23 | - run: pip install django==<< parameters.django_version >> 24 | - run: pip install -r ~/project/requirements.txt 25 | download-codeclimate-reporter: 26 | description: "Download CodeClimate test coverage reporter utility." 27 | steps: 28 | - run: curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 29 | - run: chmod +x ./cc-test-reporter 30 | run-automated-tests: 31 | steps: 32 | - run: 33 | name: activate Python virtualenv 34 | command: echo "source ~/venv/bin/activate" >> $BASH_ENV 35 | - run: ./cc-test-reporter before-build 36 | - run: PYTHONWARNINGS=all coverage run --include="session_cleanup/*" runtests.py 37 | - run: coverage xml 38 | - run: ./cc-test-reporter format-coverage -t coverage.py -o "coverage/codeclimate.$CIRCLE_NODE_INDEX.json" 39 | upload-coverage-report-to-codeclimate: 40 | steps: 41 | - run: ./cc-test-reporter sum-coverage -o - -p $CIRCLE_NODE_TOTAL coverage/codeclimate.*.json | ./cc-test-reporter upload-coverage --debug -i - 42 | jobs: 43 | lint: 44 | docker: 45 | - image: circleci/python:3.9 46 | steps: 47 | - checkout 48 | - run: 49 | name: install and activate virtualenv 50 | command: | 51 | python -m venv ~/venv 52 | echo "source ~/venv/bin/activate" >> $BASH_ENV 53 | - run: 54 | name: run linting 55 | command: | 56 | pip install flake8 57 | flake8 58 | test: 59 | parameters: 60 | django_version: 61 | type: string 62 | python_version: 63 | type: string 64 | default: latest 65 | docker: 66 | - image: circleci/python:<< parameters.python_version >> 67 | steps: 68 | - install-dependencies: 69 | django_version: << parameters.django_version >> 70 | - download-codeclimate-reporter 71 | - run-automated-tests 72 | - upload-coverage-report-to-codeclimate 73 | workflows: 74 | lint-and-test: 75 | jobs: 76 | - lint 77 | - test: 78 | name: "Python 3.5, Django 2.2" 79 | django_version: "2.2.*" 80 | python_version: "3.5" 81 | requires: 82 | - lint 83 | - test: 84 | name: "Python 3.6, Django 2.2" 85 | django_version: "2.2.*" 86 | python_version: "3.6" 87 | requires: 88 | - lint 89 | - test: 90 | name: "Python 3.7, Django 2.2" 91 | django_version: "2.2.*" 92 | python_version: "3.7" 93 | requires: 94 | - lint 95 | - test: 96 | name: "Python 3.8, Django 2.2" 97 | django_version: "2.2.*" 98 | python_version: "3.8" 99 | requires: 100 | - lint 101 | - test: 102 | name: "Python 3.9, Django 2.2" 103 | django_version: "2.2.*" 104 | python_version: "3.9" 105 | requires: 106 | - lint 107 | - test: 108 | name: "Python 3.6, Django 3.0" 109 | django_version: "3.0.*" 110 | python_version: "3.6" 111 | requires: 112 | - lint 113 | - test: 114 | name: "Python 3.7, Django 3.0" 115 | django_version: "3.0.*" 116 | python_version: "3.7" 117 | requires: 118 | - lint 119 | - test: 120 | name: "Python 3.8, Django 3.0" 121 | django_version: "3.0.*" 122 | python_version: "3.8" 123 | requires: 124 | - lint 125 | - test: 126 | name: "Python 3.9, Django 3.0" 127 | django_version: "3.0.*" 128 | python_version: "3.9" 129 | requires: 130 | - lint 131 | - test: 132 | name: "Python 3.9, Django 3.0" 133 | django_version: "3.0.*" 134 | python_version: "3.9" 135 | requires: 136 | - lint 137 | - test: 138 | name: "Python 3.6, Django 3.1" 139 | django_version: "3.1.*" 140 | python_version: "3.6" 141 | requires: 142 | - lint 143 | - test: 144 | name: "Python 3.7, Django 3.1" 145 | django_version: "3.1.*" 146 | python_version: "3.7" 147 | requires: 148 | - lint 149 | - test: 150 | name: "Python 3.8, Django 3.1" 151 | django_version: "3.1.*" 152 | python_version: "3.8" 153 | requires: 154 | - lint 155 | - test: 156 | name: "Python 3.9, Django 3.1" 157 | django_version: "3.1.*" 158 | python_version: "3.9" 159 | requires: 160 | - lint 161 | --------------------------------------------------------------------------------