v0.165.0
 1from datetime import datetime
 2from typing import ClassVar, Self
 3
 4from plain.postgres import Field, types
 5from plain.runtime import settings
 6from plain.utils import timezone
 7
 8from plain import postgres
 9
10__all__ = ["CachedItem", "CachedItemQuerySet"]
11
12
13class CachedItemQuerySet(postgres.QuerySet["CachedItem"]):
14    def live(self) -> Self:
15        """Rows readable right now: never-expiring *or* not-yet-expired.
16
17        This is the filter cache reads use -- an entry past its `expires_at`
18        reads as absent. (Contrast `unexpired()`, which matches only rows with a
19        *future* expiry.)
20        """
21        return self.where(
22            CachedItem.expires_at.is_null() | CachedItem.expires_at.gte(timezone.now())
23        )
24
25    def expired(self) -> Self:
26        return self.where(CachedItem.expires_at.lt(timezone.now()))
27
28    def unexpired(self) -> Self:
29        return self.where(CachedItem.expires_at.gte(timezone.now()))
30
31    def forever(self) -> Self:
32        return self.where(CachedItem.expires_at.is_null())
33
34
35@postgres.register_model
36class CachedItem(postgres.Model):
37    key: Field[str] = types.TextField(max_length=255)
38    # `object`, not `Any`: the cache stores any JSON-serializable value, and
39    # `Field[Any]` would make `Any` satisfy the model-valued `__get__` overload,
40    # so `CachedItem.value` would type as `type[Any]` and lose the field surface
41    # entirely.
42    value: Field[object] = types.JSONField(
43        required=False, allow_null=True, default=None
44    )
45    expires_at: Field[datetime | None] = types.DateTimeField(
46        required=False, allow_null=True, default=None
47    )
48    created_at: Field[datetime] = types.DateTimeField(create_now=True)
49    updated_at: Field[datetime] = types.DateTimeField(create_now=True, update_now=True)
50
51    # ClassVar override so @typed doesn't treat it as a constructor field;
52    # base Model.query is also a ClassVar, so this override is clean.
53    query: ClassVar[CachedItemQuerySet] = CachedItemQuerySet()
54
55    model_options = postgres.Options(
56        indexes=[
57            postgres.Index(
58                name="plaincache_cacheditem_expires_at_idx", fields=["expires_at"]
59            ),
60        ],
61        constraints=[
62            postgres.UniqueConstraint(
63                fields=["key"], name="plaincache_cacheditem_unique_key"
64            ),
65        ],
66        storage_parameters={
67            "autovacuum_vacuum_scale_factor": settings.CACHE_AUTOVACUUM_SCALE_FACTOR,
68            "toast.autovacuum_vacuum_scale_factor": settings.CACHE_TOAST_AUTOVACUUM_SCALE_FACTOR,
69        },
70    )
71
72    def __str__(self) -> str:
73        return self.key