django-ormcache 1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-ormcache
3
+ Version: 1.4
4
+ Summary: ORM cache for Django
5
+ Home-page: https://github.com/educreations/django-ormcache
6
+ Author: Corey Farwell
7
+ Author-email: coreyf@rwell.org
8
+ Maintainer: Educreations Engineering
9
+ Maintainer-email: engineering@educreations.com
10
+ License: MIT
11
+ Project-URL: Homepage, https://github.com/educreations/django-ormcache
12
+ Project-URL: Issues, https://github.com/educreations/django-ormcache/issues
13
+ Project-URL: Changelog, https://github.com/educreations/django-ormcache/blob/master/CHANGES.md
14
+ Keywords: cache django
15
+ Classifier: Framework :: Django
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Topic :: Software Development
21
+ Requires-Python: >=3.8, <3.13
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: Django<6.0,>=4.2
25
+ Requires-Dist: six
26
+ Provides-Extra: test
27
+ Requires-Dist: tox; extra == "test"
28
+ Requires-Dist: pytest; extra == "test"
29
+ Requires-Dist: pytest-django; extra == "test"
30
+ Requires-Dist: flake8; extra == "test"
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: keywords
38
+ Dynamic: license
39
+ Dynamic: license-file
40
+ Dynamic: maintainer
41
+ Dynamic: maintainer-email
42
+ Dynamic: project-url
43
+ Dynamic: provides-extra
44
+ Dynamic: requires-dist
45
+ Dynamic: requires-python
46
+ Dynamic: summary
47
+
48
+ # Django ORMCache
49
+
50
+ A cache manager mixin that provides some caching of objects for the ORM.
51
+
52
+
53
+ # Installation / Setup / Usage
54
+
55
+ TODO
56
+
57
+
58
+ # Testing
59
+
60
+ Run the tests with:
61
+
62
+ ```
63
+ pip install -e .[tests]
64
+ tox
65
+ ```
66
+
67
+ # License
68
+
69
+ Copyright © 2024, Educreations, Inc under the [MIT](LICENSE).
@@ -0,0 +1,12 @@
1
+ django_ormcache-1.4.dist-info/licenses/LICENSE,sha256=RVUF4JTQmXwGoB2iQ_ywDydZlJ0ILqbUgdN7DxvayiA,1085
2
+ ormcache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ormcache/decorators.py,sha256=2ZMqQTIJlK0uLCf4Egk5rZhp4WrBzeGyaIScWpT3yp4,1822
4
+ ormcache/managers.py,sha256=s1r9FxqN6pyxxM22aUqdI9IK22a37tTPbtB-gLBlNrQ,2034
5
+ ormcache/models.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ ormcache/queryset.py,sha256=uTFu2IUk16_R9hIZrXjuZQAEllrK-xOr1oVtkOh7cI4,5438
7
+ ormcache/signals.py,sha256=4v7Awa3QIH9pGd-VbEk9jcjQvwBABq-29fmn9p6z1lg,147
8
+ ormcache/utils.py,sha256=RSI___JrEEDEzwb-MjhqtDojYvsoX0qD-6gAObNbkBc,1792
9
+ django_ormcache-1.4.dist-info/METADATA,sha256=cvBPGc2_neM4K27oiCMM6dJAxYbZJqM7_nswsSiyn4M,1763
10
+ django_ormcache-1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ django_ormcache-1.4.dist-info/top_level.txt,sha256=aOHDwDLnkCYseV7Vm27Flc3fKlms_YQQnElZh0fSgR8,9
12
+ django_ormcache-1.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Educreations, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ ormcache
ormcache/__init__.py ADDED
File without changes
ormcache/decorators.py ADDED
@@ -0,0 +1,57 @@
1
+ from hashlib import sha1
2
+ import logging
3
+
4
+ from django.core.cache import cache
5
+ from django.utils.decorators import wraps
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+
10
+ def cache_result(ttl=60 * 5, key=None):
11
+ """
12
+ Cache the result of a function call using Django's caching mechanism.
13
+
14
+ If the decorated function returns None, cache_result raises a
15
+ RuntimeException (as the cache returns None to indicate a miss).
16
+
17
+ Note: If a key is not passed, the decorator will hash the function
18
+ signature. The ordering of function parameters is important. e.g.:
19
+ * myFunction(x=1, y=2)
20
+ * myFunction(y=2, x=1)
21
+ * myFunction(1, 2)
22
+ will each be cached separately.
23
+
24
+ Note: Does not automatically handle invalidation
25
+
26
+ Usage:
27
+ --------------------------------------------------------------------------
28
+ @cache_result
29
+ def my_expensive_method(param1, param2, param3):
30
+ ....
31
+ return expensive_result
32
+ --------------------------------------------------------------------------
33
+ """
34
+
35
+ def decorator(func):
36
+ @wraps(func)
37
+ def inner(*args, **kwargs):
38
+ key_ = key
39
+ if key_ is None:
40
+ func_signature = (
41
+ str(func.__module__) + str(func.__name__) + str(args) + str(kwargs)
42
+ ).encode("utf8")
43
+ key_ = sha1(func_signature).hexdigest()
44
+ result = cache.get(key_)
45
+ if result is None:
46
+ result = func(*args, **kwargs)
47
+ if result is None:
48
+ log.error(
49
+ "Function decorated by @cache_result returned " "None",
50
+ extra={"function": func.__name__},
51
+ )
52
+ cache.set(key_, result, ttl)
53
+ return result
54
+
55
+ return inner
56
+
57
+ return decorator
ormcache/managers.py ADDED
@@ -0,0 +1,59 @@
1
+ from django.db.models.signals import class_prepared, post_delete, post_save
2
+ from django.utils.functional import cached_property
3
+
4
+ from ormcache.queryset import CachedQuerySet
5
+
6
+
7
+ class CachedManagerMixin(object):
8
+ @cached_property
9
+ def __cache_enabled(self):
10
+ return getattr(self.model, "cache_enabled", False)
11
+
12
+ def __require_cache(func):
13
+ def wrapper(self, *args, **kwargs):
14
+ if not self.__cache_enabled:
15
+ error = "Caching is not enabled on {}".format(str(type(self)))
16
+ raise RuntimeError(error)
17
+ return func(self, *args, **kwargs)
18
+
19
+ return wrapper
20
+
21
+ @__require_cache
22
+ def from_ids(self, ids, lookup="pk__in", **kwargs):
23
+ queryset = self.get_queryset()
24
+ return queryset.from_ids(ids, lookup=lookup, **kwargs)
25
+
26
+ @__require_cache
27
+ def invalidate(self, *args, **kwargs):
28
+ return self.get_queryset().invalidate(*args, **kwargs)
29
+
30
+ @__require_cache
31
+ def cache_key(self, *args, **kwargs):
32
+ return self.get_queryset().cache_key(*args, **kwargs)
33
+
34
+ # Django overrides
35
+
36
+ def contribute_to_class(self, model, name):
37
+ """Override Django builtin"""
38
+ super(CachedManagerMixin, self).contribute_to_class(model, name)
39
+ class_prepared.connect(self.__class_prepared_cache, sender=model)
40
+
41
+ def get_queryset(self):
42
+ """Override Django builtin"""
43
+ if self.__cache_enabled:
44
+ return CachedQuerySet(self.model)
45
+ else:
46
+ return super(CachedManagerMixin, self).get_queryset()
47
+
48
+ # Signals
49
+
50
+ def __class_prepared_cache(self, sender, **kwargs):
51
+ if self.__cache_enabled:
52
+ post_save.connect(self.__post_save_cache, sender=self.model, weak=False)
53
+ post_delete.connect(self.__post_delete_cache, sender=self.model, weak=False)
54
+
55
+ def __post_save_cache(self, instance, created, **kwargs):
56
+ self.invalidate(instance.pk, recache=True)
57
+
58
+ def __post_delete_cache(self, instance, **kwargs):
59
+ self.invalidate(instance.pk)
ormcache/models.py ADDED
File without changes
ormcache/queryset.py ADDED
@@ -0,0 +1,149 @@
1
+ from __future__ import unicode_literals
2
+
3
+ try:
4
+ from collections.abc import Iterable
5
+ except ImportError:
6
+ from collections import Iterable
7
+
8
+ import logging
9
+
10
+ import django
11
+ from django.core.cache import cache
12
+ from django.db.models.query import QuerySet
13
+
14
+ if django.VERSION >= (1, 8):
15
+ from django.db.models.expressions import Col
16
+ from django.db.models.lookups import Exact
17
+ elif django.VERSION >= (1, 7):
18
+ from django.db.models.lookups import Exact
19
+ from django.db.models.sql.datastructures import Col
20
+
21
+ from ormcache.signals import cache_hit, cache_missed, cache_invalidated # noqa
22
+
23
+ log = logging.getLogger(__name__)
24
+
25
+
26
+ class CachedQuerySet(QuerySet):
27
+ __MAXIMUM_CACHE_KEY_LENGTH = 250
28
+ __CACHE_FOREVER = 2592000 # http://ur1.ca/egyvu
29
+
30
+ def get(self, *args, **kwargs):
31
+ """
32
+ Adds a layer of caching around the Manager's built in 'get()' method.
33
+ Will only cache if 'pk' or 'id' is used in kwargs. Results from the
34
+ 'get()' method will be cached indefinitely (30 days) until invalidated.
35
+ """
36
+
37
+ orig_kwargs = kwargs.copy()
38
+
39
+ # If this queryset is filtered by a single column and no arguments
40
+ # were passed to get(), treat the queryset filter as an argument to
41
+ # get(). This lets Model.objects.filter(pk=42).get() work like
42
+ # Model.objects.get(pk=42).
43
+ can_ignore_filter = False
44
+ if django.VERSION >= (1, 7) and not kwargs:
45
+ if len(self.query.where) == 1 and not self.query.where.negated:
46
+ (child,) = self.query.where.children
47
+ if isinstance(child, Exact) and isinstance(child.lhs, Col):
48
+ can_ignore_filter = True
49
+ kwargs[child.lhs.target.attname] = child.rhs
50
+ elif (
51
+ len(args) == 1
52
+ and len(args[0].children) == 1
53
+ and len(args[0].children[0]) == 2
54
+ and args[0].children[0][0] == "id"
55
+ ):
56
+ can_ignore_filter = True
57
+ kwargs[args[0].children[0][0]] = args[0].children[0][1]
58
+
59
+ # Don't access cache if using a filtered queryset
60
+ if self.query.where and not can_ignore_filter:
61
+ return super(CachedQuerySet, self).get(*args, **orig_kwargs)
62
+
63
+ # Don't access cache if using a deferred queryset
64
+ if (
65
+ len(self.query.deferred_loading[0]) > 0
66
+ or not self.query.deferred_loading[1]
67
+ ):
68
+ return super(CachedQuerySet, self).get(*args, **orig_kwargs)
69
+
70
+ # Get the cache key from the model name and pk
71
+ if "pk" in kwargs:
72
+ pk = kwargs["pk"]
73
+ elif "pk__exact" in kwargs:
74
+ pk = kwargs["pk__exact"]
75
+ elif "id" in kwargs:
76
+ pk = kwargs["id"]
77
+ elif "id__exact" in kwargs:
78
+ pk = kwargs["id__exact"]
79
+ else:
80
+ return super(CachedQuerySet, self).get(*args, **orig_kwargs)
81
+
82
+ key = self.cache_key(pk)
83
+
84
+ # Retrieve (or set) the item in the cache
85
+ item = cache.get(key)
86
+ if item is None:
87
+ cache_missed.send(sender=self.model)
88
+ item = super(CachedQuerySet, self).get(*args, **orig_kwargs)
89
+ cache.set(key, item, self.__CACHE_FOREVER)
90
+ else:
91
+ cache_hit.send(sender=self.model)
92
+
93
+ return item
94
+
95
+ def from_ids(self, ids, lookup="pk__in"):
96
+ assert isinstance(ids, Iterable)
97
+
98
+ cache_keys = [self.cache_key(id_) for id_ in ids]
99
+
100
+ instances = cache.get_many(cache_keys).values()
101
+ cached_dict = {i.id: i for i in instances if i}
102
+ uncached_ids = set(ids) - set(cached_dict.keys())
103
+
104
+ # If there are uncached instances, retrieve and cache them
105
+ if len(uncached_ids) > 0:
106
+ uncached = self.filter(**{lookup: uncached_ids})
107
+
108
+ # Add the uncached instances to the existing instances
109
+ cached_dict.update((i.id, i) for i in uncached)
110
+
111
+ # Cache the uncached instances
112
+ to_cache = {self.cache_key(i.pk): i for i in uncached}
113
+ cache.set_many(to_cache, self.__CACHE_FOREVER)
114
+
115
+ return [cached_dict[id_] for id_ in ids if id_ in cached_dict]
116
+
117
+ def cache_key(self, pk):
118
+ """Generate the cache key for an individual model"""
119
+ key = "{}-pk:{}".format(self.model.__name__, pk)
120
+
121
+ # We shouldn't have keys that are crazy long. This might mean
122
+ # we get duplicates though
123
+ key = key[: self.__MAXIMUM_CACHE_KEY_LENGTH]
124
+
125
+ return key
126
+
127
+ def invalidate(self, pk, recache=False):
128
+ """Invalidate a single item in the cache"""
129
+ key = self.cache_key(pk)
130
+ cache_invalidated.send(sender=self.model)
131
+ if recache is True:
132
+ try:
133
+ entry = super(CachedQuerySet, self).get(pk=pk)
134
+ except self.model.DoesNotExist:
135
+ log.info(
136
+ "Unable to recache model during invalidate",
137
+ exc_info=True,
138
+ extra={"data": {"model": self.model, "pk": pk}},
139
+ )
140
+ except self.model.MultipleObjectsReturned:
141
+ log.error(
142
+ "Error retrieving single entry from database",
143
+ exc_info=True,
144
+ extra={"data": {"model": self.model, "pk": pk}},
145
+ )
146
+ else:
147
+ cache.set(key, entry, self.__CACHE_FOREVER)
148
+ else:
149
+ cache.delete(key)
ormcache/signals.py ADDED
@@ -0,0 +1,6 @@
1
+ import django.dispatch
2
+
3
+
4
+ cache_hit = django.dispatch.Signal()
5
+ cache_missed = django.dispatch.Signal()
6
+ cache_invalidated = django.dispatch.Signal()
ormcache/utils.py ADDED
@@ -0,0 +1,54 @@
1
+ try:
2
+ from django.db.models.fields.related import SingleRelatedObjectDescriptor
3
+ except ImportError:
4
+ from django.db.models.fields.related_descriptors import (
5
+ ReverseOneToOneDescriptor as SingleRelatedObjectDescriptor,
6
+ )
7
+
8
+
9
+ def _distinct(l):
10
+ """Given an iterable will return a list of all distinct values."""
11
+ return list(set(l))
12
+
13
+
14
+ def attach_foreignkey(objects, field):
15
+ """
16
+ Similar to the disqus's attach_foreignkey, but pulls items from the cache
17
+ if caching is enabled on the model
18
+ """
19
+ is_foreignkey = isinstance(field, SingleRelatedObjectDescriptor)
20
+
21
+ if not is_foreignkey:
22
+ field = field.field
23
+ accessor = "_%s_cache" % field.name
24
+ model = field.remote_field.model
25
+ lookup = "%s__in" % "pk"
26
+ column = field.column
27
+ key = "pk"
28
+ else:
29
+ accessor = field.cache_name
30
+ field = field.related.field
31
+ model = field.model
32
+ lookup = "%s__in" % field.name
33
+ column = "pk"
34
+ key = field.column
35
+
36
+ # Ensure values are unique, do not contain already present values, and
37
+ # are not missing values specified in select_related
38
+ values = _distinct(
39
+ getattr(o, column) for o in objects if getattr(o, accessor, False) is False
40
+ )
41
+ if not values:
42
+ return
43
+
44
+ # Pull instances from cache if caching is enabled. This conditional logic
45
+ # can be removed when the UserManager inherits BaseManager
46
+ if getattr(model, "cache_enabled", False) is True:
47
+ instances = model.objects.from_ids(values, lookup=lookup)
48
+ else:
49
+ instances = model.objects.filter(**{lookup: values})
50
+
51
+ instances_dict = {getattr(i, key): i for i in instances}
52
+
53
+ for obj in objects:
54
+ setattr(obj, accessor, instances_dict.get(getattr(obj, column)))