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})