Basic redisent Example

This portion of the documentation covers a rather basic but illustrative example which stores a simple time-based reminder in a Redis hash key.

Here is a rather straight forward example of a Reminder Redis hash entry which stores a few basic attributes with it:

  1from __future__ import annotations
  2
  3import dateparser
  4
  5from datetime import datetime
  6from dataclasses import dataclass, field
  7
  8from typing import Union, Optional, Mapping, Any, MutableMapping, ClassVar
  9
 10from redisent import RedisEntry
 11
 12
 13@dataclass
 14class FuzzyTime:
 15    provided_when: str = field()
 16
 17    created_time: datetime = field()
 18    resolved_time: datetime = field(init=False)
 19
 20    @property
 21    def created_timestamp(self) -> float:
 22        return self.created_time.timestamp()
 23
 24    @property
 25    def resolved_timestamp(self) -> float:
 26        return self.resolved_time.timestamp()
 27
 28    @property
 29    def num_seconds_left(self) -> Optional[int]:
 30        dt_now = datetime.now()
 31
 32        if dt_now > self.resolved_time:
 33            return None
 34
 35        t_delta = (self.resolved_time - dt_now)
 36
 37        return int(t_delta.total_seconds()) if t_delta else None
 38
 39    def __post_init__(self, *args, **kwargs) -> None:
 40        res_time = dateparser.parse(self.provided_when, settings={'PREFER_DATES_FROM': 'future'})
 41        if not res_time:
 42            raise ValueError(f'Unable to resolve provided "when": {self.provided_when}')
 43
 44        self.resolved_time = res_time
 45
 46    @classmethod
 47    def build(cls, provided_when: str, created_ts: Union[float, int, datetime] = None) -> FuzzyTime:
 48        kwargs: MutableMapping[str, Any] = {'provided_when': provided_when}
 49
 50        if created_ts:
 51            kwargs['created_time'] = datetime.fromtimestamp(created_ts) if isinstance(created_ts, (int, float,)) else created_ts
 52
 53        return FuzzyTime(**kwargs)
 54
 55    @classmethod
 56    def from_dict(cls, dict_mapping: Mapping[str, Any]) -> FuzzyTime:
 57        return FuzzyTime(provided_when=dict_mapping['provided_when'], created_time=dict_mapping.get('created_time', None))
 58
 59
 60@dataclass
 61class Reminder(RedisEntry):
 62    redis_id: ClassVar[str] = 'reminders'
 63
 64    member_id: str = field(default_factory=str)
 65    member_name: str = field(default_factory=str)
 66
 67    channel_id: str = field(default_factory=str)
 68    channel_name: str = field(default_factory=str)
 69
 70    provided_when: str = field(default_factory=str)
 71    content: str = field(default_factory=str)
 72
 73    trigger_ts: float = field(default_factory=float)
 74    created_ts: float = field(default_factory=float)
 75
 76    is_complete: bool = field(default=False, compare=False)
 77
 78    @property
 79    def trigger_dt(self) -> datetime:
 80        """
 81        Property wrapper for converting the float "trigger_ts" into "datetime"
 82        """
 83
 84        return datetime.fromtimestamp(self.trigger_ts)
 85
 86    @property
 87    def created_dt(self) -> datetime:
 88        """
 89        Property wrapper for converting the float "created_ts" into "datetime"
 90        """
 91
 92        return datetime.fromtimestamp(self.created_ts)
 93
 94    def __post_init__(self, *args, **kwargs) -> None:
 95        """
 96        Set the default "is_complete" field based on current timestamp and
 97        sets "redis_name" based on member and trigger attributes
 98        """
 99
100        self.redis_name = f'{self.member_id}:{self.trigger_ts}'
101
102        if self.trigger_dt < datetime.now():
103            self.is_complete = True

Creating a Reminder instance

Next, we can use IPython to create a redisent.helpers.RedisentHelper instance (for simplicity, this will be non-async) and create an instance of the Reminder class, persist it to Redis and then fetch it back. Along the way, the intrinsic value of dataclasses will become apparent in the form of auto-generated __repr__, __init__ and __str__ methods.

Note

Invoke the ipython command from the same directory as a file named reminder.py. This will result in from reminder import Reminder auto-magically working.

In [1]: from reminder import Reminder
   ...: from redisent import RedisentHelper
   ...:
   ...: from datetime import datetime, timedelta
[2021-01-07 18:36:09,101] DEBUG in __init__: Logging started for redisent.

In [2]: dt_now = datetime.now()
   ...: trig_dt = dt_now + timedelta(minutes=5)

In [3]: rh = RedisentHelper(use_async=False)

In [4]: rem = Reminder(redis_id='reminders', member_id=12345, member_name='Jon',
   ...:                channel_id=54321, channel_name='#test',
   ...:                provided_when='in 5 minutes',
   ...:                content='Test Reminder',
   ...:                trigger_ts=trig_dt.timestamp(),
   ...:                created_ts=dt_now.timestamp())

Here, the rem object will be an instance of Reminder with the corrosponding attributes provided to the ctor for Reminder. All RedisEntry subclassess have a redisent.models.RedisEntry.dump() method which will dump a string representation of the object:

In [5]: print(rem.dump())

RedisEntry (Reminder) for key "reminders", hash entry "12345:1610323201.801648":
=> member_id    -> "12345"
=> member_name    -> "Jon"
=> channel_id    -> "54321"
=> channel_name    -> "#test"
=> provided_when    -> "in 5 minutes"
=> content    -> "Test Reminder"
=> trigger_ts    -> "1610323201.801648"
=> created_ts    -> "1610322901.801648"
=> is_complete    -> "False"

Storing an entry in Redis

Next using the already created instance of redisent.helpers.RedisentHelper, rh, the redisent.models.RedisEntry.store() method can be used to first store the reminder in Redis. Here, the Redis key will be presumed to be reminders.

In [6]: rem.store(rh)
Out[6]: 1

In [12]: with rh.wrapped_redis(op_name='hkeys("reminders")') as r_conn:
    ...:     rem_keys = r_conn.hkeys('reminders')
    ...:

In [13]: rem_keys
Out[13]: [b'12345:1610323201.801648']

By using the redisent.helpers.RedisentHelper.wrapped_redis() context manager, a new Redis connection is created for calling hkeys("reminders") and finally the keys are dumped out (with no encoding, hence the result being represented as bytes. Using the Redis command hget("reminders", "12345:1610323201.801648") will similarly return the bytes-encoded blob representing the Reminder class within Redis.

Retrieving an entry from Redis

Finally, we can fetch back the original reminder from Redis using redisent.models.RedisEntry.fetch():

In [14]: rem_fetched = Reminder.fetch(helper=rh, redis_id='reminders', redis_name='12345:1610323201.801648')

In [15]: rem_fetched
Out[15]: Reminder(redis_id='reminders', redis_name='12345:1610323201.801648', member_id=12345, member_name='Jon', channel_id=54321, channel_name='#test', provided_when='in 5 minutes', content='Test Reminder', trigger_ts=1610323201.801648, created_ts=1610322901.801648, is_complete=True)

In [16]: print(rem_fetched.dump())

RedisEntry (Reminder) for key "reminders", hash entry "12345:1610323201.801648":
=> member_id    -> "12345"
=> member_name    -> "Jon"
=> channel_id    -> "54321"
=> channel_name    -> "#test"
=> provided_when    -> "in 5 minutes"
=> content    -> "Test Reminder"
=> trigger_ts    -> "1610323201.801648"
=> created_ts    -> "1610322901.801648"
=> is_complete    -> "True"

In [17]: rem_fetched == rem
Out[17]: True

Et voila! An equivilent instance of the newly created instance of Reminder, rem was fetched and de-serialized as rem_fetched.

Wrapping Up

The astute reader will notice the requirement for providing redis_id with a static value (“reminders” in this case). If a RedisEntry subclass should always use a specific key, it is often easier to re-define the redis_id dataclass dataclasses.field().

Here is what it would look like in the case of the Reminder class:

 1@dataclass
 2class Reminder(RedisEntry):
 3    member_id: str = field(default_factory=str)
 4    member_name: str = field(default_factory=str)
 5
 6    channel_id: str = field(default_factory=str)
 7    channel_name: str = field(default_factory=str)
 8
 9    # Force "redis_id" to be "reminders"
10    redis_id: str = field(default='reminders', metadata={'redis_field': True})