Skip to content
This repository was archived by the owner on Jun 21, 2025. It is now read-only.

Commit 0e11e9d

Browse files
feat: auto_sync and auto_save (#401)
* feat: auto_sync and auto_save This is a new feature that adds _auto_save and some accompanying tweaks to _auto_sync. Additionally, it adds an update() context manager to make it simple to do a bunch of updates to a model and sync to redis at the end. * ci: restrict characters * ci: make sure ints are unique * ci: remove hypothesis
1 parent efc8373 commit 0e11e9d

File tree

11 files changed

+440
-103
lines changed

11 files changed

+440
-103
lines changed

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Inspired by
2727
## Main Dependencies
2828

2929
- [Python +3.7](https://www.python.org)
30-
- [redis-py <4.3.0](https://github.com/redis/redis-py)
30+
- [redis-py <4.2.0](https://github.com/redis/redis-py)
3131
- [pydantic](https://github.com/samuelcolvin/pydantic/)
3232

3333
## Getting Started
@@ -103,16 +103,17 @@ async def work_with_orm():
103103
books_with_few_fields = await Book.select(columns=["author", "in_stock"])
104104
print(books_with_few_fields) # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]
105105

106-
# When _auto_sync = True (default), updating any attribute will update that field in Redis too
107-
this_book = Book(title="Moby Dick", author='Herman Melvill', published_on=date(year=1851, month=10, day=18))
106+
107+
this_book = Book(title="Moby Dick", author='Herman Melvill', published_on=date(year=1851, month=10, day=17))
108108
await Book.insert(this_book)
109109
# oops, there was a typo. Fix it
110-
this_book.author = "Herman Melville"
110+
# Update is an async context manager and will update redis with all changes in one operations
111+
async with this_book.update():
112+
this_book.author = "Herman Melville"
113+
this_book.published_on=date(year=1851, month=10, day=18)
111114
this_book_from_redis = await Book.select(ids=["Moby Dick"])
112115
assert this_book_from_redis[0].author == "Herman Melville"
113-
114-
# If you have _auto_save set to false on a model, you have to await .save() to update a model in tedis
115-
await this_book.save()
116+
assert this_book_from_redis[0].author == date(year=1851, month=10, day=18)
116117

117118
# Delete any number of items
118119
await Library.delete(ids=["The Grand Library"])
@@ -122,14 +123,16 @@ loop = asyncio.get_event_loop()
122123
loop.run_until_complete(work_with_orm())
123124
```
124125

125-
#### Custom Fields in Model
126+
### Custom Fields in Model
126127

127128
| Field Name | Required | Default | Description |
128129
| ------------------- | -------- | ------------ | -------------------------------------------------------------------- |
129130
| \_primary_key_field | Yes | None | The field of your model that is the primary key |
130131
| \_redis_prefix | No | None | If set, will be added to the beginning of the keys we store in redis |
131132
| \_redis_separator | No | : | Defaults to :, used to separate prefix, table_name, and primary_key |
132-
| \_table_name | NO | cls.**name** | Defaults to the model's name, can set a custom name in redis |
133+
| \_table_name | No | cls.**name** | Defaults to the model's name, can set a custom name in redis |
134+
| \_auto_save | No | False | Defaults to false. If true, will save to redis on instantiation |
135+
| \_auto_sync | No | False | Defaults to false. If true, will save to redis on attr update |
133136

134137
## License
135138

docs/automatic_saving.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Automatic Saving
2+
================
3+
4+
By default, a pydantic-aioredis model is only saved to Redis when its .save() method is called or when it is inserted(). This is to prevent unnecessary writes to Redis.
5+
6+
pydantic-aioredis has two options you can tweak for automatic saving:
7+
* _auto_save: Used to determine if a model is saved to redis on instantiate
8+
* _auto_sync: Used to determine if a change to a model is saved on setattr
9+
10+
These options can be set on a model or on a per instance basis.
11+
12+
.. code-block::
13+
14+
import asyncio
15+
from pydantic_aioredis import RedisConfig, Model, Store
16+
17+
class Book(Model):
18+
_primary_key_field: str = 'title'
19+
title: str
20+
author: str
21+
22+
_auto_save: bool = True
23+
_auto_sync: bool = True
24+
25+
26+
class Movie(Model):
27+
_primary_key_field: str = 'title'
28+
title: str
29+
director: str
30+
31+
_auto_sync: bool = True
32+
33+
34+
35+
# Create the store and register your models
36+
store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379), life_span_in_seconds=3600)
37+
store.register_model(Book)
38+
39+
async def autol():
40+
my_book = Book(title='The Hobbit', author='J.R.R. Tolkien')
41+
# my_book is already in redis
42+
book_from_redis = await Book.select(ids['The Hobbit'])
43+
assert book_from_redis[0] == my_book
44+
45+
# _auto_save means that changing a field will automatically save the model
46+
my_book.author = 'J.R.R. Tolkien II'
47+
book_from_redis = await Book.select(ids['The Hobbit'])
48+
assert book_from_redis[0] == my_book
49+
50+
my_movie = Movie(title='The Lord of the Rings', director='Peter Jackson')
51+
# my_move is not in redis until its inserted
52+
await Movie.insert(my_movie)
53+
54+
# _auto_sync means that changing a field will automatically save the model
55+
my_movie.director = 'Peter Jackson II'
56+
movie_from_redis = await Movie.select(ids['The Hobbit'])
57+
assert movie_from_redis[0] == my_movie
58+
59+
# _auto_sync and _auto_save can be set on a per instance basis
60+
local_book = Book(title='The Silmarillion', author='J.R.R. Tolkien', _auto_save=False, _auto_sync=False)
61+
# local_book is not automatically saved in redis and won't try to sync, even though the class has _auto_save and _auto_sync set to True
62+
books_in_redis = await Book.select()
63+
assert len(books_in_redis) == 1
64+
65+
66+
loop = asyncio.get_event_loop()
67+
loop.run_until_complete(auto())
68+
69+
70+
There is also `AutoModel`, which is a subclass of `Model` that has `_auto_save` and `_auto_sync` set to True by default.
71+
72+
.. code-block::
73+
74+
import asyncio
75+
from pydantic_aioredis import RedisConfig, AutoModel, Store
76+
77+
class Book(AutoModel):
78+
_primary_key_field: str = 'title'
79+
title: str
80+
81+
async def auto_model():
82+
my_book = Book(title='The Hobbit')
83+
# my_book is already in redis
84+
book_from_redis = await Book.select(ids['The Hobbit'])
85+
assert book_from_redis[0] == my_book
86+
87+
# _auto_save means that changing a field will automatically save the model
88+
my_book.author = 'J.R.R. Tolkien II'
89+
book_from_redis = await Book.select(ids['The Hobbit'])
90+
assert book_from_redis[0] == my_book
91+
92+
loop = asyncio.get_event_loop()
93+
loop.run_until_complete(auto_model())

docs/index.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pydantic-aioredis
22
=============================================
3-
A declarative ORM for Redis, using aioredis. Use your Pydantic
3+
A declarative ORM for Redis, using redis-py in async mode. Use your Pydantic
44
models like an ORM, storing data in Redis.
55

66
Inspired by
@@ -10,8 +10,8 @@ Inspired by
1010
Dependencies
1111
-----------------
1212

13-
* `Python +3.6 <https://www.python.org>`_
14-
* `aioredis 2.0 <https://aioredis.readthedocs.io/en/latest/>`_
13+
* `Python +3.7 <https://www.python.org>`_
14+
* `redis-py <4.2 <https://aioredis.readthedocs.io/en/latest/>`_
1515
* `pydantic <https://github.com/samuelcolvin/pydantic/>`_
1616

1717

@@ -20,6 +20,7 @@ Dependencies
2020

2121
quickstart
2222
serialization
23+
automatic_saving
2324
extras
2425
development
2526
module

docs/quickstart.rst

Lines changed: 74 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,62 +25,77 @@ Store and RedisConfig let you configure and customize the connection to your red
2525

2626
.. code-block::
2727
28-
from pydantic_aioredis import RedisConfig, Model, Store
29-
30-
# Create models as you would create pydantic models i.e. using typings
31-
class Book(Model):
32-
_primary_key_field: str = 'title'
33-
title: str
34-
author: str
35-
published_on: date
36-
in_stock: bool = True
37-
38-
# Do note that there is no concept of relationships here
39-
class Library(Model):
40-
# the _primary_key_field is mandatory
41-
_primary_key_field: str = 'name'
42-
name: str
43-
address: str
44-
45-
# Create the store and register your models
46-
store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379),life_span_in_seconds=3600)
47-
store.register_model(Book)
48-
store.register_model(Library)
49-
50-
# Sample books. You can create as many as you wish anywhere in the code
51-
books = [
52-
Book(title="Oliver Twist", author='Charles Dickens', published_on=date(year=1215, month=4, day=4),
53-
in_stock=False),
54-
Book(title="Great Expectations", author='Charles Dickens', published_on=date(year=1220, month=4, day=4)),
55-
Book(title="Jane Eyre", author='Charles Dickens', published_on=date(year=1225, month=6, day=4), in_stock=False),
56-
Book(title="Wuthering Heights", author='Jane Austen', published_on=date(year=1600, month=4, day=4)),
57-
]
58-
# Some library objects
59-
libraries = [
60-
Library(name="The Grand Library", address="Kinogozi, Hoima, Uganda"),
61-
Library(name="Christian Library", address="Buhimba, Hoima, Uganda")
62-
]
63-
64-
async def work_with_orm():
65-
# Insert them into redis
66-
await Book.insert(books)
67-
await Library.insert(libraries)
68-
69-
# Select all books to view them. A list of Model instances will be returned
70-
all_books = await Book.select()
71-
print(all_books) # Will print [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), in_stock=False), Book(...]
72-
73-
# Or select some of the books
74-
some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
75-
print(some_books) # Will return only those two books
76-
77-
# Or select some of the columns. THIS RETURNS DICTIONARIES not MODEL Instances
78-
# The Dictionaries have values in string form so you might need to do some extra work
79-
books_with_few_fields = await Book.select(columns=["author", "in_stock"])
80-
print(books_with_few_fields) # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]
81-
82-
# Update any book or library
83-
await Book.update(_id="Oliver Twist", data={"author": "John Doe"})
84-
85-
# Delete any number of items
86-
await Library.delete(ids=["The Grand Library"])
28+
import asyncio
29+
from datetime import date
30+
from pydantic_aioredis import RedisConfig, Model, Store
31+
32+
# Create models as you would create pydantic models i.e. using typings
33+
class Book(Model):
34+
_primary_key_field: str = 'title'
35+
title: str
36+
author: str
37+
published_on: date
38+
in_stock: bool = True
39+
40+
# Do note that there is no concept of relationships here
41+
class Library(Model):
42+
# the _primary_key_field is mandatory
43+
_primary_key_field: str = 'name'
44+
name: str
45+
address: str
46+
47+
# Create the store and register your models
48+
store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379), life_span_in_seconds=3600)
49+
store.register_model(Book)
50+
store.register_model(Library)
51+
52+
# Sample books. You can create as many as you wish anywhere in the code
53+
books = [
54+
Book(title="Oliver Twist", author='Charles Dickens', published_on=date(year=1215, month=4, day=4),
55+
in_stock=False),
56+
Book(title="Great Expectations", author='Charles Dickens', published_on=date(year=1220, month=4, day=4)),
57+
Book(title="Jane Eyre", author='Charles Dickens', published_on=date(year=1225, month=6, day=4), in_stock=False),
58+
Book(title="Wuthering Heights", author='Jane Austen', published_on=date(year=1600, month=4, day=4)),
59+
]
60+
# Some library objects
61+
libraries = [
62+
Library(name="The Grand Library", address="Kinogozi, Hoima, Uganda"),
63+
Library(name="Christian Library", address="Buhimba, Hoima, Uganda")
64+
]
65+
66+
async def work_with_orm():
67+
# Insert them into redis
68+
await Book.insert(books)
69+
await Library.insert(libraries)
70+
71+
# Select all books to view them. A list of Model instances will be returned
72+
all_books = await Book.select()
73+
print(all_books) # Will print [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), in_stock=False), Book(...]
74+
75+
# Or select some of the books
76+
some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
77+
print(some_books) # Will return only those two books
78+
79+
# Or select some of the columns. THIS RETURNS DICTIONARIES not MODEL Instances
80+
# The Dictionaries have values in string form so you might need to do some extra work
81+
books_with_few_fields = await Book.select(columns=["author", "in_stock"])
82+
print(books_with_few_fields) # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]
83+
84+
85+
this_book = Book(title="Moby Dick", author='Herman Melvill', published_on=date(year=1851, month=10, day=17))
86+
await Book.insert(this_book)
87+
# oops, there was a typo. Fix it
88+
# Update is an async context manager and will update redis with all changes in one operations
89+
async with this_book.update():
90+
this_book.author = "Herman Melville"
91+
this_book.published_on=date(year=1851, month=10, day=18)
92+
this_book_from_redis = await Book.select(ids=["Moby Dick"])
93+
assert this_book_from_redis[0].author == "Herman Melville"
94+
assert this_book_from_redis[0].author == date(year=1851, month=10, day=18)
95+
96+
# Delete any number of items
97+
await Library.delete(ids=["The Grand Library"])
98+
99+
# Now run these updates
100+
loop = asyncio.get_event_loop()
101+
loop.run_until_complete(work_with_orm())

pydantic_aioredis/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44

55
from .config import RedisConfig # noqa: F401
66
from .model import Model # noqa: F401
7+
from .model import AutoModel # noqa: F401
78
from .store import Store # noqa: F401
9+
10+
__all__ = ["RedisConfig", "Model", "AutoModel", "Store"]

pydantic_aioredis/ext/FastAPI/crudrouter.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,10 @@ async def route(item_id: str, model: self.update_schema) -> SCHEMA: # type: ign
9292
raise NOT_FOUND
9393
item = item[0]
9494

95-
# if autosync is on, updating one key at a time would update redis a bunch of times and be slow
96-
# instead, let's update the dict, and insert a new object
97-
if item._auto_sync:
98-
this_dict = item.dict()
99-
for key, value in model.dict().items():
100-
this_dict[key] = value
101-
item = self.schema(**this_dict)
102-
103-
await self.schema.insert([item])
104-
105-
else:
95+
async with item.update() as item:
10696
for key, value in model.dict().items():
10797
setattr(item, key, value)
10898

109-
await item.save()
11099
return item
111100

112101
return route

0 commit comments

Comments
 (0)