Plain is headed towards 1.0! Subscribe for development updates →

 1from datetime import datetime, timedelta
 2from functools import cached_property
 3
 4from plain.models import IntegrityError
 5from plain.utils import timezone
 6
 7
 8class Cached:
 9    """Store and retrieve cached items."""
10
11    def __init__(self, key: str) -> None:
12        self.key = key
13
14        # So we can import Cached in __init__.py
15        # without getting the packages not ready error...
16        from .models import CachedItem
17
18        self._model_class = CachedItem
19
20    @cached_property
21    def _model_instance(self):
22        try:
23            return self._model_class.objects.get(key=self.key)
24        except self._model_class.DoesNotExist:
25            return None
26
27    def reload(self) -> None:
28        if hasattr(self, "_model_instance"):
29            del self._model_instance
30
31    def _is_expired(self):
32        if not self._model_instance:
33            return True
34
35        if not self._model_instance.expires_at:
36            return False
37
38        return self._model_instance.expires_at < timezone.now()
39
40    def exists(self) -> bool:
41        if self._model_instance is None:
42            return False
43
44        return not self._is_expired()
45
46    @property
47    def value(self):
48        if not self.exists():
49            return None
50
51        return self._model_instance.value
52
53    def set(self, value, expiration: datetime | timedelta | int | float | None = None):
54        defaults = {
55            "value": value,
56        }
57
58        if isinstance(expiration, int | float):
59            defaults["expires_at"] = timezone.now() + timedelta(seconds=expiration)
60        elif isinstance(expiration, timedelta):
61            defaults["expires_at"] = timezone.now() + expiration
62        elif isinstance(expiration, datetime):
63            defaults["expires_at"] = expiration
64        else:
65            # Keep existing expires_at value or None
66            pass
67
68        # Make sure expires_at is timezone aware
69        if defaults["expires_at"] and not timezone.is_aware(defaults["expires_at"]):
70            defaults["expires_at"] = timezone.make_aware(defaults["expires_at"])
71
72        try:
73            item, _ = self._model_class.objects.update_or_create(
74                key=self.key, defaults=defaults
75            )
76        except IntegrityError:
77            # Most likely a race condition in creating the item,
78            # so trying again should do an update
79            item, _ = self._model_class.objects.update_or_create(
80                key=self.key, defaults=defaults
81            )
82
83        self.reload()
84        return item.value
85
86    def delete(self) -> bool:
87        if not self._model_instance:
88            # A no-op, but a return value you can use to know whether it did anything
89            return False
90
91        self._model_instance.delete()
92        self.reload()
93        return True