Skip to content

Commit

Permalink
feat: auto_sync and auto_save (#401)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
andrewthetechie authored Dec 21, 2022
1 parent efc8373 commit 0e11e9d
Show file tree
Hide file tree
Showing 11 changed files with 440 additions and 103 deletions.
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Inspired by
## Main Dependencies

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

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

# When _auto_sync = True (default), updating any attribute will update that field in Redis too
this_book = Book(title="Moby Dick", author='Herman Melvill', published_on=date(year=1851, month=10, day=18))

this_book = Book(title="Moby Dick", author='Herman Melvill', published_on=date(year=1851, month=10, day=17))
await Book.insert(this_book)
# oops, there was a typo. Fix it
this_book.author = "Herman Melville"
# Update is an async context manager and will update redis with all changes in one operations
async with this_book.update():
this_book.author = "Herman Melville"
this_book.published_on=date(year=1851, month=10, day=18)
this_book_from_redis = await Book.select(ids=["Moby Dick"])
assert this_book_from_redis[0].author == "Herman Melville"

# If you have _auto_save set to false on a model, you have to await .save() to update a model in tedis
await this_book.save()
assert this_book_from_redis[0].author == date(year=1851, month=10, day=18)

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

#### Custom Fields in Model
### Custom Fields in Model

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

## License

Expand Down
93 changes: 93 additions & 0 deletions docs/automatic_saving.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Automatic Saving
================

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.

pydantic-aioredis has two options you can tweak for automatic saving:
* _auto_save: Used to determine if a model is saved to redis on instantiate
* _auto_sync: Used to determine if a change to a model is saved on setattr

These options can be set on a model or on a per instance basis.

.. code-block::
import asyncio
from pydantic_aioredis import RedisConfig, Model, Store
class Book(Model):
_primary_key_field: str = 'title'
title: str
author: str
_auto_save: bool = True
_auto_sync: bool = True
class Movie(Model):
_primary_key_field: str = 'title'
title: str
director: str
_auto_sync: bool = True
# Create the store and register your models
store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379), life_span_in_seconds=3600)
store.register_model(Book)
async def autol():
my_book = Book(title='The Hobbit', author='J.R.R. Tolkien')
# my_book is already in redis
book_from_redis = await Book.select(ids['The Hobbit'])
assert book_from_redis[0] == my_book
# _auto_save means that changing a field will automatically save the model
my_book.author = 'J.R.R. Tolkien II'
book_from_redis = await Book.select(ids['The Hobbit'])
assert book_from_redis[0] == my_book
my_movie = Movie(title='The Lord of the Rings', director='Peter Jackson')
# my_move is not in redis until its inserted
await Movie.insert(my_movie)
# _auto_sync means that changing a field will automatically save the model
my_movie.director = 'Peter Jackson II'
movie_from_redis = await Movie.select(ids['The Hobbit'])
assert movie_from_redis[0] == my_movie
# _auto_sync and _auto_save can be set on a per instance basis
local_book = Book(title='The Silmarillion', author='J.R.R. Tolkien', _auto_save=False, _auto_sync=False)
# 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
books_in_redis = await Book.select()
assert len(books_in_redis) == 1
loop = asyncio.get_event_loop()
loop.run_until_complete(auto())
There is also `AutoModel`, which is a subclass of `Model` that has `_auto_save` and `_auto_sync` set to True by default.

.. code-block::
import asyncio
from pydantic_aioredis import RedisConfig, AutoModel, Store
class Book(AutoModel):
_primary_key_field: str = 'title'
title: str
async def auto_model():
my_book = Book(title='The Hobbit')
# my_book is already in redis
book_from_redis = await Book.select(ids['The Hobbit'])
assert book_from_redis[0] == my_book
# _auto_save means that changing a field will automatically save the model
my_book.author = 'J.R.R. Tolkien II'
book_from_redis = await Book.select(ids['The Hobbit'])
assert book_from_redis[0] == my_book
loop = asyncio.get_event_loop()
loop.run_until_complete(auto_model())
7 changes: 4 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pydantic-aioredis
=============================================
A declarative ORM for Redis, using aioredis. Use your Pydantic
A declarative ORM for Redis, using redis-py in async mode. Use your Pydantic
models like an ORM, storing data in Redis.

Inspired by
Expand All @@ -10,8 +10,8 @@ Inspired by
Dependencies
-----------------

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


Expand All @@ -20,6 +20,7 @@ Dependencies

quickstart
serialization
automatic_saving
extras
development
module
Expand Down
133 changes: 74 additions & 59 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,62 +25,77 @@ Store and RedisConfig let you configure and customize the connection to your red

.. code-block::
from pydantic_aioredis import RedisConfig, Model, Store
# Create models as you would create pydantic models i.e. using typings
class Book(Model):
_primary_key_field: str = 'title'
title: str
author: str
published_on: date
in_stock: bool = True
# Do note that there is no concept of relationships here
class Library(Model):
# the _primary_key_field is mandatory
_primary_key_field: str = 'name'
name: str
address: str
# Create the store and register your models
store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379),life_span_in_seconds=3600)
store.register_model(Book)
store.register_model(Library)
# Sample books. You can create as many as you wish anywhere in the code
books = [
Book(title="Oliver Twist", author='Charles Dickens', published_on=date(year=1215, month=4, day=4),
in_stock=False),
Book(title="Great Expectations", author='Charles Dickens', published_on=date(year=1220, month=4, day=4)),
Book(title="Jane Eyre", author='Charles Dickens', published_on=date(year=1225, month=6, day=4), in_stock=False),
Book(title="Wuthering Heights", author='Jane Austen', published_on=date(year=1600, month=4, day=4)),
]
# Some library objects
libraries = [
Library(name="The Grand Library", address="Kinogozi, Hoima, Uganda"),
Library(name="Christian Library", address="Buhimba, Hoima, Uganda")
]
async def work_with_orm():
# Insert them into redis
await Book.insert(books)
await Library.insert(libraries)
# Select all books to view them. A list of Model instances will be returned
all_books = await Book.select()
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(...]
# Or select some of the books
some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
print(some_books) # Will return only those two books
# Or select some of the columns. THIS RETURNS DICTIONARIES not MODEL Instances
# The Dictionaries have values in string form so you might need to do some extra work
books_with_few_fields = await Book.select(columns=["author", "in_stock"])
print(books_with_few_fields) # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]
# Update any book or library
await Book.update(_id="Oliver Twist", data={"author": "John Doe"})
# Delete any number of items
await Library.delete(ids=["The Grand Library"])
import asyncio
from datetime import date
from pydantic_aioredis import RedisConfig, Model, Store
# Create models as you would create pydantic models i.e. using typings
class Book(Model):
_primary_key_field: str = 'title'
title: str
author: str
published_on: date
in_stock: bool = True
# Do note that there is no concept of relationships here
class Library(Model):
# the _primary_key_field is mandatory
_primary_key_field: str = 'name'
name: str
address: str
# Create the store and register your models
store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379), life_span_in_seconds=3600)
store.register_model(Book)
store.register_model(Library)
# Sample books. You can create as many as you wish anywhere in the code
books = [
Book(title="Oliver Twist", author='Charles Dickens', published_on=date(year=1215, month=4, day=4),
in_stock=False),
Book(title="Great Expectations", author='Charles Dickens', published_on=date(year=1220, month=4, day=4)),
Book(title="Jane Eyre", author='Charles Dickens', published_on=date(year=1225, month=6, day=4), in_stock=False),
Book(title="Wuthering Heights", author='Jane Austen', published_on=date(year=1600, month=4, day=4)),
]
# Some library objects
libraries = [
Library(name="The Grand Library", address="Kinogozi, Hoima, Uganda"),
Library(name="Christian Library", address="Buhimba, Hoima, Uganda")
]
async def work_with_orm():
# Insert them into redis
await Book.insert(books)
await Library.insert(libraries)
# Select all books to view them. A list of Model instances will be returned
all_books = await Book.select()
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(...]
# Or select some of the books
some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
print(some_books) # Will return only those two books
# Or select some of the columns. THIS RETURNS DICTIONARIES not MODEL Instances
# The Dictionaries have values in string form so you might need to do some extra work
books_with_few_fields = await Book.select(columns=["author", "in_stock"])
print(books_with_few_fields) # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]
this_book = Book(title="Moby Dick", author='Herman Melvill', published_on=date(year=1851, month=10, day=17))
await Book.insert(this_book)
# oops, there was a typo. Fix it
# Update is an async context manager and will update redis with all changes in one operations
async with this_book.update():
this_book.author = "Herman Melville"
this_book.published_on=date(year=1851, month=10, day=18)
this_book_from_redis = await Book.select(ids=["Moby Dick"])
assert this_book_from_redis[0].author == "Herman Melville"
assert this_book_from_redis[0].author == date(year=1851, month=10, day=18)
# Delete any number of items
await Library.delete(ids=["The Grand Library"])
# Now run these updates
loop = asyncio.get_event_loop()
loop.run_until_complete(work_with_orm())
3 changes: 3 additions & 0 deletions pydantic_aioredis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@

from .config import RedisConfig # noqa: F401
from .model import Model # noqa: F401
from .model import AutoModel # noqa: F401
from .store import Store # noqa: F401

__all__ = ["RedisConfig", "Model", "AutoModel", "Store"]
13 changes: 1 addition & 12 deletions pydantic_aioredis/ext/FastAPI/crudrouter.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,10 @@ async def route(item_id: str, model: self.update_schema) -> SCHEMA: # type: ign
raise NOT_FOUND
item = item[0]

# if autosync is on, updating one key at a time would update redis a bunch of times and be slow
# instead, let's update the dict, and insert a new object
if item._auto_sync:
this_dict = item.dict()
for key, value in model.dict().items():
this_dict[key] = value
item = self.schema(**this_dict)

await self.schema.insert([item])

else:
async with item.update() as item:
for key, value in model.dict().items():
setattr(item, key, value)

await item.save()
return item

return route
Expand Down
Loading

0 comments on commit 0e11e9d

Please sign in to comment.