plain-cache/plain/cache/core.py · Docs

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