Converters - circuitsacul/apgorm GitHub Wiki

apgorm has support for python-side converters. Essentially, Postgres will store one value (say, an integer) but your python code can work with an IntEnum or IntFlag.

It's important to note that field defaults have to be of the type actually sent to postgres. So, if you have a converter that converts integers to strings when sent to postgres, then your default would have to be a string, not an integer.

IntEFConverter

apgorm comes with a single, builtin converter. Since apgorm doesn't support enumerations at this time, it seemed reasonable to add a converter that allows for enums and flags.

IntEnums

from enum import IntEnum
import apgorm
from apgorm.types import Int, Serial

class CarStatus(IntEnum):
    STAND_STILL = 0
    DRIVING = 1
    FLYING = 2

class Car(apgorm.Model):
    carid = Serial().field()
    status = Int().field(default=0).with_converter(apgorm.IntEFConverter(CarStatus))

    primary_key = (carid,)

class Database(apgorm.Database):
    cars = Car

Now you can do this:

car = await Car.fetch(carid=1)
print(car)  # -> <Car carid:1 status:CarStatus.STAND_STILL>
print(car.status)  # -> CarStatus.STAND_STILL

car.status = CarStatus.FLYING
await car.save()
print(car.status)  # -> CarStatus.FLYING

Custom Converters

You can also write your own converters. For example, if you needed to store your integers as strings in postgres, but you want them to be numbers when used in your code, you could do this:

class IntStrConverter(apgorm.Converter[int, str]):  # typehints are always optional
    def to_stored(value: int) -> str:
        return str(value)

    def from_stored(value: str) -> int:
        return int(value)

Now, you can use the convert in a field:

int_as_str_field = Int().field().with_converter(IntStrConverter)

Non-null Array

By default, types.Array(...) returns a Sequence[... | None]. If you don't want the values to be nullable, you can use converters:

class NonNullArrayC(apgorm.Converter[Sequence[Optional[_T]], Sequence[_T]], Generic[_T]):
    def to_stored(self, value: Sequence[_T]) -> Sequence[_T]:
        return value

    def from_stored(self, value: Sequence[Optional[_T]]) -> Sequence[_T]:
        assert None not in value
        return cast("Sequence[_T]", value)


# without type hints
class NonNullArrayC(apgorm.Converter):
    def to_stored(self, value):
        return value

    def from_stored(self, value):
        assert None not in value
        return value

non_null_array = types.Array(types.Int()).with_converter(NonNullArrayC)

Note: In this case, the converter exists mostly for the sake of type-checkers. If you don't type-hint, then you probably don't need to worry about this; Just don't store None inside the array.

A Word of Caution

You have to be careful when using converters, as apgorm's support for them isn't perfect. If a field is converted, there's no garantee that you can use that converted value elsewhere. User.fetch(id=myuser.id) might fail if the id field has a converter. You might have to do User.fetch(id=int(myuser.id)). Just keep this in mind. Converters work best for things like converting a Decimal to an int, since asyncpg will accept both for a numeric field.