v0.166.0
 1from collections.abc import Callable, Sequence
 2from functools import cached_property
 3from typing import TYPE_CHECKING, Any
 4from uuid import UUID
 5
 6from plain import exceptions
 7
 8from .base import NOT_PROVIDED, ColumnField
 9
10if TYPE_CHECKING:
11    from plain.postgres.connection import DatabaseConnection
12    from plain.postgres.expressions import Func
13
14
15class UUIDField[T: (UUID, UUID | None) = UUID](ColumnField[T]):
16    db_type_sql = "uuid"
17    empty_strings_allowed = False
18
19    def __init__(
20        self,
21        *,
22        generate: bool = False,
23        default: Any = NOT_PROVIDED,
24        required: bool = True,
25        allow_null: bool = False,
26        validators: Sequence[Callable[..., Any]] = (),
27    ):
28        # `default` accepts only None (the nullable-optional marker). There is
29        # no Python-side UUID default; for generated values use generate=True.
30        self.generate = generate
31        super().__init__(
32            default=default,
33            required=required,
34            allow_null=allow_null,
35            validators=validators,
36        )
37
38    @cached_property
39    def _db_default_expression(self) -> Func | None:
40        if self.generate:
41            from plain.postgres.functions.uuid import GenRandomUUID
42
43            return GenRandomUUID()
44        return None
45
46    def get_db_default_expression(self) -> Func | None:
47        return self._db_default_expression
48
49    def deconstruct(self) -> tuple[str, str, list[Any], dict[str, Any]]:
50        name, path, args, kwargs = super().deconstruct()
51        if self.generate:
52            kwargs["generate"] = True
53        return name, path, args, kwargs
54
55    def get_prep_value(self, value: Any) -> Any:
56        value = super().get_prep_value(value)
57        return self.to_python(value)
58
59    def get_db_prep_value(
60        self, value: Any, connection: DatabaseConnection, prepared: bool = False
61    ) -> UUID | None:
62        # PostgreSQL has native UUID type
63        if value is None:
64            return None
65        if not isinstance(value, UUID):
66            value = self.to_python(value)
67        return value
68
69    def to_python(self, value: Any) -> UUID | None:
70        if value is not None and not isinstance(value, UUID):
71            input_form = "int" if isinstance(value, int) else "hex"
72            try:
73                return UUID(**{input_form: value})
74            except AttributeError, ValueError:
75                raise exceptions.ValidationError(
76                    '"%(value)s" is not a valid UUID.',
77                    code="invalid",
78                    params={"value": value},
79                )
80        return value