Source code for codegen_database.types.enums
"""Enum-backed SQLAlchemy column types for codegen_database."""
from __future__ import annotations
import enum
from typing import Any
from sqlalchemy import Integer, Text
from sqlalchemy import types as sa_types
[docs]
class TextEnum(sa_types.TypeDecorator[enum.Enum]):
"""Store a Python enum as plain text in PostgreSQL.
Values are persisted as the enum member's ``.value`` (which
must be a string) and coerced back to the corresponding Python
enum member on load. No PostgreSQL ``ENUM`` type is created —
the underlying column is ``TEXT``.
Args:
enum_class: The :class:`enum.Enum` subclass to coerce
values to and from.
Example::
import enum
from sqlalchemy import Column
from codegen_database.types import TextEnum
class Color(enum.Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
Column("color", TextEnum(Color), nullable=False)
"""
impl = Text
cache_ok = True
def __init__(self, enum_class: type[enum.Enum]) -> None:
"""Store the enum class for value coercion.
Accepts any object to surface a clear TypeError at construction
time for callers who bypass the type annotation.
"""
if not (
isinstance(enum_class, type) and issubclass(enum_class, enum.Enum)
):
msg = ( # type: ignore[unreachable]
f"TextEnum expects an Enum subclass, got {enum_class!r}"
)
raise TypeError(msg)
self.enum_class = enum_class
super().__init__()
[docs]
def process_bind_param(
self,
value: Any, # noqa: ANN401
dialect: Any, # noqa: ANN401, ARG002
) -> str | None:
"""Convert a Python enum member to its string value."""
if value is None:
return None
if isinstance(value, self.enum_class):
return value.value
if isinstance(value, str):
# Value-to-member lookup; raises ValueError if unknown.
self.enum_class(value) # type: ignore[misc]
return value
msg = (
f"Expected {self.enum_class.__name__} or str, "
f"got {type(value).__name__}"
)
raise ValueError(msg)
[docs]
def process_result_value(
self,
value: Any, # noqa: ANN401
dialect: Any, # noqa: ANN401, ARG002
) -> enum.Enum | None:
"""Convert a stored text value back to a Python enum."""
if value is None:
return None
return self.enum_class(value) # type: ignore[misc]
[docs]
class IntEnum(sa_types.TypeDecorator[enum.Enum]):
"""Store a Python enum as an integer in PostgreSQL.
Values are persisted as the enum member's ``.value`` (which
must be an integer) and coerced back to the corresponding
Python enum member on load. The underlying column is
``INTEGER``.
Args:
enum_class: The :class:`enum.Enum` subclass to coerce
values to and from. All member values must be
integers.
Example::
import enum
from sqlalchemy import Column
from codegen_database.types import IntEnum
class Priority(enum.Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
Column("priority", IntEnum(Priority), nullable=False)
"""
impl = Integer
cache_ok = True
def __init__(self, enum_class: type[enum.Enum]) -> None:
"""Store the enum class for value coercion.
Accepts any object to surface a clear TypeError at construction
time for callers who bypass the type annotation.
"""
if not (
isinstance(enum_class, type) and issubclass(enum_class, enum.Enum)
):
msg = ( # type: ignore[unreachable]
f"IntEnum expects an Enum subclass, got {enum_class!r}"
)
raise TypeError(msg)
self.enum_class = enum_class
super().__init__()
[docs]
def process_bind_param(
self,
value: Any, # noqa: ANN401
dialect: Any, # noqa: ANN401, ARG002
) -> int | None:
"""Convert a Python enum member to its integer value."""
if value is None:
return None
if isinstance(value, self.enum_class):
return value.value
if isinstance(value, int):
# Value-to-member lookup; raises ValueError if unknown.
self.enum_class(value) # type: ignore[misc]
return value
msg = (
f"Expected {self.enum_class.__name__} or int, "
f"got {type(value).__name__}"
)
raise ValueError(msg)
[docs]
def process_result_value(
self,
value: Any, # noqa: ANN401
dialect: Any, # noqa: ANN401, ARG002
) -> enum.Enum | None:
"""Convert a stored integer back to a Python enum."""
if value is None:
return None
return self.enum_class(value) # type: ignore[misc]