1from __future__ import annotations
 2
 3import re
 4from datetime import datetime
 5
 6from plain import models
 7from plain.exceptions import ValidationError
 8from plain.models import types
 9
10__all__ = ["Flag", "FlagResult"]
11
12
13def validate_flag_name(value: str) -> None:
14    if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", value):
15        raise ValidationError(f"{value} is not a valid Python identifier name")
16
17
18@models.register_model
19class FlagResult(models.Model):
20    created_at: datetime = types.DateTimeField(auto_now_add=True)
21    updated_at: datetime = types.DateTimeField(auto_now=True)
22    flag: Flag = types.ForeignKeyField("Flag", on_delete=models.CASCADE)
23    key: str = types.CharField(max_length=255)
24    value = types.JSONField()
25
26    query: models.QuerySet[FlagResult] = models.QuerySet()
27
28    model_options = models.Options(
29        constraints=[
30            models.UniqueConstraint(
31                fields=["flag", "key"], name="plainflags_flagresult_unique_key"
32            ),
33        ],
34    )
35
36    def __str__(self) -> str:
37        return self.key
38
39
40@models.register_model
41class Flag(models.Model):
42    created_at: datetime = types.DateTimeField(auto_now_add=True)
43    updated_at: datetime = types.DateTimeField(auto_now=True)
44    name: str = types.CharField(max_length=255, validators=[validate_flag_name])
45
46    # Optional description that can be filled in after the flag is used/created
47    description: str = types.TextField(required=False)
48
49    # To manually disable a flag before completing deleting
50    # (good to disable first to make sure the code doesn't use the flag anymore)
51    enabled: bool = types.BooleanField(default=True)
52
53    # To provide an easier way to see if a flag is still being used
54    used_at: datetime | None = types.DateTimeField(required=False, allow_null=True)
55
56    query: models.QuerySet[Flag] = models.QuerySet()
57
58    model_options = models.Options(
59        constraints=[
60            models.UniqueConstraint(
61                fields=["name"], name="plainflags_flag_unique_name"
62            ),
63        ],
64    )
65
66    def __str__(self) -> str:
67        return self.name