├── .coveragerc ├── .gitignore ├── .landscape.yaml ├── .travis.yml ├── AUTHORS ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── _templates │ └── .gitkeep ├── conf.py └── index.rst ├── redis_sessions_fork ├── __init__.py ├── backend.py ├── conf.py ├── connection.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── flush_orm_sessions.py │ │ ├── flush_redis_sessions.py │ │ ├── migrate_sessions_to_orm.py │ │ └── migrate_sessions_to_redis.py ├── models.py ├── serializers.py ├── session.py └── utils.py ├── setup.py ├── tests ├── __init__.py └── tests.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = redis_sessions_fork/* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | _build/ 4 | .coverage 5 | build 6 | env* 7 | dist 8 | *.egg-info 9 | *.egg 10 | .idea 11 | *.sublime* 12 | .tox 13 | .eggs 14 | -------------------------------------------------------------------------------- /.landscape.yaml: -------------------------------------------------------------------------------- 1 | doc-warnings: no 2 | test-warnings: no 3 | strictness: veryhigh 4 | max-line-length: 80 5 | autodetect: yes 6 | ignore-paths: 7 | - docs 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | services: 4 | - redis-server 5 | python: 6 | - "3.5" 7 | - "3.4" 8 | - "3.3" 9 | - "2.7" 10 | - "2.6" 11 | - "pypy" 12 | env: 13 | - DJANGO="django>=1.3,<1.4" 14 | - DJANGO="django>=1.4,<1.5" 15 | - DJANGO="django>=1.5,<1.6" 16 | - DJANGO="django>=1.6,<1.7" 17 | - DJANGO="django>=1.7,<1.8" 18 | - DJANGO="django>=1.8,<1.9" 19 | - DJANGO="https://github.com/django/django/tarball/stable/1.9.x#egg=django" 20 | matrix: 21 | exclude: 22 | - python: "3.5" 23 | env: DJANGO="django>=1.3,<1.4" 24 | - python: "3.5" 25 | env: DJANGO="django>=1.4,<1.5" 26 | - python: "3.5" 27 | env: DJANGO="django>=1.5,<1.6" 28 | - python: "3.5" 29 | env: DJANGO="django>=1.6,<1.7" 30 | - python: "3.5" 31 | env: DJANGO="django>=1.7,<1.8" 32 | - python: "3.5" 33 | env: DJANGO="django>=1.8,<1.9" 34 | - python: "3.4" 35 | env: DJANGO="django>=1.3,<1.4" 36 | - python: "3.4" 37 | env: DJANGO="django>=1.4,<1.5" 38 | - python: "3.3" 39 | env: DJANGO="django>=1.3,<1.4" 40 | - python: "3.3" 41 | env: DJANGO="django>=1.4,<1.5" 42 | - python: "3.3" 43 | env: DJANGO="https://github.com/django/django/tarball/stable/1.9.x#egg=django" 44 | - python: "2.6" 45 | env: DJANGO="django>=1.5,<1.6" 46 | - python: "2.6" 47 | env: DJANGO="django>=1.6,<1.7" 48 | - python: "2.6" 49 | env: DJANGO="django>=1.7,<1.8" 50 | - python: "2.6" 51 | env: DJANGO="django>=1.8,<1.9" 52 | - python: "2.6" 53 | env: DJANGO="https://github.com/django/django/tarball/stable/1.9.x#egg=django" 54 | install: 55 | - travis_retry pip install --upgrade pip -q 56 | - travis_retry pip install $DJANGO 57 | - travis_retry pip install nose coverage flake8 flake8-blind-except flake8-debugger flake8-print flake8-quotes pep8-naming frosted isort coveralls -q 58 | - if [[ $TRAVIS_PYTHON_VERSION != 'pypy' ]]; then travis_retry pip install ujson; fi 59 | script: 60 | - python setup.py nosetests --with-coverage 61 | after_success: 62 | - flake8 --show-source redis_sessions_fork 63 | - frosted -r redis_sessions_fork 64 | - isort --check-only -rc redis_sessions_fork --diff 65 | - travis_retry coveralls 66 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ordered by date of first contribution: 2 | Martin Rusev 3 | Chris Jones 4 | Mikhail Andreev 5 | Jeff Baier / http://jeffbaier.com/ 6 | hellysmile 7 | Calvin De Lima 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Martin Rusev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following 12 | disclaimer in the documentation and/or other materials provided 13 | with the distribution. 14 | * Neither the name of the author nor the names of other 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-redis-sessions-fork 2 | ========================== 3 | 4 | :info: Redis Session Backend For Django 5 | 6 | .. image:: https://img.shields.io/travis/hellysmile/django-redis-sessions-fork.svg 7 | :target: https://travis-ci.org/hellysmile/django-redis-sessions-fork 8 | 9 | .. image:: https://img.shields.io/coveralls/hellysmile/django-redis-sessions-fork.svg 10 | :target: https://coveralls.io/r/hellysmile/django-redis-sessions-fork 11 | 12 | .. image:: https://img.shields.io/pypi/dm/django-redis-sessions-fork.svg 13 | :target: https://pypi.python.org/pypi/django-redis-sessions-fork 14 | 15 | .. image:: https://img.shields.io/pypi/v/django-redis-sessions-fork.svg 16 | :target: https://pypi.python.org/pypi/django-redis-sessions-fork 17 | 18 | Features 19 | ******** 20 | 21 | * Fast NoSQL Django sessions backend 22 | * Invalidation via `TTL `_ 23 | * Easy migrations from ``django.contrib.sessions`` 24 | * Fastest session serializers 25 | * Backward migrations to ``django.contrib.sessions`` 26 | 27 | Installation 28 | ************ 29 | 30 | run ``pip install django-redis-sessions-fork`` 31 | 32 | or alternatively download the tarball and run ``python setup.py install`` 33 | 34 | set ``redis_sessions_fork.session`` as your session engine, like so 35 | 36 | .. code-block:: python 37 | 38 | SESSION_ENGINE = 'redis_sessions_fork.session' 39 | 40 | Configuration 41 | ************* 42 | 43 | .. code-block:: python 44 | 45 | # all these options are defaults, you can skip anyone 46 | SESSION_REDIS_HOST = '127.0.0.1' 47 | SESSION_REDIS_PORT = 6379 48 | SESSION_REDIS_DB = 0 49 | SESSION_REDIS_PASSWORD = None 50 | SESSION_REDIS_PREFIX = None 51 | 52 | # if you prefer domain socket connection 53 | # you can just add this line instead of SESSION_REDIS_HOST and SESSION_REDIS_PORT 54 | SESSION_REDIS_UNIX_DOMAIN_SOCKET_PATH = '/var/run/redis/redis.sock' 55 | 56 | # you can also use redis from url 57 | SESSION_REDIS_URL = 'redis://127.0.0.1:6379/0' 58 | 59 | # also available setup connection via redis.ConnectionPool like 60 | SESSION_REDIS_CONNECTION_POOL = 'random.app.redis_connection_pool' 61 | 62 | if you one of happy `heroku.com `_ users 63 | 64 | you can skip redis configuration at all 65 | 66 | ``django-redis-sessions-fork`` already have prefiguration for redis clouds 67 | 68 | Serializer's 69 | ************ 70 | 71 | Django>=1.5.3 `supports `_ different session serializers, such as ``django.contrib.sessions.serializers.PickleSerializer`` and ``django.contrib.sessions.serializers.JSONSerializer`` 72 | 73 | alternative you can use `ujson `_ serializer, which is more faster then default 74 | 75 | .. code-block:: console 76 | 77 | pip install ujson 78 | 79 | then 80 | 81 | .. code-block:: python 82 | 83 | SESSION_SERIALIZER = 'redis_sessions_fork.serializers.UjsonSerializer' 84 | 85 | in addition it is possible to configure `ujson `_ encoding, like 86 | 87 | .. code-block:: python 88 | 89 | SESSION_REDIS_JSON_ENCODING = 'utf8' # default is 'latin-1' 90 | 91 | Sessions migration 92 | ****************** 93 | 94 | add ``redis_sessions_fork`` to your ``INSTALLED_APPS`` 95 | 96 | .. code-block:: console 97 | 98 | # copy orm sessions to redis 99 | python manage.py migrate_sessions_to_redis 100 | # copy redis sessions to orm 101 | python manage.py migrate_sessions_to_orm 102 | # flush redis sessions 103 | python manage.py flush_redis_sessions 104 | # flush orm sessions 105 | python manage.py flush_orm_sessions 106 | 107 | Tests 108 | ***** 109 | 110 | .. code-block:: console 111 | 112 | pip install tox 113 | tox 114 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-redis-sessions-fork.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-redis-sessions-fork.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-redis-sessions-fork" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-redis-sessions-fork" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellysmile/django-redis-sessions-fork/5b8e7c502dacf52555e88b6d67a52ccc0e9ccccc/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellysmile/django-redis-sessions-fork/5b8e7c502dacf52555e88b6d67a52ccc0e9ccccc/docs/_templates/.gitkeep -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-redis-sessions-fork documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Dec 5 07:41:36 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'django-redis-sessions-fork' 47 | copyright = u'2013, hellysmile' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = 'master' 55 | # The full version, including alpha/beta/rc tags. 56 | release = 'master' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all 73 | # documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | # If true, keep warnings as "system message" paragraphs in the built documents. 94 | #keep_warnings = False 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # Add any extra paths that contain custom files (such as robots.txt or 133 | # .htaccess) here, relative to this directory. These files are copied 134 | # directly to the root of the documentation. 135 | #html_extra_path = [] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | #html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | #html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | #html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | #html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | #html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | #html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | #html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | #html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | #html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | #html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | #html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | #html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = 'django-redis-sessions-forkdoc' 180 | 181 | 182 | # -- Options for LaTeX output --------------------------------------------- 183 | 184 | latex_elements = { 185 | # The paper size ('letterpaper' or 'a4paper'). 186 | #'papersize': 'letterpaper', 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | #'pointsize': '10pt', 190 | 191 | # Additional stuff for the LaTeX preamble. 192 | #'preamble': '', 193 | } 194 | 195 | # Grouping the document tree into LaTeX files. List of tuples 196 | # (source start file, target name, title, 197 | # author, documentclass [howto, manual, or own class]). 198 | latex_documents = [ 199 | ('index', 'django-redis-sessions-fork.tex', u'django-redis-sessions-fork Documentation', 200 | u'hellysmile', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | #latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | #latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | #latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | #latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | #latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | #latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output --------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'django-redis-sessions-fork', u'django-redis-sessions-fork Documentation', 230 | [u'hellysmile'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | #man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------- 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'django-redis-sessions-fork', u'django-redis-sessions-fork Documentation', 244 | u'hellysmile', 'django-redis-sessions-fork', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | #texinfo_show_urls = 'footnote' 256 | 257 | # If true, do not generate a @detailmenu in the "Top" node's menu. 258 | #texinfo_no_detailmenu = False 259 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ../README.rst -------------------------------------------------------------------------------- /redis_sessions_fork/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | __version__ = '0.8.0' 4 | -------------------------------------------------------------------------------- /redis_sessions_fork/backend.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.contrib.sessions.backends.base import CreateError 4 | 5 | from . import connection 6 | from .utils import force_unicode, prefix 7 | 8 | 9 | @prefix 10 | def expire(key): 11 | return connection.redis_server.ttl(key) 12 | 13 | 14 | @prefix 15 | def keys(pattern): 16 | return connection.redis_server.keys(pattern) 17 | 18 | 19 | @prefix 20 | def get(key): 21 | value = connection.redis_server.get(key) 22 | 23 | value = force_unicode(value) 24 | 25 | return value 26 | 27 | 28 | @prefix 29 | def exists(key): 30 | return connection.redis_server.exists(key) 31 | 32 | 33 | @prefix 34 | def delete(key): 35 | return connection.redis_server.delete(key) 36 | 37 | 38 | @prefix 39 | def save(key, expire, data, must_create): 40 | expire = int(expire) 41 | 42 | data = force_unicode(data) 43 | 44 | if must_create: 45 | if connection.redis_server.setnx(key, data): 46 | connection.redis_server.expire(key, expire) 47 | else: 48 | raise CreateError 49 | else: 50 | connection.redis_server.setex(key, expire, data) 51 | -------------------------------------------------------------------------------- /redis_sessions_fork/conf.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | 5 | from appconf import AppConf 6 | from django.conf import settings # noqa 7 | 8 | 9 | class SessionRedisConf(AppConf): 10 | HOST = '127.0.0.1' 11 | 12 | PORT = 6379 13 | 14 | DB = 0 15 | 16 | PREFIX = 'django_sessions' 17 | 18 | PASSWORD = None 19 | 20 | UNIX_DOMAIN_SOCKET_PATH = None 21 | 22 | URL = None 23 | 24 | CONNECTION_POOL = None 25 | 26 | JSON_ENCODING = 'latin-1' 27 | 28 | ENV_URLS = ( 29 | 'REDISCLOUD_URL', 30 | 'REDISTOGO_URL', 31 | 'OPENREDIS_URL', 32 | 'REDISGREEN_URL', 33 | 'MYREDIS_URL', 34 | ) 35 | 36 | def configure(self): 37 | if self.configured_data['URL'] is None: 38 | for url in self.configured_data['ENV_URLS']: 39 | redis_env_url = os.environ.get(url) 40 | if redis_env_url: 41 | self.configured_data['URL'] = redis_env_url 42 | break 43 | 44 | return self.configured_data 45 | 46 | class Meta: 47 | prefix = 'session_redis' 48 | -------------------------------------------------------------------------------- /redis_sessions_fork/connection.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import redis 4 | 5 | from .conf import settings 6 | from .utils import import_by_path 7 | 8 | 9 | def get_redis_server(): 10 | if settings.SESSION_REDIS_CONNECTION_POOL is not None: 11 | return redis.StrictRedis( 12 | connection_pool=import_by_path( 13 | settings.SESSION_REDIS_CONNECTION_POOL 14 | ) 15 | ) 16 | 17 | if settings.SESSION_REDIS_URL is not None: 18 | return redis.StrictRedis.from_url( 19 | settings.SESSION_REDIS_URL 20 | ) 21 | 22 | if settings.SESSION_REDIS_UNIX_DOMAIN_SOCKET_PATH is not None: 23 | return redis.StrictRedis( 24 | unix_socket_path=settings.SESSION_REDIS_UNIX_DOMAIN_SOCKET_PATH, 25 | db=settings.SESSION_REDIS_DB, 26 | password=settings.SESSION_REDIS_PASSWORD 27 | ) 28 | 29 | return redis.StrictRedis( 30 | host=settings.SESSION_REDIS_HOST, 31 | port=settings.SESSION_REDIS_PORT, 32 | db=settings.SESSION_REDIS_DB, 33 | password=settings.SESSION_REDIS_PASSWORD 34 | ) 35 | 36 | 37 | redis_server = get_redis_server() 38 | -------------------------------------------------------------------------------- /redis_sessions_fork/management/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | -------------------------------------------------------------------------------- /redis_sessions_fork/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | -------------------------------------------------------------------------------- /redis_sessions_fork/management/commands/flush_orm_sessions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.contrib.sessions.models import Session 4 | from django.core.management.base import NoArgsCommand 5 | from django.db import connection, transaction 6 | from django.db.utils import DatabaseError 7 | 8 | if hasattr(transaction, 'atomic'): 9 | atomic = transaction.atomic 10 | else: 11 | from contextlib import contextmanager 12 | 13 | @contextmanager 14 | def atomic(using=None): 15 | yield 16 | transaction.commit_unless_managed(using=using) 17 | 18 | 19 | class Command(NoArgsCommand): 20 | help = 'flush all django orm sessions' 21 | 22 | def handle_noargs(self, *args, **wargs): 23 | cursor = connection.cursor() 24 | 25 | try: # raw sql truncate 26 | with atomic(): 27 | cursor.execute( 28 | 'TRUNCATE TABLE %s' % Session._meta.db_table 29 | ) 30 | except DatabaseError: # sqlite fix 31 | with atomic(): 32 | cursor.execute( 33 | 'DELETE FROM %s' % Session._meta.db_table 34 | ) 35 | except DatabaseError: # otherwise via django orm 36 | with atomic(): 37 | Session.objects.all.delete() 38 | -------------------------------------------------------------------------------- /redis_sessions_fork/management/commands/flush_redis_sessions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from binascii import Error 4 | 5 | from django.core.management.base import NoArgsCommand 6 | 7 | from ... import backend 8 | from ...session import SessionStore 9 | 10 | 11 | class Command(NoArgsCommand): 12 | help = 'flush all redis sessions' 13 | 14 | def handle_noargs(self, *args, **kwargs): 15 | session_keys = backend.keys('*') 16 | 17 | for session_key in session_keys: 18 | session_data = backend.get(session_key) 19 | 20 | if session_data is not None: 21 | try: 22 | SessionStore().decode(session_data) 23 | backend.delete(session_key) 24 | except (Error, TypeError): 25 | continue 26 | -------------------------------------------------------------------------------- /redis_sessions_fork/management/commands/migrate_sessions_to_orm.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import datetime 4 | from binascii import Error 5 | 6 | from django.contrib.sessions.models import Session 7 | from django.core.management.base import NoArgsCommand 8 | 9 | from ... import backend, utils 10 | from ...session import SessionStore 11 | 12 | try: # Django >= 1.4 13 | from django.utils import timezone 14 | except ImportError: # Django < 1.4 15 | from datetime import datetime as timezone 16 | 17 | 18 | class Command(NoArgsCommand): 19 | help = 'copy redis sessions to django orm' 20 | 21 | def handle_noargs(self, *args, **kwargs): 22 | session_keys = backend.keys('*') 23 | 24 | count = len(session_keys) 25 | counter = 1 26 | 27 | self.stdout.write('sessions to copy %d\n' % count) 28 | 29 | for session_key in session_keys: 30 | self.stdout.write('processing %d of %d\n' % (counter, count)) 31 | 32 | session_data = backend.get(session_key) 33 | 34 | if session_data is not None: 35 | try: 36 | SessionStore().decode(session_data) 37 | except (Error, TypeError): 38 | continue 39 | 40 | expire_date = timezone.now() + datetime.timedelta( 41 | seconds=backend.expire(session_key) 42 | ) 43 | 44 | session_key = utils.remove_prefix(session_key) 45 | 46 | Session.objects.filter(session_key=session_key).delete() 47 | 48 | Session( 49 | session_key=session_key, 50 | session_data=session_data, 51 | expire_date=expire_date 52 | ).save() 53 | 54 | counter += 1 55 | -------------------------------------------------------------------------------- /redis_sessions_fork/management/commands/migrate_sessions_to_redis.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.contrib.sessions.models import Session 4 | from django.core.management.base import NoArgsCommand 5 | 6 | from ... import backend 7 | from ...utils import total_seconds 8 | 9 | try: # Django >= 1.4 10 | from django.utils import timezone 11 | except ImportError: # Django < 1.4 12 | from datetime import datetime as timezone 13 | 14 | 15 | class Command(NoArgsCommand): 16 | help = 'copy django orm sessions to redis' 17 | 18 | def handle_noargs(self, *args, **kwargs): 19 | sessions = Session.objects.filter(expire_date__gt=timezone.now()) 20 | count = sessions.count() 21 | counter = 1 22 | 23 | self.stdout.write('sessions to copy %d\n' % count) 24 | 25 | for session in sessions: 26 | self.stdout.write('processing %d of %d\n' % (counter, count)) 27 | 28 | expire_in = session.expire_date - timezone.now() 29 | expire_in = round(total_seconds(expire_in)) 30 | 31 | if expire_in < 0: 32 | continue 33 | 34 | backend.delete(session.session_key) 35 | 36 | backend.save( 37 | session.session_key, 38 | expire_in, 39 | session.session_data, 40 | False 41 | ) 42 | 43 | counter += 1 44 | -------------------------------------------------------------------------------- /redis_sessions_fork/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | -------------------------------------------------------------------------------- /redis_sessions_fork/serializers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from .conf import settings 4 | 5 | try: 6 | import ujson 7 | 8 | class UjsonSerializer(object): 9 | def dumps(self, obj): 10 | return ujson.dumps(obj).encode( 11 | settings.SESSION_REDIS_JSON_ENCODING 12 | ) 13 | 14 | def loads(self, data): 15 | return ujson.loads( 16 | data.decode(settings.SESSION_REDIS_JSON_ENCODING) 17 | ) 18 | except ImportError: 19 | pass 20 | -------------------------------------------------------------------------------- /redis_sessions_fork/session.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.contrib.sessions.backends.base import CreateError, SessionBase 4 | 5 | from . import backend 6 | 7 | 8 | class SessionStore(SessionBase): 9 | ''' 10 | Redis Session Backend For Django 11 | ''' 12 | def _get_or_create_session_key(self): 13 | if self._session_key is None: 14 | self._session_key = self._get_new_session_key() 15 | 16 | return self._session_key 17 | 18 | def load(self): 19 | session_data = backend.get(self.session_key) 20 | 21 | if session_data is not None: 22 | return self.decode(session_data) 23 | else: 24 | self._session_key = None 25 | return {} 26 | 27 | def exists(self, session_key): 28 | return session_key and backend.exists(session_key) 29 | 30 | def create(self): 31 | while True: 32 | self._session_key = self._get_new_session_key() 33 | 34 | try: 35 | self.save(must_create=True) 36 | except CreateError: 37 | continue 38 | 39 | self.modified = True 40 | self._session_cache = {} 41 | 42 | return 43 | 44 | def save(self, must_create=False): 45 | session_key = self._get_or_create_session_key() 46 | 47 | expire_in = self.get_expiry_age() 48 | 49 | session_data = self.encode(self._get_session(no_load=must_create)) 50 | 51 | backend.save(session_key, expire_in, session_data, must_create) 52 | 53 | def delete(self, session_key=None): 54 | if session_key is None: 55 | if self.session_key is None: 56 | return 57 | 58 | session_key = self.session_key 59 | 60 | backend.delete(session_key) 61 | -------------------------------------------------------------------------------- /redis_sessions_fork/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from functools import wraps 4 | from importlib import import_module 5 | 6 | from .conf import settings 7 | 8 | try: # Python 2.* 9 | from django.utils.encoding import force_unicode 10 | except ImportError: # Python 3.* 11 | from django.utils.encoding import force_text 12 | force_unicode = force_text 13 | 14 | 15 | def add_prefix(key): 16 | if settings.SESSION_REDIS_PREFIX: 17 | if not force_unicode(key).startswith( 18 | '%s:' % settings.SESSION_REDIS_PREFIX 19 | ): 20 | return '%s:%s' % ( 21 | settings.SESSION_REDIS_PREFIX, 22 | key 23 | ) 24 | 25 | return key 26 | 27 | 28 | def remove_prefix(key): 29 | if settings.SESSION_REDIS_PREFIX: 30 | key = str(key) 31 | 32 | if key.startswith(settings.SESSION_REDIS_PREFIX): 33 | key = key.replace( 34 | '%s:' % settings.SESSION_REDIS_PREFIX, '', 1 35 | ) 36 | 37 | return key 38 | 39 | 40 | def prefix(fn): 41 | @wraps(fn) 42 | def wrapped(*args, **kwargs): 43 | args = list(args) 44 | args[0] = add_prefix(args[0]) 45 | return fn(*args, **kwargs) 46 | return wrapped 47 | 48 | 49 | def import_by_path(dotted_path): 50 | try: 51 | module_path, class_name = dotted_path.rsplit('.', 1) 52 | 53 | module = import_module(module_path) 54 | 55 | attr = getattr(module, class_name) 56 | except (ValueError, ImportError, AttributeError): 57 | raise ImportError('can not import %s' % dotted_path) 58 | 59 | return attr 60 | 61 | 62 | def total_seconds(dt): 63 | if hasattr(dt, 'total_seconds'): 64 | return dt.total_seconds() 65 | else: 66 | return ( 67 | (dt.microseconds + (dt.seconds + dt.days * 24 * 3600) * 10 ** 6) 68 | / 69 | 10 ** 6 70 | ) 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os 3 | import codecs 4 | import sys 5 | from setuptools import setup 6 | 7 | 8 | class VersionFinder(ast.NodeVisitor): 9 | def __init__(self): 10 | self.version = None 11 | 12 | def visit_Assign(self, node): # noqa 13 | if node.targets[0].id == '__version__': 14 | self.version = node.value.s 15 | 16 | 17 | def read(*parts): 18 | filename = os.path.join(os.path.dirname(__file__), *parts) 19 | with codecs.open(filename, encoding='utf-8') as fp: 20 | return fp.read() 21 | 22 | 23 | def find_version(*parts): 24 | finder = VersionFinder() 25 | finder.visit(ast.parse(read(*parts))) 26 | return finder.version 27 | 28 | 29 | packages = [ 30 | 'redis_sessions_fork', 31 | 'redis_sessions_fork.management', 32 | 'redis_sessions_fork.management.commands' 33 | ] 34 | 35 | 36 | install_requires = [ 37 | 'redis', 38 | 'django', 39 | 'django_appconf' 40 | ] 41 | 42 | 43 | if '__pypy__' not in sys.builtin_module_names: 44 | install_requires.append('hiredis') 45 | 46 | 47 | if sys.version_info[0:2] < (2, 7): 48 | install_requires.append('importlib') 49 | 50 | 51 | setup( 52 | name='django-redis-sessions-fork', 53 | version=find_version('redis_sessions_fork', '__init__.py'), 54 | description='Redis Session Backend For Django', 55 | long_description=read('README.rst'), 56 | keywords='django, sessions, redis', 57 | author='see AUTHORS', 58 | author_email='hellysmile@gmail.com', 59 | url='https://github.com/hellysmile/django-redis-sessions-fork', 60 | license='BSD', 61 | packages=packages, 62 | zip_safe=False, 63 | install_requires=install_requires, 64 | include_package_data=True, 65 | classifiers=[ 66 | 'Programming Language :: Python :: 2', 67 | 'Programming Language :: Python :: 3', 68 | 'Topic :: Software Development :: Libraries :: Python Modules', 69 | 'Framework :: Django', 70 | 'Environment :: Web Environment', 71 | ], 72 | ) 73 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from django.conf import settings 4 | 5 | 6 | settings.configure( 7 | SESSION_ENGINE='redis_sessions_fork.session', 8 | # SESSION_SERIALIZER='redis_sessions_fork.serializers.UjsonSerializer', 9 | SESSION_REDIS_PREFIX='django_sessions_tests', 10 | INSTALLED_APPS=( 11 | 'django.contrib.sessions', 12 | 'redis_sessions_fork' 13 | ), 14 | DATABASES={ 15 | 'default': { 16 | 'ENGINE': 'django.db.backends.sqlite3', 17 | 'NAME': ':memory:' 18 | } 19 | } 20 | ) 21 | 22 | 23 | try: 24 | # django 1.7 standalone app setup 25 | import django 26 | django.setup() 27 | except AttributeError: 28 | pass 29 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*-. 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | import os 5 | import time 6 | 7 | import django 8 | import redis 9 | from django.contrib.sessions.backends.base import CreateError 10 | from django.contrib.sessions.models import Session 11 | from django.core import management 12 | 13 | from redis_sessions_fork import backend, utils 14 | from redis_sessions_fork.conf import SessionRedisConf, settings 15 | from redis_sessions_fork.connection import get_redis_server 16 | 17 | session_module = utils.import_module(settings.SESSION_ENGINE) 18 | session = session_module.SessionStore() 19 | 20 | test_connection_pool = redis.ConnectionPool( 21 | host=settings.SESSION_REDIS_HOST, 22 | port=settings.SESSION_REDIS_PORT, 23 | db=settings.SESSION_REDIS_DB, 24 | password=settings.SESSION_REDIS_PASSWORD 25 | ) 26 | 27 | if django.VERSION >= (1, 9): 28 | syncdb = 'migrate' 29 | else: 30 | syncdb = 'syncdb' 31 | 32 | management.call_command(syncdb, interactive=False) 33 | 34 | 35 | class SettingsMock(object): 36 | def __init__(self, **kwargs): 37 | self.initial = {} 38 | self.mock = {} 39 | 40 | for key, value in kwargs.items(): 41 | self.initial[key] = getattr(settings, key) 42 | self.mock[key] = value 43 | 44 | def __enter__(self): 45 | for key, value in self.mock.items(): 46 | setattr(settings, key, value) 47 | 48 | def __exit__(self, *args, **kwargs): 49 | for key, value in self.initial.items(): 50 | setattr(settings, key, value) 51 | 52 | 53 | def test_settings_mock(): 54 | assert settings.SESSION_REDIS_PREFIX == 'django_sessions_tests' 55 | 56 | with SettingsMock(SESSION_REDIS_PREFIX='mock'): 57 | settings.SESSION_REDIS_PREFIX == 'mock' 58 | 59 | assert settings.SESSION_REDIS_PREFIX == 'django_sessions_tests' 60 | 61 | 62 | def test_redis_prefix(): 63 | assert utils.add_prefix('foo') == '%s:foo' % settings.SESSION_REDIS_PREFIX 64 | 65 | assert 'foo' == utils.remove_prefix(utils.add_prefix('foo')) 66 | 67 | with SettingsMock(SESSION_REDIS_PREFIX=''): 68 | assert utils.add_prefix('foo') == 'foo' 69 | assert 'foo' == utils.remove_prefix(utils.add_prefix('foo')) 70 | 71 | with SettingsMock(SESSION_REDIS_PREFIX='mock'): 72 | assert utils.add_prefix('foo') == 'mock:foo' 73 | assert 'foo' == utils.remove_prefix(utils.add_prefix('foo')) 74 | 75 | 76 | def test_modify_and_keys(): 77 | assert not session.modified 78 | 79 | session['test'] = 'test_me' 80 | 81 | assert session.modified 82 | 83 | assert session['test'] == 'test_me' 84 | 85 | 86 | def test_save_and_delete(): 87 | session['key'] = 'value' 88 | session.save() 89 | 90 | assert session.exists(session.session_key) 91 | 92 | session.delete(session.session_key) 93 | 94 | assert not session.exists(session.session_key) 95 | 96 | 97 | def test_flush(): 98 | session['key'] = 'another_value' 99 | session.save() 100 | 101 | key = session.session_key 102 | 103 | session.flush() 104 | 105 | assert not session.exists(key) 106 | 107 | 108 | def test_items(): 109 | session['item1'], session['item2'] = 1, 2 110 | session.save() 111 | 112 | # Python 3.* fix 113 | assert sorted(list(session.items())) == [('item1', 1), ('item2', 2)] 114 | 115 | 116 | def test_expiry(): 117 | session.set_expiry(1) 118 | 119 | assert session.get_expiry_age() == 1 120 | 121 | session['key'] = 'expiring_value' 122 | session.save() 123 | 124 | key = session.session_key 125 | 126 | assert session.exists(key) 127 | 128 | time.sleep(2) 129 | 130 | assert not session.exists(key) 131 | 132 | 133 | def test_save_and_load(): 134 | session.set_expiry(60) 135 | session.setdefault('item_test', 8) 136 | session.save() 137 | 138 | session_data = session.load() 139 | 140 | assert session_data.get('item_test') == 8 141 | 142 | 143 | def test_save_and_load_nonascii(): 144 | session['nonascii'] = 'тест' 145 | session.save() 146 | 147 | session_data = session.load() 148 | 149 | assert utils.force_unicode(session_data['nonascii']) == \ 150 | utils.force_unicode('тест') 151 | 152 | 153 | def test_save_existing_key(): 154 | try: 155 | session.save(must_create=True) 156 | 157 | assert False 158 | except CreateError: 159 | pass 160 | 161 | 162 | def test_redis_url_config_from_env(): 163 | os.environ['MYREDIS_URL'] = 'redis://localhost:6379/2' 164 | 165 | _mock_session = SessionRedisConf() 166 | _mock_session.configure() 167 | 168 | with SettingsMock(SESSION_REDIS_URL=_mock_session.configured_data['URL']): 169 | redis_server = get_redis_server() 170 | 171 | host = redis_server.connection_pool.connection_kwargs.get('host') 172 | port = redis_server.connection_pool.connection_kwargs.get('port') 173 | db = redis_server.connection_pool.connection_kwargs.get('db') 174 | 175 | assert host == 'localhost' 176 | assert port == 6379 177 | assert db == 2 178 | 179 | 180 | def test_redis_url_config(): 181 | with SettingsMock( 182 | SESSION_REDIS_URL='redis://localhost:6379/1' 183 | ): 184 | redis_server = get_redis_server() 185 | 186 | host = redis_server.connection_pool.connection_kwargs.get('host') 187 | port = redis_server.connection_pool.connection_kwargs.get('port') 188 | db = redis_server.connection_pool.connection_kwargs.get('db') 189 | 190 | assert host == 'localhost' 191 | assert port == 6379 192 | assert db == 1 193 | 194 | 195 | def test_unix_socket(): 196 | # Uncomment this in `redis.conf`: 197 | # 198 | # unixsocket /tmp/redis.sock 199 | # unixsocketperm 755 200 | with SettingsMock( 201 | SESSION_REDIS_UNIX_DOMAIN_SOCKET_PATH='unix:///tmp/redis.sock' 202 | ): 203 | redis_server = get_redis_server() 204 | 205 | path = redis_server.connection_pool.connection_kwargs.get('path') 206 | db = redis_server.connection_pool.connection_kwargs.get('db') 207 | 208 | assert path == settings.SESSION_REDIS_UNIX_DOMAIN_SOCKET_PATH 209 | 210 | assert db == 0 211 | 212 | 213 | def test_with_connection_pool_config(): 214 | with SettingsMock( 215 | SESSION_REDIS_CONNECTION_POOL='tests.tests.test_connection_pool' 216 | ): 217 | redis_server = get_redis_server() 218 | 219 | assert redis_server.connection_pool == test_connection_pool 220 | 221 | 222 | def test_serializers(): 223 | test_object = {'foo': 'bar'} 224 | 225 | for class_name in ('UjsonSerializer',): 226 | try: 227 | serializer = utils.import_by_path( 228 | 'redis_sessions_fork.serializers.%s' % class_name 229 | )() 230 | except ImportError: 231 | continue 232 | 233 | serializer_data = serializer.loads(serializer.dumps(test_object)) 234 | 235 | assert test_object == serializer_data 236 | 237 | 238 | def test_flush_redis_sessions(): 239 | session['foo'] = 'bar' 240 | session.save() 241 | 242 | keys_before_flush = backend.keys('*') 243 | 244 | management.call_command('flush_redis_sessions') 245 | 246 | keys_after_flush = backend.keys('*') 247 | 248 | assert not keys_before_flush == keys_after_flush 249 | 250 | assert len(keys_after_flush) == 0 251 | 252 | 253 | def test_migrate_to_orm(): 254 | session['foo'] = 'bar' 255 | session.save() 256 | 257 | management.call_command('migrate_sessions_to_orm') 258 | 259 | orm_session = Session.objects.all()[0] 260 | 261 | assert session.decode(orm_session.session_data)['foo'] == 'bar' 262 | 263 | 264 | def test_migrate_to_redis(): 265 | management.call_command('flush_redis_sessions') 266 | 267 | management.call_command('migrate_sessions_to_redis') 268 | 269 | orm_session = Session.objects.all()[0] 270 | 271 | check_session = session_module.SessionStore( 272 | session_key=orm_session.session_key 273 | ) 274 | 275 | assert check_session.load()['foo'] == 'bar' 276 | 277 | 278 | def test_flush_orm_sessions(): 279 | management.call_command('flush_orm_sessions') 280 | 281 | orm_session = Session.objects.all() 282 | 283 | assert orm_session.count() == 0 284 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py2-flake8, 4 | py2-frosted, 5 | py2-isort, 6 | py3-flake8, 7 | py3-frosted, 8 | py3-isort, 9 | py26-dj{13,14}, 10 | py27-dj{13,14,15,16,17,18,19}, 11 | py32-dj{15,16,17,18}, 12 | py33-dj{15,16,17,18}, 13 | py34-dj{15,16,17,18,19}, 14 | py35-dj{19}, 15 | pypy-dj{13,14,15,16,17,18,19} 16 | 17 | [testenv] 18 | basepython = 19 | py2: python2 20 | py3: python3 21 | py26: python2.6 22 | py27: python2.7 23 | py32: python3.2 24 | py33: python3.3 25 | py34: python3.4 26 | py35: python3.5 27 | pypy: pypy 28 | commands = 29 | {envpython} setup.py nosetests 30 | deps = 31 | dj13: django>=1.3,<1.4 32 | dj14: django>=1.4,<1.5 33 | dj15: django>=1.5,<1.6 34 | dj16: django>=1.6,<1.7 35 | dj17: django>=1.7,<1.8 36 | dj18: django>=1.8,<1.9 37 | dj19: https://github.com/django/django/tarball/stable/1.9.x#egg=django 38 | isort: isort 39 | flake8: flake8 40 | flake8: flake8-blind-except 41 | flake8: flake8-debugger 42 | flake8: flake8-print 43 | flake8: flake8-quotes 44 | flake8: pep8-naming 45 | frosted: frosted 46 | nose 47 | 48 | [testenv:py2-flake8] 49 | commands = 50 | flake8 --show-source redis_sessions_fork 51 | 52 | [testenv:py2-frosted] 53 | commands = 54 | frosted -r redis_sessions_fork 55 | 56 | [testenv:py2-isort] 57 | commands = 58 | isort --check-only -rc redis_sessions_fork --diff 59 | 60 | [testenv:py3-flake8] 61 | commands = 62 | flake8 --show-source redis_sessions_fork 63 | 64 | [testenv:py3-frosted] 65 | commands = 66 | frosted -r redis_sessions_fork 67 | 68 | [testenv:py3-isort] 69 | commands = 70 | isort --check-only -rc redis_sessions_fork --diff 71 | --------------------------------------------------------------------------------