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