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