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.