├── fancy_tests
├── __init__.py
└── tests
│ ├── __init__.py
│ ├── models.py
│ ├── templates
│ └── home.html
│ ├── urls.py
│ ├── test_utils.py
│ ├── test_command.py
│ ├── settings.py
│ ├── views.py
│ ├── test_memory.py
│ └── test_views.py
├── example
├── example
│ ├── __init__.py
│ ├── app
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── templates
│ │ │ ├── page.html
│ │ │ └── home.html
│ │ ├── urls.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── urls.py
│ ├── wsgi.py
│ └── settings.py
└── manage.py
├── fancy_cache
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── fancy-urls.py
├── constants.py
├── __init__.py
├── urls.py
├── cache_page.py
├── views.py
├── utils.py
├── templates
│ └── fancy-cache
│ │ └── home.html
├── memory.py
└── middleware.py
├── pyproject.toml
├── requirements.txt
├── .gitignore
├── deploy.sh
├── docs
├── changelog.rst
├── index.rst
├── gettingstarted.rst
├── stats.rst
├── Makefile
├── conf.py
└── gettingfancy.rst
├── MANIFEST.in
├── .github
└── workflows
│ ├── black.yml
│ └── ci.yml
├── runtests.py
├── fabfile.py
├── tox.ini
├── LICENSE
├── setup.py
└── README.rst
/fancy_tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/example/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/example/app/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fancy_tests/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fancy_cache/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fancy_cache/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/fancy_tests/tests/models.py:
--------------------------------------------------------------------------------
1 | # lonely in here
2 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.black]
2 | line-length = 80
3 |
--------------------------------------------------------------------------------
/fancy_tests/tests/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 | Random:{{ random_string }}
3 |
4 |
--------------------------------------------------------------------------------
/example/example/app/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/fancy_cache/constants.py:
--------------------------------------------------------------------------------
1 | REMEMBERED_URLS_KEY = "fancy-urls"
2 | LONG_TIME = 60 * 60 * 24 * 30
3 |
--------------------------------------------------------------------------------
/fancy_cache/__init__.py:
--------------------------------------------------------------------------------
1 | from .cache_page import cache_page # NOQA
2 |
3 | __version__ = "1.3.1"
4 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Tests
2 | Django>=3.2.0,<4.1
3 | django-nose
4 | fabric
5 | pylibmc
6 | sphinx
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | build/
3 | fancy_cache.egg-info/
4 | dist/
5 | django_fancy_cache.egg-info/
6 | cover/
7 | docs/_build/
8 | .tox/
9 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | rm -fr dist/*
5 | python setup.py sdist bdist_wheel
6 | twine check dist/*
7 | twine upload dist/*
8 |
--------------------------------------------------------------------------------
/docs/changelog.rst:
--------------------------------------------------------------------------------
1 | .. index:: changelog
2 |
3 | .. _changelog-chapter:
4 |
5 | See https://github.com/peterbe/django-fancy-cache#changelog
6 |
--------------------------------------------------------------------------------
/fancy_tests/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 |
5 | urlpatterns = [path("", views.home, name="home")]
6 |
--------------------------------------------------------------------------------
/fancy_cache/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 |
5 | urlpatterns = [
6 | path("", views.home, name="home"),
7 | ]
8 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.rst
3 | recursive-include fancy_cache/templates/fancy-cache *.html
4 | recursive-include fancy_tests/tests/templates *.html
5 |
--------------------------------------------------------------------------------
/example/example/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import include, path
2 |
3 |
4 | urlpatterns = [
5 | path("", include("example.app.urls")),
6 | path("fancy-cache", include("fancy_cache.urls")),
7 | ]
8 |
--------------------------------------------------------------------------------
/example/example/app/templates/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fancy! Page {{ page }}
6 |
7 |
8 |
9 | Hello world!
10 | Just so you know, the sum of the first 25,000,000 is {{ result }}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.github/workflows/black.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Run black
3 |
4 | on: [push, pull_request]
5 |
6 | jobs:
7 | black:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: actions/setup-python@v2
12 | with:
13 | python-version: '3.x'
14 | - name: install black
15 | run: pip install black==22.3.0
16 | - name: Black
17 | run: black . --check
18 |
--------------------------------------------------------------------------------
/example/example/app/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 |
4 | urlpatterns = [
5 | path("", views.home, name="home"),
6 | path("page1.html", views.page1, name="page1"),
7 | path("page2.html", views.page2, name="page2"),
8 | path("page3.html", views.page3, name="page3"),
9 | path("page4.html", views.page4, name="page4"),
10 | path("page5.html", views.page5, name="page5"),
11 | ]
12 |
--------------------------------------------------------------------------------
/example/example/app/tests.py:
--------------------------------------------------------------------------------
1 | """
2 | This file demonstrates writing tests using the unittest module. These will pass
3 | when you run "manage.py test".
4 |
5 | Replace this with more appropriate tests for your application.
6 | """
7 |
8 | from django.test import TestCase
9 |
10 |
11 | class SimpleTest(TestCase):
12 | def test_basic_addition(self):
13 | """
14 | Tests that 1 + 1 always equals 2.
15 | """
16 | self.assertEqual(1 + 1, 2)
17 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | # make sure we're running the fancy_cache here and not anything installed
6 | parent = os.path.normpath(os.path.join(__file__, "../.."))
7 | sys.path.insert(0, parent)
8 |
9 | if __name__ == "__main__":
10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
11 |
12 | from django.core.management import execute_from_command_line
13 |
14 | execute_from_command_line(sys.argv)
15 |
--------------------------------------------------------------------------------
/fancy_cache/cache_page.py:
--------------------------------------------------------------------------------
1 | import typing
2 |
3 | from django.utils.decorators import decorator_from_middleware_with_args
4 |
5 | from .middleware import FancyCacheMiddleware
6 |
7 |
8 | def cache_page(
9 | timeout: typing.Optional[float],
10 | *,
11 | cache: str = None,
12 | key_prefix: str = None,
13 | **kwargs
14 | ) -> typing.Callable:
15 | return decorator_from_middleware_with_args(FancyCacheMiddleware)(
16 | page_timeout=timeout, cache_alias=cache, key_prefix=key_prefix, **kwargs
17 | )
18 |
--------------------------------------------------------------------------------
/fancy_cache/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.conf import settings
3 |
4 | from fancy_cache.memory import find_urls
5 |
6 |
7 | def home(request):
8 | data = {
9 | "found": find_urls([]),
10 | "remember_all_urls_setting": getattr(
11 | settings, "FANCY_REMEMBER_ALL_URLS", False
12 | ),
13 | "remember_stats_all_urls_setting": getattr(
14 | settings, "FANCY_REMEMBER_STATS_ALL_URLS", False
15 | ),
16 | }
17 | return render(request, "fancy-cache/home.html", data)
18 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. django-fancy-cache documentation master file, created by
2 | sphinx-quickstart on Sun Feb 10 16:18:37 2013.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to django-fancy-cache's documentation!
7 | ==============================================
8 |
9 | Contents:
10 |
11 | .. toctree::
12 | :maxdepth: 2
13 |
14 | gettingstarted
15 | gettingfancy
16 | stats
17 | changelog
18 |
19 |
20 |
21 |
22 | Indices and tables
23 | ==================
24 |
25 | * :ref:`genindex`
26 | * :ref:`modindex`
27 | * :ref:`search`
28 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | import django
5 | from django.test.utils import get_runner
6 | from django.conf import settings
7 |
8 |
9 | def runtests():
10 | test_dir = os.path.join(os.path.dirname(__file__), "fancy_tests/tests")
11 | sys.path.insert(0, test_dir)
12 |
13 | os.environ["DJANGO_SETTINGS_MODULE"] = "settings"
14 | django.setup()
15 |
16 | TestRunner = get_runner(settings)
17 | test_runner = TestRunner(interactive=False, failfast=False)
18 | failures = test_runner.run_tests(["fancy_tests.tests"])
19 |
20 | sys.exit(bool(failures))
21 |
22 |
23 | if __name__ == "__main__":
24 | runtests()
25 |
--------------------------------------------------------------------------------
/fancy_cache/utils.py:
--------------------------------------------------------------------------------
1 | import hashlib
2 | import time
3 | import typing
4 |
5 |
6 | def md5(x) -> str:
7 | return hashlib.md5(x.encode("utf-8")).hexdigest()
8 |
9 |
10 | def filter_remembered_urls(
11 | remembered_urls: typing.Dict[str, typing.Tuple[str, int]],
12 | ) -> typing.Dict[str, typing.Tuple[str, int]]:
13 | """
14 | Filter out any expired URLs from Fancy Cache's remembered urls.
15 | """
16 | now = int(time.time())
17 | # TODO: Remove the check for tuple in a future release as it will
18 | # no longer be needed once the new dictionary structure {url: (cache_key, expiration_time)}
19 | # has been implemented.
20 | remembered_urls = {
21 | key: value
22 | for key, value in remembered_urls.items()
23 | if isinstance(value, tuple) and value[1] > now
24 | }
25 | return remembered_urls
26 |
--------------------------------------------------------------------------------
/fabfile.py:
--------------------------------------------------------------------------------
1 | """
2 | This Source Code Form is subject to the terms of the Mozilla Public
3 | License, v. 2.0. If a copy of the MPL was not distributed with this
4 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 | """
6 | import os
7 |
8 | from fabric.api import local
9 |
10 |
11 | ROOT = os.path.abspath(os.path.dirname(__file__))
12 | os.environ["PYTHONPATH"] = ROOT
13 |
14 |
15 | def _test(extra_args):
16 | """Run test suite."""
17 | os.environ["DJANGO_SETTINGS_MODULE"] = "fancy_tests.tests.settings"
18 | os.environ["REUSE_DB"] = "0"
19 |
20 | # Add tables and flush DB
21 | local("django-admin.py syncdb --noinput")
22 | local("django-admin.py flush --noinput")
23 |
24 | local("django-admin.py test %s" % extra_args)
25 |
26 |
27 | def test():
28 | _test("-s")
29 |
30 |
31 | def coverage():
32 | _test(
33 | "-s --with-coverage --cover-erase --cover-html "
34 | "--cover-package=fancy_cache"
35 | )
36 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | # Based on
2 | # https://pypi.org/project/tox-gh-actions/
3 |
4 | ---
5 | name: Python
6 |
7 | on: [push, pull_request]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | # PyPy disabled per issue #69:
15 | # https://github.com/peterbe/django-fancy-cache/issues/69
16 | # python-version: [3.7, 3.8, 3.9, "3.10", "pypy-3.8"]
17 | python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
18 |
19 | steps:
20 | - name: apt update
21 | run: sudo apt update
22 | - uses: niden/actions-memcached@v7
23 | - uses: actions/checkout@v2
24 | - name: Set up Python ${{ matrix.python-version }}
25 | uses: actions/setup-python@v2
26 | with:
27 | python-version: ${{ matrix.python-version }}
28 | - name: Install dependencies
29 | run: |
30 | python -m pip install --upgrade pip
31 | pip install tox tox-gh-actions
32 | sudo apt install libmemcached-dev
33 | - name: Test with tox
34 | run: tox -v
35 |
--------------------------------------------------------------------------------
/fancy_tests/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | import time
2 | import unittest
3 |
4 | from django.core.cache import cache
5 |
6 | from fancy_cache.constants import REMEMBERED_URLS_KEY
7 | from fancy_cache.utils import filter_remembered_urls
8 |
9 |
10 | class TestUtils(unittest.TestCase):
11 | def setUp(self):
12 | expiration_time = int(time.time()) + 5
13 | self.urls = {
14 | "/page1.html": ("key1", expiration_time),
15 | "/page2.html": ("key2", expiration_time),
16 | "/page3.html?foo=bar": ("key3", expiration_time),
17 | "/page3.html?foo=else": ("key4", expiration_time),
18 | }
19 | for key, value in self.urls.items():
20 | cache.set(value[0], key)
21 | cache.set(REMEMBERED_URLS_KEY, self.urls, 5)
22 |
23 | def tearDown(self):
24 | cache.clear()
25 |
26 | def test_filter_remembered_urls(self):
27 | remembered_urls = filter_remembered_urls(self.urls)
28 | self.assertDictEqual(remembered_urls, self.urls)
29 |
30 | url = "/page1.html"
31 | self.urls[url] = ("key1", int(time.time()) - 1)
32 | remembered_urls = filter_remembered_urls(self.urls)
33 | self.assertEqual(len(remembered_urls.keys()), len(self.urls.keys()) - 1)
34 | self.assertNotIn(url, remembered_urls.keys())
35 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # Tox (http://tox.testrun.org/) is a tool for running tests
2 | # in multiple virtualenvs. This configuration file will run the
3 | # test suite on all supported python versions. To use it, "pip install tox"
4 | # and then run "tox" from this directory.
5 |
6 | [tox]
7 | envlist = py38-dj32,
8 | py39-dj32,
9 | py310-dj32,
10 | py311-dj32,
11 | py38-dj41,
12 | py39-dj41,
13 | py310-dj41,
14 | py38-dj42,
15 | py39-dj42,
16 | py310-dj42,
17 | py311-dj42
18 | # PyPy disabled per issue #69:
19 | # https://github.com/peterbe/django-fancy-cache/issues/69
20 | # pypy-3-dj22,
21 | # pypy-3-dj31,
22 | # pypy-3-dj32,
23 | # pypy-3-dj40
24 |
25 | [gh-actions]
26 | python =
27 | 3.8: py38
28 | 3.9: py39
29 | 3.10: py310
30 | 3.11: py311
31 | # pypy-3.8: pypy-3
32 |
33 | [testenv]
34 | basepython =
35 | py38: python3.8
36 | py39: python3.9
37 | py310: python3.10
38 | py311: python3.11
39 | # pypy-3: pypy3
40 |
41 | deps =
42 | dj32: Django>=3.2.0, <4.0.0
43 | dj41: Django>=4.1.0, <4.2.0
44 | dj42: Django>=4.2.0, <4.3.0
45 | mock
46 |
47 | usedevelop = true
48 |
49 | commands = pip install -r requirements.txt
50 | {envpython} setup.py test
51 |
--------------------------------------------------------------------------------
/example/example/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for example project.
3 |
4 | This module contains the WSGI application used by Django's development server
5 | and any production WSGI deployments. It should expose a module-level variable
6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
7 | this application via the ``WSGI_APPLICATION`` setting.
8 |
9 | Usually you will have the standard Django WSGI application here, but it also
10 | might make sense to replace the whole Django WSGI application with a custom one
11 | that later delegates to the Django one. For example, you could introduce WSGI
12 | middleware here, or combine a Django application with an application of another
13 | framework.
14 |
15 | """
16 | import os
17 |
18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
19 | # if running multiple sites in the same mod_wsgi process. To fix this, use
20 | # mod_wsgi daemon mode with each site in its own daemon process, or use
21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings"
22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
23 |
24 | # This application object is used by any WSGI server configured to use this
25 | # file. This includes Django's development server, if the WSGI_APPLICATION
26 | # setting points here.
27 | from django.core.wsgi import get_wsgi_application
28 |
29 | application = get_wsgi_application()
30 |
31 | # Apply WSGI middleware here.
32 | # from helloworld.wsgi import HelloWorldApplication
33 | # application = HelloWorldApplication(application)
34 |
--------------------------------------------------------------------------------
/docs/gettingstarted.rst:
--------------------------------------------------------------------------------
1 | .. index:: gettingstarted
2 |
3 | .. _gettingstarted-chapter:
4 |
5 | Getting started
6 | ===============
7 |
8 | The minimal you need to do is install ``django-fancy-cache`` and add
9 | it to a view.
10 |
11 | Installing is easy::
12 |
13 | $ pip install django-fancy-cache
14 |
15 | Now, let's assume you have a ``views.py`` that looks like this::
16 |
17 | from django.shortcuts import render
18 |
19 | def my_view(request):
20 | something_really_slow...
21 | return render(request, 'template.html')
22 |
23 |
24 | What you add is this::
25 |
26 | from django.shortcuts import render
27 | from fancy_cache import cache_page
28 |
29 | @cache_page(3600)
30 | def my_view(request):
31 | something_really_slow...
32 | return render(request, 'template.html')
33 |
34 |
35 | Fancy cache also works for Class-based Views with the help of [Django's `method_decorator`](https://docs.djangoproject.com/en/dev/topics/class-based-views/intro/#decorating-the-class)::
36 |
37 | from django.utils.decorators import method_decorator
38 | from django.views.generic import TemplateView
39 | from fancy_cache import cache_page
40 |
41 |
42 | class MyView(TemplateView):
43 | template_name = "template.html"
44 |
45 | @method_decorator(cache_page(3600))
46 | def get(self, request, *args, **kwargs):
47 | return super().get(request, *args, **kwargs)
48 |
49 |
50 | Getting fancy
51 | -------------
52 |
53 | The above stuff isn't particularly fancy. The next steps is to
54 | start :ref:`gettingfancy-chapter`.
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Peter Bengtsson.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of django-fancy-cache nor the names of its contributors
15 | may be used to endorse or promote products derived from this software
16 | without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/fancy_tests/tests/test_command.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from django.test import TestCase
4 | from nose.tools import ok_
5 | from django.core.cache import cache
6 | from django.core.management import call_command
7 | from io import StringIO
8 | from fancy_cache.constants import REMEMBERED_URLS_KEY
9 |
10 |
11 | class TestBaseCommand(TestCase):
12 | def setUp(self):
13 | expiration_time = int(time.time()) + 5
14 | self.urls = {
15 | "/page1.html": (
16 | "key1",
17 | expiration_time,
18 | ),
19 | "/page2.html": (
20 | "key2",
21 | expiration_time,
22 | ),
23 | "/page3.html?foo=bar": (
24 | "key3",
25 | expiration_time,
26 | ),
27 | "/page3.html?foo=else": (
28 | "key4",
29 | expiration_time,
30 | ),
31 | }
32 | for key, value in self.urls.items():
33 | cache.set(value[0], key)
34 | cache.set(REMEMBERED_URLS_KEY, self.urls, 5)
35 |
36 | def tearDown(self):
37 | cache.clear()
38 |
39 | def test_fancyurls_command(self):
40 | out = StringIO()
41 | call_command("fancy-urls", verbosity=3, stdout=out)
42 | self.assertIn("4 URLs cached", out.getvalue())
43 |
44 | def test_purge_command(self):
45 | out = StringIO()
46 | # Note: first call will show 4 URLs, so call again to confirm deletion
47 | call_command("fancy-urls", "--purge")
48 | call_command("fancy-urls", verbosity=3, stdout=out)
49 | self.assertIn("0 URLs cached", out.getvalue())
50 |
--------------------------------------------------------------------------------
/fancy_cache/templates/fancy-cache/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 | Stats about
12 | django-fancy-cache
13 | usage
14 |
15 |
16 | {% if not remember_all_urls_setting %}
17 |
18 | Note!
19 | You do not have FANCY_REMEMBER_ALL_URLS set in your
20 | settings so cached URLs will not be recorded unless explicitely
21 | set on the cache_page decorator.
22 |
23 | {% else %}
24 | {% if not remember_stats_all_urls_setting %}
25 |
26 | Note!
27 | You do not have FANCY_REMEMBER_STATS_ALL_URLS set in your
28 | settings so statistics about hits and misses will not be recorded.
29 |
30 | {% endif %}
31 |
32 | {% endif %}
33 |
34 | {% for url, cache_key, stats in found %}
35 | {% if forloop.first %}
36 |
37 |
38 | | Path |
39 | Cache key |
40 | Hits |
41 | Misses |
42 |
43 | {% endif %}
44 |
45 | | {{ url }} |
46 | {{ cache_key }} |
47 | {% if stats %}
48 | {{ stats.hits }} |
49 | {{ stats.misses }} |
50 | {% else %}
51 | - |
52 | - |
53 | {% endif %}
54 |
55 | {% if forloop.last %}
56 |
57 | {% endif %}
58 | {% endfor %}
59 |
60 | {% if not found %}
61 | No found URLs in the cache at the moment.
62 |
63 | {% endif %}
64 |
65 |
--------------------------------------------------------------------------------
/example/example/app/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Fancy!
6 |
7 |
8 |
9 | Open each of these pages. You'll notice that they're initially
10 | very slow. That's because they're expensive view functions.
11 | After loading them a second time (within 10 seconds) you'll notice
12 | that they're incredibly fast.
13 |
14 |
15 |
22 |
23 | This is just for the uber-curious about what's going on. These are the current
24 | remembered URLs:
25 |
26 | {% for url, key, stats in remembered_urls %}
27 | {% if forloop.first %}
28 |
29 |
30 | | URL |
31 | Cache key |
32 | Hits |
33 | Misses |
34 |
35 | {% endif %}
36 |
37 | | {{ url }} |
38 | {{ key }} |
39 | {% if stats %}
40 | {{ stats.hits }} |
41 | {{ stats.misses }} |
42 | {% else %}
43 | n/a
44 | {% endif %}
45 | |
46 | {% if forloop.last %}
47 |
48 | {% endif %}
49 | {% endfor %}
50 |
51 |
52 |
--------------------------------------------------------------------------------
/fancy_tests/tests/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | BASE_DIR = os.path.dirname(os.path.dirname(__file__))
4 |
5 | SECRET_KEY = "anything"
6 |
7 | ALLOWED_HOSTS = ("testserver",)
8 |
9 | DATABASES = {
10 | "default": {
11 | "NAME": ":memory:",
12 | "ENGINE": "django.db.backends.sqlite3",
13 | }
14 | }
15 |
16 | CACHES = {
17 | "default": {
18 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
19 | "LOCATION": "unique-snowflake",
20 | },
21 | "second_backend": {
22 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
23 | "LOCATION": "unique-snowflake",
24 | },
25 | "dummy_backend": {
26 | "BACKEND": "django.core.cache.backends.dummy.DummyCache",
27 | "LOCATION": "unique-snowflake",
28 | },
29 | "memcached_backend": {
30 | "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache",
31 | "LOCATION": "127.0.0.1:11211",
32 | "OPTIONS": {"behaviors": {"cas": True}},
33 | },
34 | }
35 |
36 | INSTALLED_APPS = [
37 | "fancy_cache",
38 | "fancy_tests.tests",
39 | "django.contrib.auth",
40 | "django.contrib.contenttypes",
41 | ]
42 |
43 |
44 | ROOT_URLCONF = "fancy_tests.tests.urls"
45 |
46 |
47 | TEMPLATES = [
48 | {
49 | "BACKEND": "django.template.backends.django.DjangoTemplates",
50 | "DIRS": [],
51 | "APP_DIRS": True,
52 | "OPTIONS": {
53 | "context_processors": [
54 | "django.template.context_processors.debug",
55 | "django.template.context_processors.request",
56 | "django.contrib.auth.context_processors.auth",
57 | "django.contrib.messages.context_processors.messages",
58 | ],
59 | },
60 | },
61 | ]
62 |
--------------------------------------------------------------------------------
/fancy_tests/tests/views.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from django.shortcuts import render
3 | from django.views.decorators.cache import never_cache
4 | from fancy_cache import cache_page
5 |
6 |
7 | def _view(request):
8 | random_string = uuid.uuid4().hex
9 | return render(request, "home.html", dict(random_string=random_string))
10 |
11 |
12 | @cache_page(60)
13 | def home(request):
14 | return _view(request)
15 |
16 |
17 | def prefixer1(request):
18 | if request.META.get("AUTH_USER"):
19 | # disable
20 | return None
21 | return "a_key"
22 |
23 |
24 | @cache_page(60, key_prefix=prefixer1)
25 | def home2(request):
26 | return _view(request)
27 |
28 |
29 | def post_processor1(response, request):
30 | assert "In your HTML" not in response.content.decode("utf8")
31 | response.content += ("In your HTML:%s" % uuid.uuid4().hex).encode("utf8")
32 | return response
33 |
34 |
35 | @cache_page(60, key_prefix=prefixer1, post_process_response=post_processor1)
36 | def home3(request):
37 | return _view(request)
38 |
39 |
40 | @cache_page(
41 | 60, key_prefix=prefixer1, post_process_response_always=post_processor1
42 | )
43 | def home4(request):
44 | return _view(request)
45 |
46 |
47 | @cache_page(60, only_get_keys=["foo", "bar"])
48 | def home5(request):
49 | return _view(request)
50 |
51 |
52 | @cache_page(60, forget_get_keys=["bar"])
53 | def home5bis(request):
54 | return _view(request)
55 |
56 |
57 | @cache_page(60, remember_stats_all_urls=True, remember_all_urls=True)
58 | def home6(request):
59 | return _view(request)
60 |
61 |
62 | @cache_page(60, cache="second_backend")
63 | def home7(request):
64 | return _view(request)
65 |
66 |
67 | @cache_page(60, cache="dummy_backend", remember_all_urls=True)
68 | def home8(request):
69 | return _view(request)
70 |
71 |
72 | @never_cache
73 | @cache_page(60, remember_stats_all_urls=True, remember_all_urls=True)
74 | def home9(request):
75 | return _view(request)
76 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import codecs
2 | import os
3 | import re
4 |
5 |
6 | # Prevent spurious errors during `python setup.py test`, a la
7 | # http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html:
8 | try:
9 | import multiprocessing
10 | except ImportError:
11 | pass
12 |
13 | from setuptools import setup, find_packages
14 |
15 |
16 | def read(*parts):
17 | content = codecs.open(
18 | os.path.join(os.path.dirname(__file__), *parts)
19 | ).read()
20 | try:
21 | return content.decode("utf8")
22 | except AttributeError:
23 | # python 3
24 | return content
25 |
26 |
27 | def find_version(*file_paths):
28 | version_file = read(*file_paths)
29 | version_match = re.search(
30 | r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M
31 | )
32 | if version_match:
33 | return version_match.group(1)
34 | raise RuntimeError("Unable to find version string.")
35 |
36 |
37 | setup(
38 | name="django-fancy-cache",
39 | version=find_version("fancy_cache/__init__.py"),
40 | description="A Django 'cache_page' decorator on steroids",
41 | long_description=read("README.rst"),
42 | long_description_content_type="text/x-rst",
43 | author="Peter Bengtsson",
44 | author_email="mail@peterbe.com",
45 | license="BSD",
46 | packages=find_packages(),
47 | include_package_data=True,
48 | zip_safe=False,
49 | classifiers=[
50 | "Development Status :: 4 - Beta",
51 | "Intended Audience :: Developers",
52 | "License :: OSI Approved :: BSD License",
53 | "Operating System :: OS Independent",
54 | "Programming Language :: Python",
55 | "Programming Language :: Python :: 3.8",
56 | "Programming Language :: Python :: 3.9",
57 | "Programming Language :: Python :: 3.10",
58 | "Programming Language :: Python :: 3.11",
59 | ],
60 | tests_require=["nose"],
61 | test_suite="runtests.runtests",
62 | url="https://github.com/peterbe/django-fancy-cache",
63 | )
64 |
--------------------------------------------------------------------------------
/docs/stats.rst:
--------------------------------------------------------------------------------
1 | .. index:: stats
2 |
3 | .. _stats-chapter:
4 |
5 | Stats - hits and misses
6 | =======================
7 |
8 | In :ref:`gettingfancy-chapter` we went through various ways of using
9 | ``django-fancy-cache`` that all have a functional difference.
10 |
11 | What you can do is enable stats so you can get an insight into what
12 | ``django-fancy-cache`` does for you.
13 |
14 | Setting up
15 | ----------
16 |
17 | The first step is to switch on a setting. Put this in your
18 | ``settings``::
19 |
20 | FANCY_REMEMBER_ALL_URLS = True
21 | FANCY_REMEMBER_STATS_ALL_URLS = True
22 |
23 | The other thing you need to do is to add ``fancy_cache`` to
24 | ``INSTALLED_APPS`` like this::
25 |
26 | INSTALLED_APPS += ('fancy_cache',)
27 |
28 | The first one is to tell ``django-fancy-cache`` to remember every URL
29 | it caches and the second one is to keep track of how many hits and
30 | misses you have per URL.
31 |
32 | This will enable stats collections on all uses of the ``cache_page``
33 | decorator. Alternatively you can do it explicitely on just one view.
34 | Like this::
35 |
36 | from django.shortcuts import render
37 | from fancy_cache import cache_page
38 |
39 | @cache_page(3600,
40 | remember_all_urls=True,
41 | remember_stats_all_urls=True)
42 | def my_view(request):
43 | something_really_slow...
44 | return render(request, 'template.html')
45 |
46 |
47 | Now, run your view a couple of times and then you can use the
48 | management command to get an output of this::
49 |
50 | $ ./manage.py fancy-urls
51 | /page5.html HITS 62 MISSES 5
52 | /page4.html HITS 4 MISSES 1
53 |
54 | Another way add ``django-fancy-cache`` to your root urls.py like this::
55 |
56 | urlpatterns = [
57 | ...your other stuff...,
58 | path('fancy-cache', include('fancy_cache.urls')),
59 | ]
60 |
61 |
62 | Now you can visit ``http://localhost:8000/fancy-cache``. It'll give
63 | you a very basic table with all cache keys, their hits and misses.
64 |
--------------------------------------------------------------------------------
/fancy_cache/management/commands/fancy-urls.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 | import os
3 |
4 | _this_wo_ext = os.path.basename(__file__).rsplit(".", 1)[0]
5 |
6 | __doc__ = """
7 | If you enable `FANCY_REMEMBER_ALL_URLS` then every URL take is turned
8 | into a cache key for cache_page() to remember is recorded.
9 | You can use this to do statistics or to do invalidation by URL.
10 |
11 | To use: simply add the URL patterns after like this::
12 |
13 | $ ./manage.py %(this_file)s /path1.html /path3/*/*.json
14 |
15 | To show all cached URLs simply run it with no pattern like this::
16 |
17 | $ ./manage.py %(this_file)s
18 |
19 | Equally the ``--purge`` switch can always be added. For example,
20 | running this will purge all cached URLs::
21 |
22 | $ ./manage.py %(this_file)s --purge
23 |
24 | If you enable `FANCY_REMEMBER_STATS_ALL_URLS` you can get a tally for each
25 | URL how many cache HITS and MISSES it has had.
26 |
27 | """ % dict(
28 | this_file=_this_wo_ext
29 | )
30 |
31 | from optparse import make_option
32 |
33 | from django.core.management.base import BaseCommand
34 |
35 | from fancy_cache.memory import find_urls
36 |
37 |
38 | class Command(BaseCommand):
39 | help = __doc__.strip()
40 |
41 | def add_arguments(self, parser):
42 | parser.add_argument(
43 | "-p",
44 | "--purge",
45 | dest="purge",
46 | action="store_true",
47 | help="Purge found URLs",
48 | )
49 |
50 | args = "urls"
51 |
52 | def handle(self, *urls, **options):
53 | verbose = int(options["verbosity"]) > 1
54 | _count = 0
55 | for url, cache_key, stats in find_urls(urls, purge=options["purge"]):
56 | _count += 1
57 | if stats:
58 | self.stdout.write(url[:70].ljust(65)),
59 | self.stdout.write("HITS", str(stats["hits"]).ljust(5)),
60 | self.stdout.write("MISSES", str(stats["misses"]).ljust(5))
61 |
62 | else:
63 | self.stdout.write(url)
64 |
65 | if verbose:
66 | self.stdout.write("-- %s URLs cached --" % _count)
67 |
--------------------------------------------------------------------------------
/example/example/app/views.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from django.shortcuts import render, redirect
4 |
5 | from fancy_cache import cache_page
6 | from fancy_cache.memory import find_urls
7 |
8 |
9 | def home(request):
10 | remembered_urls = find_urls([])
11 | return render(request, "home.html", {"remembered_urls": remembered_urls})
12 |
13 |
14 | def commafy(s):
15 | r = []
16 | for i, c in enumerate(reversed(str(s))):
17 | if i and (not (i % 3)):
18 | r.insert(0, ",")
19 | r.insert(0, c)
20 | return "".join(r)
21 |
22 |
23 | @cache_page(60)
24 | def page1(request):
25 | print("CACHE MISS", request.build_absolute_uri())
26 | t0 = time.time()
27 | result = sum(x for x in xrange(25000000))
28 | t1 = time.time()
29 | print(t1 - t0)
30 | return render(request, "page.html", dict(result=commafy(result), page="1"))
31 |
32 |
33 | def key_prefixer(request):
34 | # if it's not there, don't cache
35 | return request.GET.get("number")
36 |
37 |
38 | @cache_page(60, key_prefix=key_prefixer)
39 | def page2(request):
40 | if not request.GET.get("number"):
41 | return redirect(request.build_absolute_uri() + "?number=25000000")
42 | print("CACHE MISS", request.build_absolute_uri())
43 | t0 = time.time()
44 | result = sum(x for x in xrange(25000000))
45 | t1 = time.time()
46 | print(t1 - t0)
47 | return render(request, "page.html", dict(result=commafy(result), page="2"))
48 |
49 |
50 | def post_processor(response, request):
51 | response.content = response.content.replace(
52 | "