redis-om-python
redis-om-python copied to clipboard
clean way to add non-persistable fields?
I want to initialize my model with extra attributes that are inferred from persisted fields
For example, let's say I have birthdate and I want to calculate age. There is no reason to store both fields, so I can do the following:
import datetime
from typing import Optional
from pydantic import EmailStr
from redis_om import HashModel
class Customer(HashModel):
first_name: str
last_name: str
email: EmailStr
join_date: datetime.date
birth_date: datetime.date
bio: Optional[str]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.age = helper_func.get_age_from_birthdate(self.birthdate)
The problem is with the above approach is "age" gets persisted.
I can do the following:
class Customer(HashModel):
first_name: str
last_name: str
email: EmailStr
join_date: datetime.date
birth_date: datetime.date
bio: Optional[str]
def populate_extra_fields(self):
self.age = helper_func.get_age_from_birthdate(self.birthdate)
c = Customer.get(id)
c.populate_extra_fields()
But then on subsequent .save() the age field gets persisted. I will have to re-fetch to update the model without age getting persisted, e.g:
# BAD: (age gets persisted)
c = Customer.get(id)
c.populate_extra_fields()
if c.age > 35:
c.first_name = "melder"
c.save()
# GOOD: (age does not get persisted)
c = Customer.get(id)
c.populate_extra_fields()
if c.age > 35:
c = Customer.get(id)
c.first_name = "melder"
c.save()
Is this simply outside the scope of the library or how it should be used? Would it better to write a wrapper to handle these extra fields?
Ok so I tried the wrapper approach and it seems to work. Here an example:
class CustomerWrapper:
"""
Wrapper to extend redis_om functionality since its behavior is somewhat magical.
Best strategy is to keep the models as concise as possible.
"""
class Customer(HashModel):
"""
Customer model
"""
first_name: str
last_name: str
email: EmailStr
join_date: datetime.date
birth_date: datetime.date
bio: Optional[str]
customer_fields = list(Customer.__fields__.keys())
def __init__(self, _customer):
self.customer = _customer
self.age = None
for k, v in vars(self.customer).items():
if k in self.customer_fields:
setattr(self, k, v)
self.age = helper.birthdate_to_age(self.birth_date)
@classmethod
def parse(cls, **kwargs):
return { k: kwargs[k] for k in cls.customer_fields if k in kwargs }
@classmethod
def new(cls, **kwargs):
return cls(cls.Customer(**cls.parse(kwargs)))
def save(self, **kwargs):
for k, v in (kwargs or vars(self)).items():
if k in self.customer_fields:
setattr(self, k, v)
setattr(self.customer, k, v)
self.customer.save()
return self
def new(**kwargs):
return CustomerWrapper.new(**kwargs)
customer_data = { 'firstname': "melder", ... }
customer = new(**customer_data).save()
print(customer.age)
So CustomerWrapper should behave similarly to Customer, and Customer methods can be extended in CustomerWrapper or by accessing them through the "customer instance field", e.g:
c = customer.customer.get(id)
@simonprickett seems like this would be a great example for us to highlight in some documentation but isn't currently an issue
@sav-norem Yea feel free to close this. But I'd update the docs with a warning that all instance vars of the OM get persisted.
I'm still relatively new to python so if I have the time I'll look into implementing this as a decorator and package it as a separate plugin module- unless you have a better approach in mind and can offer guidance. Let me know how I can help.