from collections.abc import Collection
from dataclasses import dataclass, field
from datetime import date, datetime
from decimal import Decimal
from enum import Enum
from typing import Annotated, Any, Literal, Union
from uuid import UUID
from msgspec import Meta
from kaiju_models.bases import BaseTypeT, Field, FormProperties, MetaData
__all__ = [
"TextField",
"StringField",
"EnumField",
"EnumFormProperties",
"PasswordField",
"ListField",
"JsonMapField",
"JsonListField",
"IntegerField",
"IntegerFormProperties",
"EmailField",
"DecimalField",
"DecimalFormProperties",
"DateTimeField",
"DateField",
"MixedListField",
"BooleanField",
"AnyOfField",
"UUIDField",
"ByteField",
"SetField",
]
[docs]class BooleanField(Field[bool]):
"""Boolean value."""
__base_type__ = bool
__ui_type__ = "bool"
[docs]class DateField(Field[date]):
"""Date."""
__base_type__ = date
__ui_type__ = "date"
[docs]@dataclass(slots=True)
class DateTimeField(Field[datetime]):
"""Date and time."""
__base_type__ = datetime
__ui_type__ = "datetime"
tz: bool = False
def _get_annotation_meta(self):
return Meta(title=self.title, description=self.description, tz=self.tz)
def _validation_schema(self) -> dict[str, Any]:
return {
"tz": self.tz,
}
[docs]@dataclass(slots=True)
class IntegerField(Field[int]):
"""Integer number."""
__base_type__ = int
__ui_type__ = "int"
gt: int = None
"""Validation constraint: provided value must be greater than this number."""
ge: int = None
"""Validation constraint: provided value must be greater or equal to this number."""
lt: int = None
"""Validation constraint: provided value must be less than this number."""
le: int = None
"""Validation constraint: provided value must be less or equal to this number."""
multiple_of: int = None
"""Validation constraint: provided value must be divisible by this number."""
meta: MetaData = field(default_factory=lambda: MetaData(form_properties=IntegerFormProperties()))
"""Additional field metadata."""
def _get_annotation_meta(self):
return Meta(
gt=self.gt,
ge=self.ge,
lt=self.lt,
le=self.le,
multiple_of=self.multiple_of,
description=self.description,
title=self.title,
)
def _validation_schema(self) -> dict[str, Any]:
return {
"gt": self.gt,
"ge": self.ge,
"lt": self.lt,
"le": self.le,
"multiple_of": self.multiple_of,
}
[docs]@dataclass(slots=True)
class DecimalField(IntegerField[Decimal]):
"""Decimal number."""
__base_type__ = Decimal
__ui_type__ = "decimal"
multiple_of: Decimal = None
meta: MetaData = field(default_factory=lambda: MetaData(form_properties=DecimalFormProperties()))
[docs]@dataclass(slots=True)
class JsonMapField(Field[dict[str, Any]]):
"""Dictionary field."""
__base_type__ = dict[str, Any]
__ui_type__ = "json_map"
min_length: int = None
"""Validation constraint: provided number of properties must be larger or equal to this number."""
max_length: int = None
"""Validation constraint: provided number of properties must be smaller or equal to this number."""
default: dict[str, Any] = field(default_factory=dict, init=False)
def _get_annotation_meta(self):
return Meta(
min_length=self.min_length,
max_length=self.max_length,
title=self.title,
description=self.description,
)
def _validation_schema(self):
return {
"min_length": self.min_length,
"max_length": self.max_length,
}
[docs]@dataclass(slots=True)
class JsonListField(Field[tuple]):
"""Unstructured list field."""
__base_type__ = tuple
__ui_type__ = "json_list"
min_length: int = None
"""Validation constraint: provided list length must be larger or equal to this number."""
max_length: int = None
"""Validation constraint: provided list length must be smaller or equal to this number."""
default: list = field(default_factory=tuple, init=False)
def _get_annotation_meta(self):
return Meta(
min_length=self.min_length,
max_length=self.max_length,
title=self.title,
description=self.description,
)
def _validation_schema(self):
return {
"min_length": self.min_length,
"max_length": self.max_length,
}
[docs]class ListField(Field[tuple]):
"""List of fixed type values."""
__base_type__ = tuple
__ui_type__ = "list"
__slots__ = ("field", "min_length", "max_length")
min_length: int
"""Validation constraint: provided list length must be larger or equal to this number."""
max_length: int
"""Validation constraint: provided list length must be smaller or equal to this number."""
def __init__(self, model: Field, *args, min_length: int = None, max_length: int = None, **kws: Any) -> None:
self.model = model
self.min_length = min_length
self.max_length = max_length
super().__init__(*args, default=tuple(), **kws)
def _get_annotation(self) -> type:
return Annotated[self.__base_type__[self.model.__annotation__, ...], self._get_annotation_meta()]
def _get_annotation_meta(self):
return Meta(
min_length=self.min_length, max_length=self.max_length, title=self.title, description=self.description
)
def _validation_schema(self):
return {
"min_length": self.min_length,
"max_length": self.max_length,
}
[docs] def get_schema(self) -> dict[str, Any]:
return {
**Field.get_schema(self),
"field": self.model.get_schema(),
}
[docs]class SetField(ListField[set]):
"""Unordered set of values."""
__base_type__ = set
__ui_type__ = "set"
def _get_annotation(self) -> type:
return Annotated[self.__base_type__[self.model.__annotation__], self._get_annotation_meta()]
[docs]class AnyOfField(Field[Any]):
"""Multi-type value."""
__slots__ = ("models",)
__ui_type__ = "any_of"
def __init__(self, models: Collection[Field], *args, **kws) -> None:
self.models = models
super().__init__(*args, **kws)
def _get_annotation(self) -> type:
model_types = [type(m).__base_type__ for m in self.models]
annotation = Annotated[Union[*model_types], self._get_annotation_meta()]
if self.required:
return annotation
return annotation | None
[docs] def get_schema(self) -> dict[str, Any]:
schema = super().get_schema()
return {**schema, "models": [_model.get_schema() for _model in self.models]}
[docs]class MixedListField(Field[tuple]):
"""List of multi-type values."""
__slots__ = ("models", "min_length", "max_length")
__base_type__ = tuple
__ui_type__ = "mixed_list"
min_length: int
"""Validation constraint: provided list length must be larger or equal to this number."""
max_length: int
"""Validation constraint: provided list length must be smaller or equal to this number."""
def __init__(
self, models: Collection[Field], *args, min_length: int = None, max_length: int = None, **kws: Any
) -> None:
self.models = models
self.min_length = min_length
self.max_length = max_length
super().__init__(*args, default=tuple(), **kws)
def _get_annotation(self) -> type:
model_types = [type(m).__base_type__ for m in self.models]
return Annotated[self.__base_type__[Union[*model_types], ...], self._get_annotation_meta()]
def _get_annotation_meta(self):
return Meta(
min_length=self.min_length, max_length=self.max_length, title=self.title, description=self.description
)
[docs] def get_schema(self) -> dict[str, Any]:
schema = super().get_schema()
return {**schema, "models": [_model.get_schema() for _model in self.models]}
[docs]class EnumField(Field[BaseTypeT]):
"""Select value."""
__slots__ = ("options", '_options')
__ui_type__ = "enum"
options: list[BaseTypeT]
"""Validation constraint: list of possible values."""
def __init__(self, options: list[BaseTypeT] | type[Enum], *args, meta=None, **kws: Any) -> None:
if meta is None:
meta = MetaData(form_properties=EnumFormProperties())
if isinstance(options, list):
self.options = self._options = options
else:
self.options = list(options.__members__.values())
self._options = options
super().__init__(*args, meta=meta, **kws)
def _get_annotation(self) -> type:
if isinstance(self._options, list):
annotation = Annotated[Literal[*self.options], self._get_annotation_meta()] # noqa: python bug?
else:
annotation = Annotated[self._options, self._get_annotation_meta()]
if self.required:
return annotation
return annotation | None
[docs] def get_schema(self) -> dict[str, Any]:
return {**Field.get_schema(self), "options": self.options}
[docs]class UUIDField(Field[UUID]):
"""UUID field."""
__base_type__ = UUID
__ui_type__ = "uuid"
[docs]class ByteField(Field[UUID]):
"""Byte field."""
__base_type__ = bytes
__ui_type__ = "bytes"
[docs]@dataclass(slots=True)
class StringField(Field[str]):
"""Normal string."""
__base_type__ = str
__ui_type__ = "str"
min_length: int = None
"""Validation constraint: provided string length must be larger or equal to this number."""
max_length: int = None
"""Validation constraint: provided string length must be smaller or equal to this number."""
pattern: str = None
"""Validation constraint: provided string must match this regular expression."""
pattern_error_text: str = None
def _get_annotation_meta(self):
return Meta(
min_length=self.min_length,
max_length=self.max_length,
pattern=self.pattern,
title=self.title,
description=self.description,
)
def _validation_schema(self):
return {
"min_length": self.min_length,
"max_length": self.max_length,
"pattern": self.pattern,
"pattern_error_text": self.pattern_error_text,
}
[docs]@dataclass(slots=True)
class EmailField(StringField):
"""Email string."""
__ui_type__ = "email"
pattern: str = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
pattern_error_text = "Invalid email"
[docs]@dataclass(slots=True)
class PasswordField(StringField):
"""Password string."""
__ui_type__ = "password"
[docs]@dataclass(slots=True)
class TextField(StringField):
"""Formatted text."""
__ui_type__ = "text"