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