From 9d28610a736626c846cfdedae212f990f3f4d0b1 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Dec 2022 04:54:04 +0300 Subject: [PATCH 01/16] Add docs --- docs/explanation/why-use-orms.md | 83 +++++++ .../how-to-guides/creating-nested-models.md | 0 docs/how-to-guides/paginating-queries.md | 0 docs/index.md | 211 ++++++++++++++++++ docs/reference.md | 5 + .../tutorials/asynchronous/getting-started.md | 1 + docs/tutorials/synchronous/getting-started.md | 1 + mkdocs.yml | 49 ++++ pydantic_redis/__init__.py | 23 +- pydantic_redis/_shared/__init__.py | 26 +++ pydantic_redis/{shared => _shared}/config.py | 4 +- .../{shared => _shared}/lua_scripts.py | 0 .../{shared => _shared}/model/__init__.py | 0 .../{shared => _shared}/model/base.py | 2 +- .../{shared => _shared}/model/delete_utils.py | 4 +- .../{shared => _shared}/model/insert_utils.py | 0 .../{shared => _shared}/model/prop_utils.py | 0 .../{shared => _shared}/model/select_utils.py | 2 +- pydantic_redis/{shared => _shared}/store.py | 0 pydantic_redis/{shared => _shared}/utils.py | 0 pydantic_redis/asyncio/__init__.py | 6 +- pydantic_redis/asyncio/model.py | 13 +- pydantic_redis/asyncio/store.py | 2 +- pydantic_redis/syncio/__init__.py | 7 +- pydantic_redis/syncio/model.py | 9 +- pydantic_redis/syncio/store.py | 2 +- requirements.txt | 3 + test/test_async_pydantic_redis.py | 4 +- test/test_pydantic_redis.py | 6 +- 29 files changed, 433 insertions(+), 30 deletions(-) create mode 100644 docs/explanation/why-use-orms.md rename pydantic_redis/shared/__init__.py => docs/how-to-guides/creating-nested-models.md (100%) create mode 100644 docs/how-to-guides/paginating-queries.md create mode 100644 docs/index.md create mode 100644 docs/reference.md create mode 100644 docs/tutorials/asynchronous/getting-started.md create mode 100644 docs/tutorials/synchronous/getting-started.md create mode 100644 mkdocs.yml create mode 100644 pydantic_redis/_shared/__init__.py rename pydantic_redis/{shared => _shared}/config.py (90%) rename pydantic_redis/{shared => _shared}/lua_scripts.py (100%) rename pydantic_redis/{shared => _shared}/model/__init__.py (100%) rename pydantic_redis/{shared => _shared}/model/base.py (99%) rename pydantic_redis/{shared => _shared}/model/delete_utils.py (86%) rename pydantic_redis/{shared => _shared}/model/insert_utils.py (100%) rename pydantic_redis/{shared => _shared}/model/prop_utils.py (100%) rename pydantic_redis/{shared => _shared}/model/select_utils.py (99%) rename pydantic_redis/{shared => _shared}/store.py (100%) rename pydantic_redis/{shared => _shared}/utils.py (100%) diff --git a/docs/explanation/why-use-orms.md b/docs/explanation/why-use-orms.md new file mode 100644 index 00000000..0d4fe6be --- /dev/null +++ b/docs/explanation/why-use-orms.md @@ -0,0 +1,83 @@ +# Why Use ORMs + +An object-relational-mapping (ORM) makes writing business logic intuitive +because the data representation is closer to what the real-life situation is. +It helps decouple the way such data is programmed from the way such data is +actually persisted in any of the data persistence technologies we have, +typically a database. + +Take the example of a book. +In code, one will represent a book as an object with a number of properties such as "title", "edition", "author" etc. + +```python +class Book(Model): + title: str + edition: int + author: Author +``` + +However, in the underlying data store, the same book could be saved as say, +a row in a table for a relational database like PostgreSQL, +or as a document in a document-based NoSQL databases like MongoDB +or as a hashmap in redis. +Of these, the document-based NoSQL databases are the closest to the definition in code. + +For MongoDB, the same book might be represented as the object below: + +```json +{ + "id": "some-random-string", + "title": "the title of the book", + "edition": 2, + "author": { + "name": "Charles Payne", + "yearsActive": [ + 1992, + 2008 + ] + } +} +``` + +As you can see, it is still quite different. + +However, for redis, the representation is even going to be further off. +It will most likely be saved as hashmap, with a given key. +The properties of book will be 'fields' for that hashmap. + +In order to interact with the book representation in the redis server, +one has to write commands like: + +```shell +# to save the book in the data store +HSET "some key" "title" "the title of the book" "edition" 2 "author" "{\"name\":\"Charles Payne\",\"yearsActive\":[1992,2008]}" +# to retrieve the entire book +HGETALL "some key" +# to retrieve just a few details of the book +HMGET "some key" "title" "edition" +# to update the book - see the confusion? are you saving a new book or updating one? +HSET "some key" "edition" 2 +# to delete the book +DEL "some key" +``` + +The above is so unrelated to the business logic that most of us +will take a number of minutes or hours trying to understand what +kind of data is even being saved. + +Is it a book or some random stuff? + +Now consider something like this: + +```python +book = Book(title="some title", edition=2, author=Author(name="Charles Payne", years_active=(1992, 2008))) +store = Store(url="redis://localhost:6379/0", pool_size=5, default_ttl=3000, timeout=1) +store.register_model(Book) + +Book.insert(data=book) +response = Book.select(ids=["some title"]) +Book.update(_id="some title", data={"edition": 1}) +Book.delete(ids=["some title", "another title"]) +``` + +Beautiful, isn't it? diff --git a/pydantic_redis/shared/__init__.py b/docs/how-to-guides/creating-nested-models.md similarity index 100% rename from pydantic_redis/shared/__init__.py rename to docs/how-to-guides/creating-nested-models.md diff --git a/docs/how-to-guides/paginating-queries.md b/docs/how-to-guides/paginating-queries.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..c203eb80 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,211 @@ +# Pydantic-redis + +A simple declarative ORM for redis based on pydantic + +## Features + +1. A subclass-able `Model` class to create Object Relational Mapping to redis hashes +2. A redis `Store` class to mutate and query `Model`'s registered in it +3. A `RedisConfig` class to pass to the `Store` constructor to connect to a redis instance +4. A synchronous `syncio` and an asynchronous `asyncio` interface to the above classes + +### Installation + +```shell +pip install pydantic-redis +``` + +### Usage + +Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis` and use accordingly + +```python +from datetime import date +from typing import Tuple, List, Optional +from pydantic_redis import RedisConfig, Model, Store + + +class Author(Model): + """ + An Author model, just like a pydantic model with appropriate + type annotations + NOTE: The `_primary_key_field` is mandatory + """ + _primary_key_field: str = 'name' + name: str + active_years: Tuple[int, int] + + +class Book(Model): + """ + A Book model. + + Models can have the following field types + - The usual i.e. float, int, dict, list, date, str, dict, Optional etc + as long as they are serializable by orjson + - Nested models e.g. `author: Author` or `author: Optional[Author]` + - List of nested models e.g. `authors: List[Author]` + or `authors: Optional[List[Author]]` + - Tuples including nested models e.g. `access_log: Tuple[Author, date]` + or `access_log: Optional[Tuple[Author, date]]]` + + NOTE: 1. Any nested model whether plain or in a list or tuple will automatically + inserted into the redis store when the parent model is inserted. + e.g. a Book with an author field, when inserted, will also insert + the author. The author can then be queried directly if that's something + one wishes to do. + + 2. When a parent model is inserted with a nested model instance that + already exists, the older nested model instance is overwritten. + This is one way of updating nested models. + All parent models that contain that nested model instance will see the change. + """ + _primary_key_field: str = 'title' + title: str + author: Author + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +class Library(Model): + """ + A library model. + + It shows a number of complicated nested models. + + About Nested Model Performance + --- + To minimize the performance penalty for nesting models, + we use REDIS EVALSHA to eagerly load the nested models + before the response is returned to the client. + This ensures that only ONE network call is made every time. + """ + _primary_key_field: str = 'name' + name: str + address: str + books: List[Book] = None + lost: Optional[List[Book]] = None + popular: Optional[Tuple[Book, Book]] = None + new: Tuple[Book, Author, int] = None + + +# Create the store +store = Store( + name='some_name', + redis_config=RedisConfig(db=5, host='localhost', port=6379), + life_span_in_seconds=3600) + +# register your models. DON'T FORGET TO DO THIS. +store.register_model(Book) +store.register_model(Library) +store.register_model(Author) + +# sample authors. You can create as many as you wish anywhere in the code +authors = { + "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), + "jane": Author(name="Jane Austen", active_years=(1580, 1640)), +} + +# Sample books. +books = [ + Book( + title="Oliver Twist", + author=authors["charles"], + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ), + Book( + title="Great Expectations", + author=authors["charles"], + published_on=date(year=1220, month=4, day=4), + rating=5, + tags=["Classic"], + ), + Book( + title="Jane Eyre", + author=authors["charles"], + published_on=date(year=1225, month=6, day=4), + in_stock=False, + rating=3.4, + tags=["Classic", "Romance"], + ), + Book( + title="Wuthering Heights", + author=authors["jane"], + published_on=date(year=1600, month=4, day=4), + rating=4.0, + tags=["Classic", "Romance"], + ), +] + +# Some library objects +libraries = [ + Library( + name="The Grand Library", + address="Kinogozi, Hoima, Uganda", + lost=[books[1]], + ), + Library( + name="Christian Library", + address="Buhimba, Hoima, Uganda", + new=(books[0], authors["jane"], 30), + ) +] + +# Insert Many. You can given them a TTL (life_span_seconds). +Book.insert(books, life_span_seconds=3600) +Library.insert(libraries) + +# Insert One. You can also given it a TTL (life_span_seconds). +Author.insert(Author(name="Jack Myers", active_years=(1240, 1300))) + +# Update One. You can also given it a TTL (life_span_seconds). +Book.update(_id="Oliver Twist", data={"author": authors["jane"]}) + +# Update nested model indirectly +updated_jane = Author(**authors["jane"].dict()) +updated_jane.active_years = (1999, 2008) +Book.update(_id="Oliver Twist", data={"author": updated_jane}) + +# Query the data +# Get all, with all fields shown. Data returned is a list of models instances. +all_books = Book.select() +print(all_books) +# Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), +# in_stock=False), Book(...] + +# or paginate i.e. skip some books and return only upto a given number +paginated_books = Book.select(skip=2, limit=2) +print(paginated_books) + +# Get some, with all fields shown. Data returned is a list of models instances. +some_books = Book.select(ids=["Oliver Twist", "Jane Eyre"]) +print(some_books) + +# Note: Pagination does not work when ids are provided i.e. +assert some_books == Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10) + +# Get all, with only a few fields shown. Data returned is a list of dictionaries. +books_with_few_fields = Book.select(columns=["author", "in_stock"]) +print(books_with_few_fields) +# Prints [{"author": "'Charles Dickens", "in_stock": "True"},...] + +# or paginate i.e. skip some books and return only upto a given number +paginated_books_with_few_fields = Book.select(columns=["author", "in_stock"], skip=2, limit=2) +print(paginated_books_with_few_fields) + +# Get some, with only some fields shown. Data returned is a list of dictionaries. +some_books_with_few_fields = Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"]) +print(some_books_with_few_fields) + +# Query the nested models directly. +some_authors = Author.select(ids=["Jane Austen"]) +print(some_authors) + +# Delete any number of items +Library.delete(ids=["The Grand Library"]) +``` \ No newline at end of file diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 00000000..868bcf5c --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,5 @@ +This part of the project documentation includes +the technical reference for the `pydantic-redis` +package. It can be used on a need-by-need basis. + +::: pydantic_redis \ No newline at end of file diff --git a/docs/tutorials/asynchronous/getting-started.md b/docs/tutorials/asynchronous/getting-started.md new file mode 100644 index 00000000..bad55622 --- /dev/null +++ b/docs/tutorials/asynchronous/getting-started.md @@ -0,0 +1 @@ +# Getting Started diff --git a/docs/tutorials/synchronous/getting-started.md b/docs/tutorials/synchronous/getting-started.md new file mode 100644 index 00000000..8b3a7945 --- /dev/null +++ b/docs/tutorials/synchronous/getting-started.md @@ -0,0 +1 @@ +# Getting Started \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..3fca5106 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,49 @@ +site_name: Pydantic-redis +site_description: Pydantic-redis, simple declarative ORM for redis +site_url: https://sopherapps.github.io/pydantic-redis/ + +theme: + name: material + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode + features: + - search.suggest + - search.highlight + - content.tabs.link + +plugins: + - search + - mkdocstrings + +repo_name: sopherapps/pydantic-redis +repo_url: https://github.com/sopherapps/pydantic-redis + +nav: + - 'Pydantic-redis': index.md + - Tutorials: + - Synchronous API: + - tutorials/synchronous/getting-started.md + - Asynchronous API: + - tutorials/asynchronous/getting-started.md + - 'How-to Guides': + - how-to-guides/creating-nested-models.md + - how-to-guides/paginating-queries.md + - 'Explanation': + - explanation/why-use-orms.md + - reference.md + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences \ No newline at end of file diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py index 9603a70b..0e56e024 100644 --- a/pydantic_redis/__init__.py +++ b/pydantic_redis/__init__.py @@ -1,4 +1,25 @@ -"""Entry point for redisy""" +""" +Pydantic-redis +============== + +A simple declarative ORM for redis based on pydantic + +Provides + 1. A subclass-able `Model` class to create Object Relational Mapping to + redis hashes + 2. A redis `Store` class to mutate and query `Model`'s registered in it + 3. A `RedisConfig` class to pass to the `Store` constructor to connect + to a redis instance + 4. A synchronous `syncio` and an asynchronous `asyncio` interface to the + above classes + +Available subpackages +--------------------- +asyncio + Asynchronous API for the ORM +syncio + Synchronous API for the ORM +""" from pydantic_redis.syncio import Store, Model, RedisConfig import pydantic_redis.asyncio diff --git a/pydantic_redis/_shared/__init__.py b/pydantic_redis/_shared/__init__.py new file mode 100644 index 00000000..09765af1 --- /dev/null +++ b/pydantic_redis/_shared/__init__.py @@ -0,0 +1,26 @@ +"""Shared utilities and base classes for pydantic-redis + +This includes basic functionality of mutating and querying +redis via the pydantic-redis ORM regardless of whether this +is done asynchronously or synchronously. +This is a private package. + +Available subpackages +--------------------- +model + defines the base `AbstractModel` class to be extended by async + and sync versions of the `Model` class + +Available modules +----------------- +config + defines the `RedisConfig` class to be used to make a + connection to a redis server +lua_scripts + defines the lua scripts to be used in querying redis +store + defines the base `AbstractStore` class to be extended by the + async and sync versions of the `Store` class +utils + defines utility functions used across the project +""" diff --git a/pydantic_redis/shared/config.py b/pydantic_redis/_shared/config.py similarity index 90% rename from pydantic_redis/shared/config.py rename to pydantic_redis/_shared/config.py index 73081cb0..6454c3ed 100644 --- a/pydantic_redis/shared/config.py +++ b/pydantic_redis/_shared/config.py @@ -1,4 +1,6 @@ -"""Module containing the main config classes""" +"""Defines the `RedisConfig` for connecting to a redis server + +""" from typing import Optional from pydantic import BaseModel diff --git a/pydantic_redis/shared/lua_scripts.py b/pydantic_redis/_shared/lua_scripts.py similarity index 100% rename from pydantic_redis/shared/lua_scripts.py rename to pydantic_redis/_shared/lua_scripts.py diff --git a/pydantic_redis/shared/model/__init__.py b/pydantic_redis/_shared/model/__init__.py similarity index 100% rename from pydantic_redis/shared/model/__init__.py rename to pydantic_redis/_shared/model/__init__.py diff --git a/pydantic_redis/shared/model/base.py b/pydantic_redis/_shared/model/base.py similarity index 99% rename from pydantic_redis/shared/model/base.py rename to pydantic_redis/_shared/model/base.py index 4b68698c..f858872f 100644 --- a/pydantic_redis/shared/model/base.py +++ b/pydantic_redis/_shared/model/base.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from pydantic_redis.shared.utils import ( +from pydantic_redis._shared.utils import ( typing_get_origin, typing_get_args, from_any_to_str_or_bytes, diff --git a/pydantic_redis/shared/model/delete_utils.py b/pydantic_redis/_shared/model/delete_utils.py similarity index 86% rename from pydantic_redis/shared/model/delete_utils.py rename to pydantic_redis/_shared/model/delete_utils.py index 19709111..3ac431e5 100644 --- a/pydantic_redis/shared/model/delete_utils.py +++ b/pydantic_redis/_shared/model/delete_utils.py @@ -4,8 +4,8 @@ from redis.client import Pipeline from redis.asyncio.client import Pipeline as AioPipeline -from pydantic_redis.shared.model import AbstractModel -from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key +from pydantic_redis._shared.model import AbstractModel +from pydantic_redis._shared.model.prop_utils import get_primary_key, get_table_index_key def delete_on_pipeline( diff --git a/pydantic_redis/shared/model/insert_utils.py b/pydantic_redis/_shared/model/insert_utils.py similarity index 100% rename from pydantic_redis/shared/model/insert_utils.py rename to pydantic_redis/_shared/model/insert_utils.py diff --git a/pydantic_redis/shared/model/prop_utils.py b/pydantic_redis/_shared/model/prop_utils.py similarity index 100% rename from pydantic_redis/shared/model/prop_utils.py rename to pydantic_redis/_shared/model/prop_utils.py diff --git a/pydantic_redis/shared/model/select_utils.py b/pydantic_redis/_shared/model/select_utils.py similarity index 99% rename from pydantic_redis/shared/model/select_utils.py rename to pydantic_redis/_shared/model/select_utils.py index 5911f505..54e8469b 100644 --- a/pydantic_redis/shared/model/select_utils.py +++ b/pydantic_redis/_shared/model/select_utils.py @@ -1,7 +1,7 @@ """Module containing the mixin functionality for selecting""" from typing import List, Any, Type, Union, Awaitable, Optional -from pydantic_redis.shared.model.prop_utils import ( +from pydantic_redis._shared.model.prop_utils import ( NESTED_MODEL_PREFIX, NESTED_MODEL_LIST_FIELD_PREFIX, NESTED_MODEL_TUPLE_FIELD_PREFIX, diff --git a/pydantic_redis/shared/store.py b/pydantic_redis/_shared/store.py similarity index 100% rename from pydantic_redis/shared/store.py rename to pydantic_redis/_shared/store.py diff --git a/pydantic_redis/shared/utils.py b/pydantic_redis/_shared/utils.py similarity index 100% rename from pydantic_redis/shared/utils.py rename to pydantic_redis/_shared/utils.py diff --git a/pydantic_redis/asyncio/__init__.py b/pydantic_redis/asyncio/__init__.py index 168f33ff..89f94745 100644 --- a/pydantic_redis/asyncio/__init__.py +++ b/pydantic_redis/asyncio/__init__.py @@ -1,7 +1,9 @@ -"""Package containing the async version of pydantic_redis""" +"""Asynchronous version of pydantic-redis + +""" from .model import Model from .store import Store -from ..shared.config import RedisConfig +from .._shared.config import RedisConfig __all__ = [Model, Store, RedisConfig] diff --git a/pydantic_redis/asyncio/model.py b/pydantic_redis/asyncio/model.py index ea04ddea..38738335 100644 --- a/pydantic_redis/asyncio/model.py +++ b/pydantic_redis/asyncio/model.py @@ -1,12 +1,9 @@ """Module containing the model classes""" -from typing import Optional, List, Any, Union, Dict, Tuple, Type +from typing import Optional, List, Any, Union, Dict -import redis.asyncio - -from pydantic_redis.shared.model import AbstractModel -from pydantic_redis.shared.model.insert_utils import insert_on_pipeline -from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key -from pydantic_redis.shared.model.select_utils import ( +from .._shared.model import AbstractModel +from .._shared.model.insert_utils import insert_on_pipeline +from .._shared.model.select_utils import ( select_all_fields_all_ids, select_all_fields_some_ids, select_some_fields_all_ids, @@ -15,7 +12,7 @@ ) from .store import Store -from ..shared.model.delete_utils import delete_on_pipeline +from .._shared.model.delete_utils import delete_on_pipeline class Model(AbstractModel): diff --git a/pydantic_redis/asyncio/store.py b/pydantic_redis/asyncio/store.py index 4c24ecae..85f30b58 100644 --- a/pydantic_redis/asyncio/store.py +++ b/pydantic_redis/asyncio/store.py @@ -3,7 +3,7 @@ from redis import asyncio as redis -from ..shared.store import AbstractStore +from .._shared.store import AbstractStore if TYPE_CHECKING: from .model import Model diff --git a/pydantic_redis/syncio/__init__.py b/pydantic_redis/syncio/__init__.py index 2af01c3f..e7efcd3b 100644 --- a/pydantic_redis/syncio/__init__.py +++ b/pydantic_redis/syncio/__init__.py @@ -1,6 +1,9 @@ -"""Package containing the synchronous and thus default version of pydantic_redis""" +"""Synchronous version of pydantic-redis ORM + +""" + from .model import Model from .store import Store -from ..shared.config import RedisConfig +from .._shared.config import RedisConfig __all__ = [Model, Store, RedisConfig] diff --git a/pydantic_redis/syncio/model.py b/pydantic_redis/syncio/model.py index 25cbe545..e5abcf95 100644 --- a/pydantic_redis/syncio/model.py +++ b/pydantic_redis/syncio/model.py @@ -1,10 +1,9 @@ """Module containing the model classes""" from typing import Optional, List, Any, Union, Dict -from pydantic_redis.shared.model import AbstractModel -from pydantic_redis.shared.model.insert_utils import insert_on_pipeline -from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key -from pydantic_redis.shared.model.select_utils import ( +from .._shared.model import AbstractModel +from .._shared.model.insert_utils import insert_on_pipeline +from .._shared.model.select_utils import ( select_all_fields_all_ids, select_all_fields_some_ids, select_some_fields_all_ids, @@ -13,7 +12,7 @@ ) from .store import Store -from ..shared.model.delete_utils import delete_on_pipeline +from .._shared.model.delete_utils import delete_on_pipeline class Model(AbstractModel): diff --git a/pydantic_redis/syncio/store.py b/pydantic_redis/syncio/store.py index 0b36cc73..a4a6e4f6 100644 --- a/pydantic_redis/syncio/store.py +++ b/pydantic_redis/syncio/store.py @@ -3,7 +3,7 @@ import redis -from ..shared.store import AbstractStore +from .._shared.store import AbstractStore if TYPE_CHECKING: from .model import Model diff --git a/requirements.txt b/requirements.txt index 6e52ed5f..9af1b877 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,6 @@ black==22.8.0 pre-commit build pytest-asyncio +mkdocs +mkdocstrings +mkdocs-material diff --git a/test/test_async_pydantic_redis.py b/test/test_async_pydantic_redis.py index 4208075f..0d8e3a1e 100644 --- a/test/test_async_pydantic_redis.py +++ b/test/test_async_pydantic_redis.py @@ -4,8 +4,8 @@ import pytest -from pydantic_redis.shared.model.prop_utils import NESTED_MODEL_PREFIX -from pydantic_redis.shared.utils import strip_leading +from pydantic_redis._shared.model.prop_utils import NESTED_MODEL_PREFIX # noqa +from pydantic_redis._shared.utils import strip_leading # noqa from pydantic_redis.asyncio import Model, RedisConfig from test.conftest import ( async_redis_store_fixture, diff --git a/test/test_pydantic_redis.py b/test/test_pydantic_redis.py index 99f7ca0a..a1c21f24 100644 --- a/test/test_pydantic_redis.py +++ b/test/test_pydantic_redis.py @@ -4,9 +4,9 @@ import pytest -from pydantic_redis.shared.config import RedisConfig -from pydantic_redis.shared.model.prop_utils import NESTED_MODEL_PREFIX -from pydantic_redis.shared.utils import strip_leading +from pydantic_redis._shared.config import RedisConfig # noqa +from pydantic_redis._shared.model.prop_utils import NESTED_MODEL_PREFIX # noqa +from pydantic_redis._shared.utils import strip_leading # noqa from pydantic_redis.syncio.model import Model from test.conftest import ( redis_store_fixture, From 84a9f33b108c252b9228dd4ac7266adc2a0635f3 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Dec 2022 05:06:47 +0300 Subject: [PATCH 02/16] Bump to v0.4.1 --- .github/workflows/ci.yml | 3 ++ CHANGELOG.md | 14 ++++++ docs/change-log.md | 88 ++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + pydantic_redis/__init__.py | 2 +- setup.py | 2 +- 6 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 docs/change-log.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc0875e4..268fe909 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,3 +64,6 @@ jobs: - name: Upload to pypi run: | twine upload -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} dist/* + - name: Deploy docs + run: | + mkdocs gh-deploy diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbbd67f..a3bc0eac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.4.1] - 2022-12-20 + +### Added + +- Added the docs site at `https://sopherapps.github.io/pydantic-redis/` + +### Changed + +- Made the `shared` package protected + +### Fixed + ## [0.4.0] - 2022-12-17 ### Added @@ -17,6 +29,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Changed redis index to use sorted sets instead of ordinary sets +### Fixed + ## [0.3.0] - 2022-12-15 ### Added diff --git a/docs/change-log.md b/docs/change-log.md new file mode 100644 index 00000000..765ddc3d --- /dev/null +++ b/docs/change-log.md @@ -0,0 +1,88 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## [Unreleased] + +## [0.4.1] - 2022-12-20 + +### Added + +- Added the docs site at `https://sopherapps.github.io/pydantic-redis/` + +### Changed + +- Made the `shared` package protected + +### Fixed + +## [0.4.0] - 2022-12-17 + +### Added + +- Added pagination + +### Changed + +- Changed redis index to use sorted sets instead of ordinary sets + +### Fixed + +## [0.3.0] - 2022-12-15 + +### Added + +- Added asyncio support, to be got from the `pydantic_redis.asyncio` module + +### Changed + +- Moved the synchronous version to the `pydantic_redis.syncio` module, but kept its contents exposed in pydantic_redis + for backward-compatibility + +### Fixed + +## [0.2.0] - 2022-12-15 + +### Added + +### Changed + +- Changed the `NESTED_MODEL_LIST_FIELD_PREFIX` to "___" and `NESTED_MODEL_TUPLE_FIELD_PREFIX` to "____" +- Changed all queries (selects) to use lua scripts +- Changed `Model.deserialize_partially` to receive data either as a dict or as a flattened list of key-values + +### Fixed + +## [0.1.8] - 2022-12-13 + +### Added + +- Add support for model fields that are tuples of nested models + +### Changed + +### Fixed + +## [0.1.7] - 2022-12-12 + +### Added + +### Changed + +### Fixed + +- Fixed support for model properties that are *Optional* lists of nested models +- Fixed issue with field names being disfigured by `lstrip` when attempting to strip nested-mode-prefixes + +## [0.1.6] - 2022-11-01 + +### Added + +- Support for model properties that are lists of nested models + +### Changed + +### Fixed \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 3fca5106..2f10bfc6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,6 +40,7 @@ nav: - 'Explanation': - explanation/why-use-orms.md - reference.md + - change-log.md markdown_extensions: - pymdownx.highlight: diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py index 0e56e024..95d19aa5 100644 --- a/pydantic_redis/__init__.py +++ b/pydantic_redis/__init__.py @@ -26,4 +26,4 @@ __all__ = [Store, RedisConfig, Model, asyncio] -__version__ = "0.4.0" +__version__ = "0.4.1" diff --git a/setup.py b/setup.py index 26d00ecf..3d2d4e4d 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # This call to setup() does all the work setup( name="pydantic-redis", - version="0.4.0", + version="0.4.1", description="This package provides a simple ORM for redis using pydantic-like models.", long_description=README, long_description_content_type="text/markdown", From e8e4b5ff34505e2a536b120ed9fcf7a3caa4b83e Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Dec 2022 05:16:15 +0300 Subject: [PATCH 03/16] Fix docs deployment in CI --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 268fe909..fc1c8606 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,9 +61,9 @@ jobs: - name: Build run: | python -m build + - name: Deploy docs + run: | + mkdocs gh-deploy -c - name: Upload to pypi run: | twine upload -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} dist/* - - name: Deploy docs - run: | - mkdocs gh-deploy From 4012a0abef7d09d815e0f791b5a9737286cc0f65 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Dec 2022 05:19:32 +0300 Subject: [PATCH 04/16] Bump to v0.4.2 --- CHANGELOG.md | 10 ++++++++++ docs/change-log.md | 10 ++++++++++ pydantic_redis/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3bc0eac..48aa6e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.4.2] - 2022-12-20 + +### Added + +### Changed + +### Fixed + +- Fixed docs building in CI + ## [0.4.1] - 2022-12-20 ### Added diff --git a/docs/change-log.md b/docs/change-log.md index 765ddc3d..71262b3a 100644 --- a/docs/change-log.md +++ b/docs/change-log.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.4.2] - 2022-12-20 + +### Added + +### Changed + +### Fixed + +- Fixed docs building in CI + ## [0.4.1] - 2022-12-20 ### Added diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py index 95d19aa5..3c1f1332 100644 --- a/pydantic_redis/__init__.py +++ b/pydantic_redis/__init__.py @@ -26,4 +26,4 @@ __all__ = [Store, RedisConfig, Model, asyncio] -__version__ = "0.4.1" +__version__ = "0.4.2" diff --git a/setup.py b/setup.py index 3d2d4e4d..75195730 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ # This call to setup() does all the work setup( name="pydantic-redis", - version="0.4.1", + version="0.4.2", description="This package provides a simple ORM for redis using pydantic-like models.", long_description=README, long_description_content_type="text/markdown", From baf2cf5bd181926362156ecc508eee770ee17761 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 28 Dec 2022 20:58:27 +0300 Subject: [PATCH 05/16] Add docs for synchronous models --- docs/css/custom.css | 13 + docs/css/termynal.css | 109 +++++++ docs/how-to-guides/creating-nested-models.md | 0 docs/how-to-guides/paginating-queries.md | 0 docs/index.md | 273 +++++------------- docs/js/custom.js | 114 ++++++++ docs/js/termynal.js | 264 +++++++++++++++++ .../tutorials/asynchronous/getting-started.md | 1 - docs/tutorials/asynchronous/models.md | 210 ++++++++++++++ docs/tutorials/intro.md | 49 ++++ docs/tutorials/synchronous/getting-started.md | 1 - docs/tutorials/synchronous/models.md | 73 +++++ docs/tutorials/synchronous/nested-models.md | 140 +++++++++ docs_src/index/async_main.py | 89 ++++++ docs_src/index/sync_main.py | 83 ++++++ docs_src/tutorials/synchronous/models.py | 40 +++ .../tutorials/synchronous/nested-models.py | 66 +++++ mkdocs.yml | 39 ++- pydantic_redis/__init__.py | 19 +- requirements.txt | 1 + 20 files changed, 1362 insertions(+), 222 deletions(-) create mode 100644 docs/css/custom.css create mode 100644 docs/css/termynal.css delete mode 100644 docs/how-to-guides/creating-nested-models.md delete mode 100644 docs/how-to-guides/paginating-queries.md create mode 100644 docs/js/custom.js create mode 100644 docs/js/termynal.js delete mode 100644 docs/tutorials/asynchronous/getting-started.md create mode 100644 docs/tutorials/asynchronous/models.md create mode 100644 docs/tutorials/intro.md delete mode 100644 docs/tutorials/synchronous/getting-started.md create mode 100644 docs/tutorials/synchronous/models.md create mode 100644 docs/tutorials/synchronous/nested-models.md create mode 100644 docs_src/index/async_main.py create mode 100644 docs_src/index/sync_main.py create mode 100644 docs_src/tutorials/synchronous/models.py create mode 100644 docs_src/tutorials/synchronous/nested-models.py diff --git a/docs/css/custom.css b/docs/css/custom.css new file mode 100644 index 00000000..daeeff3b --- /dev/null +++ b/docs/css/custom.css @@ -0,0 +1,13 @@ +.termynal-comment { + color: #4a968f; + font-style: italic; + display: block; +} + +.termy { + direction: ltr; +} + +.termy [data-termynal] { + white-space: pre-wrap; +} diff --git a/docs/css/termynal.css b/docs/css/termynal.css new file mode 100644 index 00000000..406c0089 --- /dev/null +++ b/docs/css/termynal.css @@ -0,0 +1,109 @@ +/** + * termynal.js + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + +:root { + --color-bg: #252a33; + --color-text: #eee; + --color-text-subtle: #a2a2a2; +} + +[data-termynal] { + width: 750px; + max-width: 100%; + background: var(--color-bg); + color: var(--color-text); + /* font-size: 18px; */ + font-size: 15px; + /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ + font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; + border-radius: 4px; + padding: 75px 45px 35px; + position: relative; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +[data-termynal]:before { + content: ''; + position: absolute; + top: 15px; + left: 15px; + display: inline-block; + width: 15px; + height: 15px; + border-radius: 50%; + /* A little hack to display the window buttons in one pseudo element. */ + background: #d9515d; + -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; + box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; +} + +[data-termynal]:after { + content: 'bash'; + position: absolute; + color: var(--color-text-subtle); + top: 5px; + left: 0; + width: 100%; + text-align: center; +} + +a[data-terminal-control] { + text-align: right; + display: block; + color: #aebbff; +} + +[data-ty] { + display: block; + line-height: 2; +} + +[data-ty]:before { + /* Set up defaults and ensure empty lines are displayed. */ + content: ''; + display: inline-block; + vertical-align: middle; +} + +[data-ty="input"]:before, +[data-ty-prompt]:before { + margin-right: 0.75em; + color: var(--color-text-subtle); +} + +[data-ty="input"]:before { + content: '$'; +} + +[data-ty][data-ty-prompt]:before { + content: attr(data-ty-prompt); +} + +[data-ty-cursor]:after { + content: attr(data-ty-cursor); + font-family: monospace; + margin-left: 0.5em; + -webkit-animation: blink 1s infinite; + animation: blink 1s infinite; +} + + +/* Cursor animation */ + +@-webkit-keyframes blink { + 50% { + opacity: 0; + } +} + +@keyframes blink { + 50% { + opacity: 0; + } +} diff --git a/docs/how-to-guides/creating-nested-models.md b/docs/how-to-guides/creating-nested-models.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/how-to-guides/paginating-queries.md b/docs/how-to-guides/paginating-queries.md deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/index.md b/docs/index.md index c203eb80..bf3a8f6d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,202 +10,83 @@ A simple declarative ORM for redis based on pydantic 4. A synchronous `syncio` and an asynchronous `asyncio` interface to the above classes ### Installation +
-```shell -pip install pydantic-redis +```console +$ pip install pydantic-redis + +---> 100% +``` +
+ +### Synchronous Example + +#### Create it + +- Create a file `main.py` with: + +```Python +{!../docs_src/index/sync_main.py!} +``` + +#### Run it + +Run the example with: + +
+ +```console +$ python main.py +All: +[ Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True), + Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False), + Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False), + Book(title='Great Expectations', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=5.0, published_on=datetime.date(1220, 4, 4), tags=['Classic'], in_stock=True)] + +Paginated: +[ Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False), + Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True)] + +Paginated but with few fields: +[ { 'author': Author(name='Charles Dickens', active_years=(1220, 1280)), + 'in_stock': False}, + { 'author': Author(name='Jane Austen', active_years=(1580, 1640)), + 'in_stock': True}] +``` +
+ +### Asynchronous Example + +#### Create it + +- Create a file `main.py` with: + +```Python +{!../docs_src/index/async_main.py!} ``` -### Usage - -Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis` and use accordingly - -```python -from datetime import date -from typing import Tuple, List, Optional -from pydantic_redis import RedisConfig, Model, Store - - -class Author(Model): - """ - An Author model, just like a pydantic model with appropriate - type annotations - NOTE: The `_primary_key_field` is mandatory - """ - _primary_key_field: str = 'name' - name: str - active_years: Tuple[int, int] - - -class Book(Model): - """ - A Book model. - - Models can have the following field types - - The usual i.e. float, int, dict, list, date, str, dict, Optional etc - as long as they are serializable by orjson - - Nested models e.g. `author: Author` or `author: Optional[Author]` - - List of nested models e.g. `authors: List[Author]` - or `authors: Optional[List[Author]]` - - Tuples including nested models e.g. `access_log: Tuple[Author, date]` - or `access_log: Optional[Tuple[Author, date]]]` - - NOTE: 1. Any nested model whether plain or in a list or tuple will automatically - inserted into the redis store when the parent model is inserted. - e.g. a Book with an author field, when inserted, will also insert - the author. The author can then be queried directly if that's something - one wishes to do. - - 2. When a parent model is inserted with a nested model instance that - already exists, the older nested model instance is overwritten. - This is one way of updating nested models. - All parent models that contain that nested model instance will see the change. - """ - _primary_key_field: str = 'title' - title: str - author: Author - rating: float - published_on: date - tags: List[str] = [] - in_stock: bool = True - - -class Library(Model): - """ - A library model. - - It shows a number of complicated nested models. - - About Nested Model Performance - --- - To minimize the performance penalty for nesting models, - we use REDIS EVALSHA to eagerly load the nested models - before the response is returned to the client. - This ensures that only ONE network call is made every time. - """ - _primary_key_field: str = 'name' - name: str - address: str - books: List[Book] = None - lost: Optional[List[Book]] = None - popular: Optional[Tuple[Book, Book]] = None - new: Tuple[Book, Author, int] = None - - -# Create the store -store = Store( - name='some_name', - redis_config=RedisConfig(db=5, host='localhost', port=6379), - life_span_in_seconds=3600) - -# register your models. DON'T FORGET TO DO THIS. -store.register_model(Book) -store.register_model(Library) -store.register_model(Author) - -# sample authors. You can create as many as you wish anywhere in the code -authors = { - "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), - "jane": Author(name="Jane Austen", active_years=(1580, 1640)), -} - -# Sample books. -books = [ - Book( - title="Oliver Twist", - author=authors["charles"], - published_on=date(year=1215, month=4, day=4), - in_stock=False, - rating=2, - tags=["Classic"], - ), - Book( - title="Great Expectations", - author=authors["charles"], - published_on=date(year=1220, month=4, day=4), - rating=5, - tags=["Classic"], - ), - Book( - title="Jane Eyre", - author=authors["charles"], - published_on=date(year=1225, month=6, day=4), - in_stock=False, - rating=3.4, - tags=["Classic", "Romance"], - ), - Book( - title="Wuthering Heights", - author=authors["jane"], - published_on=date(year=1600, month=4, day=4), - rating=4.0, - tags=["Classic", "Romance"], - ), -] - -# Some library objects -libraries = [ - Library( - name="The Grand Library", - address="Kinogozi, Hoima, Uganda", - lost=[books[1]], - ), - Library( - name="Christian Library", - address="Buhimba, Hoima, Uganda", - new=(books[0], authors["jane"], 30), - ) -] - -# Insert Many. You can given them a TTL (life_span_seconds). -Book.insert(books, life_span_seconds=3600) -Library.insert(libraries) - -# Insert One. You can also given it a TTL (life_span_seconds). -Author.insert(Author(name="Jack Myers", active_years=(1240, 1300))) - -# Update One. You can also given it a TTL (life_span_seconds). -Book.update(_id="Oliver Twist", data={"author": authors["jane"]}) - -# Update nested model indirectly -updated_jane = Author(**authors["jane"].dict()) -updated_jane.active_years = (1999, 2008) -Book.update(_id="Oliver Twist", data={"author": updated_jane}) - -# Query the data -# Get all, with all fields shown. Data returned is a list of models instances. -all_books = Book.select() -print(all_books) -# Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), -# in_stock=False), Book(...] - -# or paginate i.e. skip some books and return only upto a given number -paginated_books = Book.select(skip=2, limit=2) -print(paginated_books) - -# Get some, with all fields shown. Data returned is a list of models instances. -some_books = Book.select(ids=["Oliver Twist", "Jane Eyre"]) -print(some_books) - -# Note: Pagination does not work when ids are provided i.e. -assert some_books == Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10) - -# Get all, with only a few fields shown. Data returned is a list of dictionaries. -books_with_few_fields = Book.select(columns=["author", "in_stock"]) -print(books_with_few_fields) -# Prints [{"author": "'Charles Dickens", "in_stock": "True"},...] - -# or paginate i.e. skip some books and return only upto a given number -paginated_books_with_few_fields = Book.select(columns=["author", "in_stock"], skip=2, limit=2) -print(paginated_books_with_few_fields) - -# Get some, with only some fields shown. Data returned is a list of dictionaries. -some_books_with_few_fields = Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"]) -print(some_books_with_few_fields) - -# Query the nested models directly. -some_authors = Author.select(ids=["Jane Austen"]) -print(some_authors) - -# Delete any number of items -Library.delete(ids=["The Grand Library"]) -``` \ No newline at end of file +#### Run it + +Run the example with: + +
+ +```console +$ python main.py +All: +[ Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True), + Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False), + Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False), + Book(title='Great Expectations', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=5.0, published_on=datetime.date(1220, 4, 4), tags=['Classic'], in_stock=True)] + +Paginated: +[ Book(title='Jane Eyre', author=Author(name='Charles Dickens', active_years=(1220, 1280)), rating=3.4, published_on=datetime.date(1225, 6, 4), tags=['Classic', 'Romance'], in_stock=False), + Book(title='Wuthering Heights', author=Author(name='Jane Austen', active_years=(1580, 1640)), rating=4.0, published_on=datetime.date(1600, 4, 4), tags=['Classic', 'Romance'], in_stock=True)] + +Paginated but with few fields: +[ { 'author': Author(name='Charles Dickens', active_years=(1220, 1280)), + 'in_stock': False}, + { 'author': Author(name='Jane Austen', active_years=(1580, 1640)), + 'in_stock': True}] +``` +
diff --git a/docs/js/custom.js b/docs/js/custom.js new file mode 100644 index 00000000..1ac2a50c --- /dev/null +++ b/docs/js/custom.js @@ -0,0 +1,114 @@ +function setupTermynal() { + document.querySelectorAll(".use-termynal").forEach(node => { + node.style.display = "block"; + new Termynal(node, { + lineDelay: 500 + }); + }); + const progressLiteralStart = "---> 100%"; + const promptLiteralStart = "$ "; + const customPromptLiteralStart = "# "; + const termynalActivateClass = "termy"; + let termynals = []; + + function createTermynals() { + document + .querySelectorAll(`.${termynalActivateClass} .highlight`) + .forEach(node => { + const text = node.textContent; + const lines = text.split("\n"); + const useLines = []; + let buffer = []; + function saveBuffer() { + if (buffer.length) { + let isBlankSpace = true; + buffer.forEach(line => { + if (line) { + isBlankSpace = false; + } + }); + dataValue = {}; + if (isBlankSpace) { + dataValue["delay"] = 0; + } + if (buffer[buffer.length - 1] === "") { + // A last single
won't have effect + // so put an additional one + buffer.push(""); + } + const bufferValue = buffer.join("
"); + dataValue["value"] = bufferValue; + useLines.push(dataValue); + buffer = []; + } + } + for (let line of lines) { + if (line === progressLiteralStart) { + saveBuffer(); + useLines.push({ + type: "progress" + }); + } else if (line.startsWith(promptLiteralStart)) { + saveBuffer(); + const value = line.replace(promptLiteralStart, "").trimEnd(); + useLines.push({ + type: "input", + value: value + }); + } else if (line.startsWith("// ")) { + saveBuffer(); + const value = "💬 " + line.replace("// ", "").trimEnd(); + useLines.push({ + value: value, + class: "termynal-comment", + delay: 0 + }); + } else if (line.startsWith(customPromptLiteralStart)) { + saveBuffer(); + const promptStart = line.indexOf(promptLiteralStart); + if (promptStart === -1) { + console.error("Custom prompt found but no end delimiter", line) + } + const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") + let value = line.slice(promptStart + promptLiteralStart.length); + useLines.push({ + type: "input", + value: value, + prompt: prompt + }); + } else { + buffer.push(line); + } + } + saveBuffer(); + const div = document.createElement("div"); + node.replaceWith(div); + const termynal = new Termynal(div, { + lineData: useLines, + noInit: true, + lineDelay: 500 + }); + termynals.push(termynal); + }); + } + + function loadVisibleTermynals() { + termynals = termynals.filter(termynal => { + if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { + termynal.init(); + return false; + } + return true; + }); + } + window.addEventListener("scroll", loadVisibleTermynals); + createTermynals(); + loadVisibleTermynals(); +} + + +async function main() { + setupTermynal(); +} + +main() diff --git a/docs/js/termynal.js b/docs/js/termynal.js new file mode 100644 index 00000000..4ac32708 --- /dev/null +++ b/docs/js/termynal.js @@ -0,0 +1,264 @@ +/** + * termynal.js + * A lightweight, modern and extensible animated terminal window, using + * async/await. + * + * @author Ines Montani + * @version 0.0.1 + * @license MIT + */ + +'use strict'; + +/** Generate a terminal widget. */ +class Termynal { + /** + * Construct the widget's settings. + * @param {(string|Node)=} container - Query selector or container element. + * @param {Object=} options - Custom settings. + * @param {string} options.prefix - Prefix to use for data attributes. + * @param {number} options.startDelay - Delay before animation, in ms. + * @param {number} options.typeDelay - Delay between each typed character, in ms. + * @param {number} options.lineDelay - Delay between each line, in ms. + * @param {number} options.progressLength - Number of characters displayed as progress bar. + * @param {string} options.progressChar – Character to use for progress bar, defaults to █. + * @param {number} options.progressPercent - Max percent of progress. + * @param {string} options.cursor – Character to use for cursor, defaults to ▋. + * @param {Object[]} lineData - Dynamically loaded line data objects. + * @param {boolean} options.noInit - Don't initialise the animation. + */ + constructor(container = '#termynal', options = {}) { + this.container = (typeof container === 'string') ? document.querySelector(container) : container; + this.pfx = `data-${options.prefix || 'ty'}`; + this.originalStartDelay = this.startDelay = options.startDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; + this.originalTypeDelay = this.typeDelay = options.typeDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; + this.originalLineDelay = this.lineDelay = options.lineDelay + || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; + this.progressLength = options.progressLength + || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; + this.progressChar = options.progressChar + || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; + this.progressPercent = options.progressPercent + || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; + this.cursor = options.cursor + || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; + this.lineData = this.lineDataToElements(options.lineData || []); + this.loadLines() + if (!options.noInit) this.init() + } + + loadLines() { + // Load all the lines and create the container so that the size is fixed + // Otherwise it would be changing and the user viewport would be constantly + // moving as she/he scrolls + const finish = this.generateFinish() + finish.style.visibility = 'hidden' + this.container.appendChild(finish) + // Appends dynamically loaded lines to existing line elements. + this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); + for (let line of this.lines) { + line.style.visibility = 'hidden' + this.container.appendChild(line) + } + const restart = this.generateRestart() + restart.style.visibility = 'hidden' + this.container.appendChild(restart) + this.container.setAttribute('data-termynal', ''); + } + + /** + * Initialise the widget, get lines, clear container and start animation. + */ + init() { + /** + * Calculates width and height of Termynal container. + * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. + */ + const containerStyle = getComputedStyle(this.container); + this.container.style.width = containerStyle.width !== '0px' ? + containerStyle.width : undefined; + this.container.style.minHeight = containerStyle.height !== '0px' ? + containerStyle.height : undefined; + + this.container.setAttribute('data-termynal', ''); + this.container.innerHTML = ''; + for (let line of this.lines) { + line.style.visibility = 'visible' + } + this.start(); + } + + /** + * Start the animation and rener the lines depending on their data attributes. + */ + async start() { + this.addFinish() + await this._wait(this.startDelay); + + for (let line of this.lines) { + const type = line.getAttribute(this.pfx); + const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; + + if (type == 'input') { + line.setAttribute(`${this.pfx}-cursor`, this.cursor); + await this.type(line); + await this._wait(delay); + } + + else if (type == 'progress') { + await this.progress(line); + await this._wait(delay); + } + + else { + this.container.appendChild(line); + await this._wait(delay); + } + + line.removeAttribute(`${this.pfx}-cursor`); + } + this.addRestart() + this.finishElement.style.visibility = 'hidden' + this.lineDelay = this.originalLineDelay + this.typeDelay = this.originalTypeDelay + this.startDelay = this.originalStartDelay + } + + generateRestart() { + const restart = document.createElement('a') + restart.onclick = (e) => { + e.preventDefault() + this.container.innerHTML = '' + this.init() + } + restart.href = '#' + restart.setAttribute('data-terminal-control', '') + restart.innerHTML = "restart ↻" + return restart + } + + generateFinish() { + const finish = document.createElement('a') + finish.onclick = (e) => { + e.preventDefault() + this.lineDelay = 0 + this.typeDelay = 0 + this.startDelay = 0 + } + finish.href = '#' + finish.setAttribute('data-terminal-control', '') + finish.innerHTML = "fast →" + this.finishElement = finish + return finish + } + + addRestart() { + const restart = this.generateRestart() + this.container.appendChild(restart) + } + + addFinish() { + const finish = this.generateFinish() + this.container.appendChild(finish) + } + + /** + * Animate a typed line. + * @param {Node} line - The line element to render. + */ + async type(line) { + const chars = [...line.textContent]; + line.textContent = ''; + this.container.appendChild(line); + + for (let char of chars) { + const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; + await this._wait(delay); + line.textContent += char; + } + } + + /** + * Animate a progress bar. + * @param {Node} line - The line element to render. + */ + async progress(line) { + const progressLength = line.getAttribute(`${this.pfx}-progressLength`) + || this.progressLength; + const progressChar = line.getAttribute(`${this.pfx}-progressChar`) + || this.progressChar; + const chars = progressChar.repeat(progressLength); + const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) + || this.progressPercent; + line.textContent = ''; + this.container.appendChild(line); + + for (let i = 1; i < chars.length + 1; i++) { + await this._wait(this.typeDelay); + const percent = Math.round(i / chars.length * 100); + line.textContent = `${chars.slice(0, i)} ${percent}%`; + if (percent>progressPercent) { + break; + } + } + } + + /** + * Helper function for animation delays, called with `await`. + * @param {number} time - Timeout, in ms. + */ + _wait(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + + /** + * Converts line data objects into line elements. + * + * @param {Object[]} lineData - Dynamically loaded lines. + * @param {Object} line - Line data object. + * @returns {Element[]} - Array of line elements. + */ + lineDataToElements(lineData) { + return lineData.map(line => { + let div = document.createElement('div'); + div.innerHTML = `${line.value || ''}`; + + return div.firstElementChild; + }); + } + + /** + * Helper function for generating attributes string. + * + * @param {Object} line - Line data object. + * @returns {string} - String of attributes. + */ + _attributes(line) { + let attrs = ''; + for (let prop in line) { + // Custom add class + if (prop === 'class') { + attrs += ` class=${line[prop]} ` + continue + } + if (prop === 'type') { + attrs += `${this.pfx}="${line[prop]}" ` + } else if (prop !== 'value') { + attrs += `${this.pfx}-${prop}="${line[prop]}" ` + } + } + + return attrs; + } +} + +/** +* HTML API: If current script has container(s) specified, initialise Termynal. +*/ +if (document.currentScript.hasAttribute('data-termynal-container')) { + const containers = document.currentScript.getAttribute('data-termynal-container'); + containers.split('|') + .forEach(container => new Termynal(container)) +} diff --git a/docs/tutorials/asynchronous/getting-started.md b/docs/tutorials/asynchronous/getting-started.md deleted file mode 100644 index bad55622..00000000 --- a/docs/tutorials/asynchronous/getting-started.md +++ /dev/null @@ -1 +0,0 @@ -# Getting Started diff --git a/docs/tutorials/asynchronous/models.md b/docs/tutorials/asynchronous/models.md new file mode 100644 index 00000000..fdfbc118 --- /dev/null +++ b/docs/tutorials/asynchronous/models.md @@ -0,0 +1,210 @@ +# Getting Started + +- Install the package + +```bash +pip install pydantic-redis +``` + +- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis.asyncio` and use accordingly + +```python +import asyncio +from datetime import date +from typing import Tuple, List, Optional +from pydantic_redis.asyncio import RedisConfig, Model, Store + +# The features are exactly the same as the synchronous version, +# except for the ability to return coroutines when `insert`, +# `update`, `select` or `delete` are called. + + +class Author(Model): + """ + An Author model, just like a pydantic model with appropriate + type annotations + NOTE: The `_primary_key_field` is mandatory + """ + _primary_key_field: str = 'name' + name: str + active_years: Tuple[int, int] + + +class Book(Model): + """ + A Book model. + + Models can have the following field types + - The usual i.e. float, int, dict, list, date, str, dict, Optional etc + as long as they are serializable by orjson + - Nested models e.g. `author: Author` or `author: Optional[Author]` + - List of nested models e.g. `authors: List[Author]` + or `authors: Optional[List[Author]]` + - Tuples including nested models e.g. `access_log: Tuple[Author, date]` + or `access_log: Optional[Tuple[Author, date]]]` + + NOTE: 1. Any nested model whether plain or in a list or tuple will automatically + inserted into the redis store when the parent model is inserted. + e.g. a Book with an author field, when inserted, will also insert + the author. The author can then be queried directly if that's something + one wishes to do. + + 2. When a parent model is inserted with a nested model instance that + already exists, the older nested model instance is overwritten. + This is one way of updating nested models. + All parent models that contain that nested model instance will see the change. + """ + _primary_key_field: str = 'title' + title: str + author: Author + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +class Library(Model): + """ + A library model. + + It shows a number of complicated nested models. + + About Nested Model Performance + --- + To minimize the performance penalty for nesting models, + we use REDIS EVALSHA to eagerly load the nested models + before the response is returned to the client. + This ensures that only ONE network call is made every time. + """ + _primary_key_field: str = 'name' + name: str + address: str + books: List[Book] = None + lost: Optional[List[Book]] = None + popular: Optional[Tuple[Book, Book]] = None + new: Tuple[Book, Author, int] = None + + +async def run_async(): + """The async coroutine""" + # Create the store + store = Store( + name='some_name', + redis_config=RedisConfig(db=5, host='localhost', port=6379), + life_span_in_seconds=3600) + + # register your models. DON'T FORGET TO DO THIS. + store.register_model(Book) + store.register_model(Library) + store.register_model(Author) + + # sample authors. You can create as many as you wish anywhere in the code + authors = { + "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), + "jane": Author(name="Jane Austen", active_years=(1580, 1640)), + } + + # Sample books. + books = [ + Book( + title="Oliver Twist", + author=authors["charles"], + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ), + Book( + title="Great Expectations", + author=authors["charles"], + published_on=date(year=1220, month=4, day=4), + rating=5, + tags=["Classic"], + ), + Book( + title="Jane Eyre", + author=authors["charles"], + published_on=date(year=1225, month=6, day=4), + in_stock=False, + rating=3.4, + tags=["Classic", "Romance"], + ), + Book( + title="Wuthering Heights", + author=authors["jane"], + published_on=date(year=1600, month=4, day=4), + rating=4.0, + tags=["Classic", "Romance"], + ), + ] + + # Some library objects + libraries = [ + Library( + name="The Grand Library", + address="Kinogozi, Hoima, Uganda", + lost=[books[1]], + ), + Library( + name="Christian Library", + address="Buhimba, Hoima, Uganda", + new=(books[0], authors["jane"], 30), + ) + ] + + # Insert Many. You can given them a TTL (life_span_seconds). + await Book.insert(books, life_span_seconds=3600) + await Library.insert(libraries) + + # Insert One. You can also given it a TTL (life_span_seconds). + await Author.insert(Author(name="Jack Myers", active_years=(1240, 1300))) + + # Update One. You can also given it a TTL (life_span_seconds). + await Book.update(_id="Oliver Twist", data={"author": authors["jane"]}) + + # Update nested model indirectly + updated_jane = Author(**authors["jane"].dict()) + updated_jane.active_years = (1999, 2008) + await Book.update(_id="Oliver Twist", data={"author": updated_jane}) + + # Query the data + # Get all, with all fields shown. Data returned is a list of models instances. + all_books = await Book.select() + print(all_books) + # Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), + # in_stock=False), Book(...] + + # or paginate i.e. skip some books and return only upto a given number + paginated_books = await Book.select(skip=2, limit=2) + print(paginated_books) + + # Get some, with all fields shown. Data returned is a list of models instances. + some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"]) + print(some_books) + + # Note: Pagination does not work when ids are provided i.e. + assert some_books == await Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10) + + # Get all, with only a few fields shown. Data returned is a list of dictionaries. + books_with_few_fields = await Book.select(columns=["author", "in_stock"]) + print(books_with_few_fields) + # Prints [{"author": "'Charles Dickens", "in_stock": "True"},...] + + # or paginate i.e. skip some books and return only upto a given number + paginated_books_with_few_fields = await Book.select(columns=["author", "in_stock"], skip=2, limit=2) + print(paginated_books_with_few_fields) + + # Get some, with only some fields shown. Data returned is a list of dictionaries. + some_books_with_few_fields = await Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"]) + print(some_books_with_few_fields) + + # Query the nested models directly. + some_authors = await Author.select(ids=["Jane Austen"]) + print(some_authors) + + # Delete any number of items + await Library.delete(ids=["The Grand Library"]) + + +asyncio.run(run_async()) +``` \ No newline at end of file diff --git a/docs/tutorials/intro.md b/docs/tutorials/intro.md new file mode 100644 index 00000000..ce3b40d9 --- /dev/null +++ b/docs/tutorials/intro.md @@ -0,0 +1,49 @@ +# Intro + +This tutorial shows you how to use **pydantic-redis** step by step. + +There are two tutorials: Synchronous API and Asynchronous API. + +In either tutorials, each child section gradually builds on the previous one. These child sections are separate topics +so that one can go directly to a specific topic, just like a reference. + +## Synchronous API + +In case you are looking to use pydantic-redis without async/await, you can read the **Synchronous API** version of this +tutorial. + +This is the default API for pydantic-redis. + +## Asynchronous API + +In case you are looking to use pydantic-redis with async/await, e.g. in [FastAPI](https://fastapi.tiangolo.com) +or [asyncio](https://docs.python.org/3/library/asyncio.html) , you can read the **Asynchronous API** version of this +tutorial. + +## Run the Code + +All the code blocks can be copied and used directly. + +To run any of the examples, copy the code to a file `main.py`, and run the command below in your terminal: + +
+ +```console +$ python main.py +``` + +
+ +## Install Pydantic-redis + +First install pydantic-redis + +
+ +```console +$ pip install pydantic-redis + +---> 100% +``` + +
diff --git a/docs/tutorials/synchronous/getting-started.md b/docs/tutorials/synchronous/getting-started.md deleted file mode 100644 index 8b3a7945..00000000 --- a/docs/tutorials/synchronous/getting-started.md +++ /dev/null @@ -1 +0,0 @@ -# Getting Started \ No newline at end of file diff --git a/docs/tutorials/synchronous/models.md b/docs/tutorials/synchronous/models.md new file mode 100644 index 00000000..5e5ba6c5 --- /dev/null +++ b/docs/tutorials/synchronous/models.md @@ -0,0 +1,73 @@ +# Models + +The very first thing you need to create for pydantic-redis are the models (or schemas) that +the data you are to save in redis is to be based on. + +These models are derived from [pydantic's](https://docs.pydantic.dev/) `BaseModel`. + +## Import Pydantic-redis' `Model` + +First, import pydantic-redis' `Model` + +```Python hl_lines="5" +{!../docs_src/tutorials/synchronous/models.py!} +``` + +## Create the Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="8-15" +{!../docs_src/tutorials/synchronous/models.py!} +``` + +## Specify the `_primary_key_field` Attribute + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +In this case, there can be no two books with the same `title`. + +```Python hl_lines="9" +{!../docs_src/tutorials/synchronous/models.py!} +``` + +## Register the Model in the Store + +Then, in order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="26" +{!../docs_src/tutorials/synchronous/models.py!} +``` + +## Use the Model + +Then you can use the model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +The store is connected to the Redis instance, so any changes you make will +reflect in redis itself. + +```Python hl_lines="28-39" +{!../docs_src/tutorials/synchronous/models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce +(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first):: + +
+ +```console +$ python main.py +[ Book(title='Oliver Twist', author='Charles Dickens', rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)] +``` +
\ No newline at end of file diff --git a/docs/tutorials/synchronous/nested-models.md b/docs/tutorials/synchronous/nested-models.md new file mode 100644 index 00000000..3b4f728c --- /dev/null +++ b/docs/tutorials/synchronous/nested-models.md @@ -0,0 +1,140 @@ +# Nested Models + +The very first thing you need to create for pydantic-redis are the models (or schemas) that +the data you are to save in redis is to be based on. + +It is possible to refer one model in another model in a parent-child relationship. + +## Import Pydantic-redis' `Model` + +First, import pydantic-redis' `Model` + +```Python hl_lines="5" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Create the Child Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="8-11" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Set the `_primary_key_field` of the Child Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +In this case, there can be no two authors with the same `name`. + +```Python hl_lines="9" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Create the Parent Model + +Next, declare another model as a class that inherits from `Model`. + +Use standard Python types for all attributes, as before. + +```Python hl_lines="14-21" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Add the Nested Model to the Parent Model + +Annotate the field that is to hold the child model with the child class. + +In this case, the field `author` is annotated with `Author` class. + +```Python hl_lines="17" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Set the `_primary_key_field` of the Parent Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the parent Model. + +In this case, there can be no two books with the same `title`. + +```Python hl_lines="15" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Register the Models in the Store + +Then, in order for the store to know the existence of each given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="32-33" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Use the Parent Model + +Then you can use the parent model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +**Note: The child model will be automatically inserted, or updated if it already exists** + +The store is connected to the Redis instance, so any changes you make will +reflect in redis itself. + +```Python hl_lines="35-47" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Use the Child Model Independently + +You can also use the child model independently. + +Any mutation on the child model will also be reflected in the any parent model instances +fetched from redis after that mutation. + +```Python hl_lines="49-50" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Indirectly Update Child Model + +A child model can be indirectly updated via the parent model. + +Set the attribute containing the child model with an instance of the child model + +The new instance of the child model should have the **SAME** primary key as the original +child model. + +```Python hl_lines="52-56" +{!../docs_src/tutorials/synchronous/nested-models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce +(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first): + +
+ +```console +$ python main.py +book: +[ Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1999, 2007)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)] + +author: +[Author(name='Charles Dickens', active_years=(1999, 2007))] + +indirectly updated book: +[ Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1227, 1277)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)] + +indirectly updated author: +[Author(name='Charles Dickens', active_years=(1969, 1999))] +``` +
diff --git a/docs_src/index/async_main.py b/docs_src/index/async_main.py new file mode 100644 index 00000000..e553776e --- /dev/null +++ b/docs_src/index/async_main.py @@ -0,0 +1,89 @@ +import asyncio +import pprint +from datetime import date +from typing import Tuple, List +from pydantic_redis.asyncio import RedisConfig, Model, Store + + +class Author(Model): + _primary_key_field: str = "name" + name: str + active_years: Tuple[int, int] + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: Author + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +async def run_async(): + pp = pprint.PrettyPrinter(indent=4) + 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(Author) + + authors = { + "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), + "jane": Author(name="Jane Austen", active_years=(1580, 1640)), + } + + books = [ + Book( + title="Oliver Twist", + author=authors["charles"], + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ), + Book( + title="Great Expectations", + author=authors["charles"], + published_on=date(year=1220, month=4, day=4), + rating=5, + tags=["Classic"], + ), + Book( + title="Jane Eyre", + author=authors["charles"], + published_on=date(year=1225, month=6, day=4), + in_stock=False, + rating=3.4, + tags=["Classic", "Romance"], + ), + Book( + title="Wuthering Heights", + author=authors["jane"], + published_on=date(year=1600, month=4, day=4), + rating=4.0, + tags=["Classic", "Romance"], + ), + ] + + await Book.insert(books, life_span_seconds=3600) + all_books = await Book.select() + paginated_books = await Book.select(skip=2, limit=2) + paginated_books_with_few_fields = await Book.select( + columns=["author", "in_stock"], skip=2, limit=2 + ) + print("All:") + pp.pprint(all_books) + print("\nPaginated:") + pp.pprint(paginated_books) + print("\nPaginated but with few fields:") + pp.pprint(paginated_books_with_few_fields) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(run_async()) diff --git a/docs_src/index/sync_main.py b/docs_src/index/sync_main.py new file mode 100644 index 00000000..f83df7a7 --- /dev/null +++ b/docs_src/index/sync_main.py @@ -0,0 +1,83 @@ +import pprint +from datetime import date +from typing import Tuple, List +from pydantic_redis import RedisConfig, Model, Store + + +class Author(Model): + _primary_key_field: str = "name" + name: str + active_years: Tuple[int, int] + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: Author + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + 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(Author) + + authors = { + "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), + "jane": Author(name="Jane Austen", active_years=(1580, 1640)), + } + + books = [ + Book( + title="Oliver Twist", + author=authors["charles"], + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ), + Book( + title="Great Expectations", + author=authors["charles"], + published_on=date(year=1220, month=4, day=4), + rating=5, + tags=["Classic"], + ), + Book( + title="Jane Eyre", + author=authors["charles"], + published_on=date(year=1225, month=6, day=4), + in_stock=False, + rating=3.4, + tags=["Classic", "Romance"], + ), + Book( + title="Wuthering Heights", + author=authors["jane"], + published_on=date(year=1600, month=4, day=4), + rating=4.0, + tags=["Classic", "Romance"], + ), + ] + + Book.insert(books, life_span_seconds=3600) + all_books = Book.select() + paginated_books = Book.select(skip=2, limit=2) + paginated_books_with_few_fields = Book.select( + columns=["author", "in_stock"], skip=2, limit=2 + ) + print("All:") + pp.pprint(all_books) + print("\nPaginated:") + pp.pprint(paginated_books) + print("\nPaginated but with few fields:") + pp.pprint(paginated_books_with_few_fields) diff --git a/docs_src/tutorials/synchronous/models.py b/docs_src/tutorials/synchronous/models.py new file mode 100644 index 00000000..63f987d2 --- /dev/null +++ b/docs_src/tutorials/synchronous/models.py @@ -0,0 +1,40 @@ +import pprint +from datetime import date +from typing import List + +from pydantic_redis import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", + redis_config=RedisConfig(db=5, host="localhost", port=6379), + life_span_in_seconds=3600, + ) + + store.register_model(Book) + + Book.insert( + Book( + title="Oliver Twist", + author="Charles Dickens", + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ) + ) + + response = Book.select(ids=["Oliver Twist"]) + pp.pprint(response) diff --git a/docs_src/tutorials/synchronous/nested-models.py b/docs_src/tutorials/synchronous/nested-models.py new file mode 100644 index 00000000..2327595c --- /dev/null +++ b/docs_src/tutorials/synchronous/nested-models.py @@ -0,0 +1,66 @@ +import pprint +from datetime import date +from typing import List, Tuple + +from pydantic_redis import Model, Store, RedisConfig + + +class Author(Model): + _primary_key_field: str = "name" + name: str + active_years: Tuple[int, int] + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: Author + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", + redis_config=RedisConfig(db=5, host="localhost", port=6379), + life_span_in_seconds=3600, + ) + + store.register_model(Author) + store.register_model(Book) + + Book.insert( + Book( + title="Oliver Twist", + author=Author(name="Charles Dickens", active_years=(1999, 2007)), + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ) + ) + + book_response = Book.select(ids=["Oliver Twist"]) + author_response = Author.select(ids=["Charles Dickens"]) + + Author.update(_id="Charles Dickens", data={"active_years": (1227, 1277)}) + updated_book_response = Book.select(ids=["Oliver Twist"]) + + Book.update( + _id="Oliver Twist", + data={"author": Author(name="Charles Dickens", active_years=(1969, 1999))}, + ) + updated_author_response = Author.select(ids=["Charles Dickens"]) + + print("book:") + pp.pprint(book_response) + print("\nauthor:") + pp.pprint(author_response) + + print("\nindirectly updated book:") + pp.pprint(updated_book_response) + print("\nindirectly updated author:") + pp.pprint(updated_author_response) diff --git a/mkdocs.yml b/mkdocs.yml index 2f10bfc6..90efdb26 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,16 +5,16 @@ site_url: https://sopherapps.github.io/pydantic-redis/ theme: name: material palette: - - media: '(prefers-color-scheme: light)' - scheme: default - toggle: - icon: material/lightbulb - name: Switch to light mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - toggle: - icon: material/lightbulb-outline - name: Switch to dark mode + - media: '(prefers-color-scheme: light)' + scheme: default + toggle: + icon: material/lightbulb + name: Switch to light mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + toggle: + icon: material/lightbulb-outline + name: Switch to dark mode features: - search.suggest - search.highlight @@ -30,13 +30,12 @@ repo_url: https://github.com/sopherapps/pydantic-redis nav: - 'Pydantic-redis': index.md - Tutorials: + - tutorials/intro.md - Synchronous API: - - tutorials/synchronous/getting-started.md + - tutorials/synchronous/models.md + - tutorials/synchronous/nested-models.md - Asynchronous API: - - tutorials/asynchronous/getting-started.md - - 'How-to Guides': - - how-to-guides/creating-nested-models.md - - how-to-guides/paginating-queries.md + - tutorials/asynchronous/models.md - 'Explanation': - explanation/why-use-orms.md - reference.md @@ -47,4 +46,12 @@ markdown_extensions: anchor_linenums: true - pymdownx.inlinehilite - pymdownx.snippets - - pymdownx.superfences \ No newline at end of file + - pymdownx.superfences + - mdx_include: + base_path: docs +extra_css: + - css/termynal.css + - css/custom.css +extra_javascript: + - js/termynal.js + - js/custom.js \ No newline at end of file diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py index 3c1f1332..ae2d8198 100644 --- a/pydantic_redis/__init__.py +++ b/pydantic_redis/__init__.py @@ -4,19 +4,22 @@ A simple declarative ORM for redis based on pydantic -Provides - 1. A subclass-able `Model` class to create Object Relational Mapping to - redis hashes - 2. A redis `Store` class to mutate and query `Model`'s registered in it - 3. A `RedisConfig` class to pass to the `Store` constructor to connect - to a redis instance - 4. A synchronous `syncio` and an asynchronous `asyncio` interface to the - above classes +Provides: + +1. A subclass-able `Model` class to create Object Relational Mapping to +redis hashes +2. A redis `Store` class to mutate and query `Model`'s registered in it +3. A `RedisConfig` class to pass to the `Store` constructor to connect +to a redis instance +4. A synchronous `syncio` and an asynchronous `asyncio` interface to the +above classes Available subpackages --------------------- + asyncio Asynchronous API for the ORM + syncio Synchronous API for the ORM """ diff --git a/requirements.txt b/requirements.txt index 9af1b877..d023eccf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ pytest-asyncio mkdocs mkdocstrings mkdocs-material +mdx_include From 5c8f7ffb00c614593d99a00965d8e6d17720e3e0 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 01:31:40 +0300 Subject: [PATCH 06/16] Add docs for synchronous list of nested models --- docs/tutorials/intro.md | 16 +- .../synchronous/list-of-nested-models.md | 145 ++++++++++++++++++ .../synchronous/list-of-nested-models.py | 77 ++++++++++ mkdocs.yml | 1 + 4 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/synchronous/list-of-nested-models.md create mode 100644 docs_src/tutorials/synchronous/list-of-nested-models.py diff --git a/docs/tutorials/intro.md b/docs/tutorials/intro.md index ce3b40d9..b460fdda 100644 --- a/docs/tutorials/intro.md +++ b/docs/tutorials/intro.md @@ -10,7 +10,7 @@ so that one can go directly to a specific topic, just like a reference. ## Synchronous API In case you are looking to use pydantic-redis without async/await, you can read the **Synchronous API** version of this -tutorial. +tutorial. This is the default API for pydantic-redis. @@ -20,6 +20,20 @@ In case you are looking to use pydantic-redis with async/await, e.g. in [FastAPI or [asyncio](https://docs.python.org/3/library/asyncio.html) , you can read the **Asynchronous API** version of this tutorial. +## Install Python + +Pydantic-redis requires python 3.6 and above. The latest stable python version is the recommended version. + +You can install python from [the official python downloads site](https://www.python.org/downloads/). + +## Install Redis + +In order to use pydantic-redis, you need a redis server instance running. You can install a local instance +via [the official redis stack](https://redis.io/docs/stack/get-started/install/) instructions. + +You may also need a visual client to view the data in redis. The recommended app to use +is [RedisInsight](https://redis.com/redis-enterprise/redis-insight/). + ## Run the Code All the code blocks can be copied and used directly. diff --git a/docs/tutorials/synchronous/list-of-nested-models.md b/docs/tutorials/synchronous/list-of-nested-models.md new file mode 100644 index 00000000..ebb5869b --- /dev/null +++ b/docs/tutorials/synchronous/list-of-nested-models.md @@ -0,0 +1,145 @@ +# Fields of Lists of Nested Models + +Sometimes, one might need to have models (schemas) that have lists of other models (schemas). + +An example is a `Folder` model that can have child `Folder`'s and `File`'s. + +This can easily be pulled off with pydantic-redis. + +## Import Pydantic-redis' `Model` + +First, import pydantic-redis' `Model` + +```Python hl_lines="5" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Create the Child Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="14-17" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Child Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +In this case, there can be no two `File`'s with the same `path`. + +```Python hl_lines="15" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Create the Parent Model + +Next, declare another model as a class that inherits from `Model`. + +Use standard Python types for all attributes, as before. + +```Python hl_lines="20-24" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Add the Nested Model List to the Parent Model + +Annotate the field that is to hold the child model list with the List of child class. + +In this case, the field `files` is annotated with `File` class. + +And the field `folders` is annotated with `"Folder"` class i.e. itself. + +```Python hl_lines="23-24" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Parent Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the parent Model. + +In this case, there can be no two `Folder`'s with the same `path`. + +```Python hl_lines="21" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Register the Models in the Store + +Then, in order for the store to know the existence of each given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="35-36" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Use the Parent Model + +Then you can use the parent model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +**Note: The child models will be automatically inserted, or updated if they already exist** + +The store is connected to the Redis instance, so any changes you make will +reflect in redis itself. + +```Python hl_lines="38-60" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Use the Child Model Independently + +You can also use the child model independently. + +Any mutation on the child model will also be reflected in the any parent model instances +fetched from redis after that mutation. + +```Python hl_lines="62-67" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Indirectly Update Child Model + +A child model can be indirectly updated via the parent model. + +Set the attribute containing the child model list with a list of instances of the child model + +If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis. + +```Python hl_lines="62-65" +{!../docs_src/tutorials/synchronous/list-of-nested-models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce +(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first): + +
+ +```console +$ python main.py +parent folder: +[ Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=), File(path='path/to/bar.jpg', type=)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=), File(path='path/to/foo.jpg', type=)], folders=[])])] + +files: +[ File(path='path/to/foo.txt', type=), + File(path='path/to/foo.jpg', type=), + File(path='path/to/bar.txt', type=), + File(path='path/to/bar.jpg', type=)] + +indirectly updated parent folder: +[ Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=), File(path='path/to/bar.jpg', type=)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=)], folders=[])])] + +indirectly updated files: +[File(path='path/to/foo.txt', type=)] +``` +
diff --git a/docs_src/tutorials/synchronous/list-of-nested-models.py b/docs_src/tutorials/synchronous/list-of-nested-models.py new file mode 100644 index 00000000..fa4961a0 --- /dev/null +++ b/docs_src/tutorials/synchronous/list-of-nested-models.py @@ -0,0 +1,77 @@ +import pprint +from enum import Enum +from typing import List + +from pydantic_redis import Model, Store, RedisConfig + + +class FileType(Enum): + TEXT = "text" + IMAGE = "image" + EXEC = "executable" + + +class File(Model): + _primary_key_field: str = "path" + path: str + type: FileType + + +class Folder(Model): + _primary_key_field: str = "path" + path: str + files: List[File] = [] + folders: List["Folder"] = [] + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", + redis_config=RedisConfig(db=5, host="localhost", port=6379), + life_span_in_seconds=3600, + ) + + store.register_model(File) + store.register_model(Folder) + + child_folder = Folder( + path="path/to/child-folder", + files=[ + File(path="path/to/foo.txt", type=FileType.TEXT), + File(path="path/to/foo.jpg", type=FileType.IMAGE), + ], + ) + + Folder.insert( + Folder( + path="path/to/parent-folder", + files=[ + File(path="path/to/bar.txt", type=FileType.TEXT), + File(path="path/to/bar.jpg", type=FileType.IMAGE), + ], + folders=[child_folder], + ) + ) + + parent_folder_response = Folder.select(ids=["path/to/parent-folder"]) + files_response = File.select( + ids=["path/to/foo.txt", "path/to/foo.jpg", "path/to/bar.txt", "path/to/bar.jpg"] + ) + + Folder.update( + _id="path/to/child-folder", + data={"files": [File(path="path/to/foo.txt", type=FileType.EXEC)]}, + ) + updated_parent_folder_response = Folder.select(ids=["path/to/parent-folder"]) + updated_file_response = File.select(ids=["path/to/foo.txt"]) + + print("parent folder:") + pp.pprint(parent_folder_response) + print("\nfiles:") + pp.pprint(files_response) + + print("\nindirectly updated parent folder:") + pp.pprint(updated_parent_folder_response) + print("\nindirectly updated files:") + pp.pprint(updated_file_response) diff --git a/mkdocs.yml b/mkdocs.yml index 90efdb26..a15c5985 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,7 @@ nav: - Synchronous API: - tutorials/synchronous/models.md - tutorials/synchronous/nested-models.md + - tutorials/synchronous/list-of-nested-models.md - Asynchronous API: - tutorials/asynchronous/models.md - 'Explanation': From f95415ed6300598eaaeb11deb76f2a105fb2c401 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 02:53:18 +0300 Subject: [PATCH 07/16] Add docs for synchronous tuples of nested models --- .../synchronous/list-of-nested-models.md | 4 +- .../synchronous/tuple-of-nested-models.md | 142 ++++++++++++++++++ .../synchronous/tuple-of-nested-models.py | 59 ++++++++ mkdocs.yml | 1 + 4 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 docs/tutorials/synchronous/tuple-of-nested-models.md create mode 100644 docs_src/tutorials/synchronous/tuple-of-nested-models.py diff --git a/docs/tutorials/synchronous/list-of-nested-models.md b/docs/tutorials/synchronous/list-of-nested-models.md index ebb5869b..427376ad 100644 --- a/docs/tutorials/synchronous/list-of-nested-models.md +++ b/docs/tutorials/synchronous/list-of-nested-models.md @@ -1,4 +1,4 @@ -# Fields of Lists of Nested Models +# Lists of Nested Models Sometimes, one might need to have models (schemas) that have lists of other models (schemas). @@ -49,7 +49,7 @@ Use standard Python types for all attributes, as before. Annotate the field that is to hold the child model list with the List of child class. -In this case, the field `files` is annotated with `File` class. +In this case, the field `files` is annotated with `List[File]`. And the field `folders` is annotated with `"Folder"` class i.e. itself. diff --git a/docs/tutorials/synchronous/tuple-of-nested-models.md b/docs/tutorials/synchronous/tuple-of-nested-models.md new file mode 100644 index 00000000..3e4221ab --- /dev/null +++ b/docs/tutorials/synchronous/tuple-of-nested-models.md @@ -0,0 +1,142 @@ +# Tuples of Nested Models + +Sometimes, one might need to have models (schemas) that have tuples of other models (schemas). + +An example is a `ScoreBoard` model that can have Tuples of player name and `Scores`'. + +This can easily be pulled off with pydantic-redis. + +## Import Pydantic-redis' `Model` + +First, import pydantic-redis' `Model` + +```Python hl_lines="3" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Create the Child Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="6-9" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Child Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +In this case, there can be no two `Score`'s with the same `id`. + +```Python hl_lines="7" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Create the Parent Model + +Next, declare another model as a class that inherits from `Model`. + +Use standard Python types for all attributes, as before. + +```Python hl_lines="12-15" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Add the Nested Model Tuple to the Parent Model + +Annotate the field that is to hold the tuple of child models with the Tuple of child class. + +In this case, the field `scores` is annotated with `Tuple[str, Score]` class. + +The `str` is the player's name. + +```Python hl_lines="15" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Parent Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the parent Model. + +In this case, there can be no two `ScoreBoard`'s with the same `id`. + +```Python hl_lines="13" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Register the Models in the Store + +Then, in order for the store to know the existence of each given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="22-23" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Use the Parent Model + +Then you can use the parent model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +**Note: The child models will be automatically inserted, or updated if they already exist** + +The store is connected to the Redis instance, so any changes you make will +reflect in redis itself. + +```Python hl_lines="25-27" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Use the Child Model Independently + +You can also use the child model independently. + +Any mutation on the child model will also be reflected in the any parent model instances +fetched from redis after that mutation. + +```Python hl_lines="29-30" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Indirectly Update Child Model + +A child model can be indirectly updated via the parent model. + +Set the attribute containing the child model tuple with a tuple of instances of the child model + +If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis. + +```Python hl_lines="32-36" +{!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce +(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first): + +
+ +```console +$ python main.py +score board: +[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=50)))] + +scores: +[Score(id='some id', total=50)] + +indirectly updated score board: +[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=78)))] + +indirectly updated score: +[Score(id='some id', total=60)] +``` +
diff --git a/docs_src/tutorials/synchronous/tuple-of-nested-models.py b/docs_src/tutorials/synchronous/tuple-of-nested-models.py new file mode 100644 index 00000000..89f95313 --- /dev/null +++ b/docs_src/tutorials/synchronous/tuple-of-nested-models.py @@ -0,0 +1,59 @@ +import pprint +from typing import Tuple +from pydantic_redis import RedisConfig, Model, Store + + +class Score(Model): + _primary_key_field: str = "id" + id: str + total: int + + +class ScoreBoard(Model): + _primary_key_field: str = "id" + id: str + scores: Tuple[str, Score] + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store(name="test", redis_config=RedisConfig()) + + store.register_model(Score) + store.register_model(ScoreBoard) + + ScoreBoard.insert( + data=ScoreBoard( + id="test", + scores=( + "mark", + Score(id="some id", total=50), + ), + ) + ) + score_board_response = ScoreBoard.select(ids=["test"]) + scores_response = Score.select(ids=["some id"]) + + Score.update(_id="some id", data={"total": 78}) + updated_score_board_response = ScoreBoard.select(ids=["test"]) + + ScoreBoard.update( + _id="test", + data={ + "scores": ( + "tom", + Score(id="some id", total=60), + ) + }, + ) + updated_score_response = Score.select(ids=["some id"]) + + print("score board:") + pp.pprint(score_board_response) + print("\nscores:") + pp.pprint(scores_response) + + print("\nindirectly updated score board:") + pp.pprint(updated_score_board_response) + print("\nindirectly updated score:") + pp.pprint(updated_score_response) diff --git a/mkdocs.yml b/mkdocs.yml index a15c5985..134cf441 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - tutorials/synchronous/models.md - tutorials/synchronous/nested-models.md - tutorials/synchronous/list-of-nested-models.md + - tutorials/synchronous/tuple-of-nested-models.md - Asynchronous API: - tutorials/asynchronous/models.md - 'Explanation': From 53cdb5e7ea99b56477784ee7dafeb59d9ec50341 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 03:01:01 +0300 Subject: [PATCH 08/16] Fix typo in docs for synchronous tuples of nested models --- docs/tutorials/synchronous/tuple-of-nested-models.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/synchronous/tuple-of-nested-models.md b/docs/tutorials/synchronous/tuple-of-nested-models.md index 3e4221ab..e7f336b9 100644 --- a/docs/tutorials/synchronous/tuple-of-nested-models.md +++ b/docs/tutorials/synchronous/tuple-of-nested-models.md @@ -91,7 +91,7 @@ Then you can use the parent model class to: The store is connected to the Redis instance, so any changes you make will reflect in redis itself. -```Python hl_lines="25-27" +```Python hl_lines="25-35" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} ``` @@ -102,7 +102,7 @@ You can also use the child model independently. Any mutation on the child model will also be reflected in the any parent model instances fetched from redis after that mutation. -```Python hl_lines="29-30" +```Python hl_lines="37-38" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} ``` @@ -114,7 +114,7 @@ Set the attribute containing the child model tuple with a tuple of instances of If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis. -```Python hl_lines="32-36" +```Python hl_lines="40-49" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} ``` From c509df0b344a172d6db43948386e7598c7a39f17 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 10:44:13 +0300 Subject: [PATCH 09/16] Add docs for inserting into redis synchronously --- docs/tutorials/synchronous/insert.md | 79 ++++++++++++++++++++++++ docs_src/tutorials/synchronous/insert.py | 39 ++++++++++++ mkdocs.yml | 1 + 3 files changed, 119 insertions(+) create mode 100644 docs/tutorials/synchronous/insert.md create mode 100644 docs_src/tutorials/synchronous/insert.py diff --git a/docs/tutorials/synchronous/insert.md b/docs/tutorials/synchronous/insert.md new file mode 100644 index 00000000..7f56dc63 --- /dev/null +++ b/docs/tutorials/synchronous/insert.md @@ -0,0 +1,79 @@ +# Insert + +Pydantic-redis can be used to insert new model instances into redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="5-8 17" +{!../docs_src/tutorials/synchronous/insert.py!} +``` + +## Insert One Record + +To add a single record to the redis instance, pass that model's instance as first argument to the model's `insert` +method + +```Python hl_lines="19" +{!../docs_src/tutorials/synchronous/insert.py!} +``` + +## Insert One Record With TTL + +To make the record added to redis temporary, add a `life_span_seconds` (Time To Live i.e. TTL) key-word argument +when calling the model's `insert` method. + +When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during +initialization is used. + +The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. + +```Python hl_lines="20-23 13-15" +{!../docs_src/tutorials/synchronous/insert.py!} +``` + +## Insert Many Records + +To add many records to the redis instance, pass a list of that model's instances as first argument to the model's +`insert` method. + +Adding many records at once is more performant than adding one record at a time repeatedly because less network requests +are made in the former. + +```Python hl_lines="24-29" +{!../docs_src/tutorials/synchronous/insert.py!} +``` + +## Insert Many Records With TTL + +To add temporary records to redis, add a `life_span_seconds` (Time To Live i.e. TTL) argument +when calling the model's `insert` method. + +When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during +initialization is used. + +The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. + +```Python hl_lines="30-36 13-15" +{!../docs_src/tutorials/synchronous/insert.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce +(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first):: + +
+ +```console +$ python main.py +[ Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Great Expectations', author='Charles Dickens'), + Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Pride and Prejudice', author='Jane Austen')] +``` +
\ No newline at end of file diff --git a/docs_src/tutorials/synchronous/insert.py b/docs_src/tutorials/synchronous/insert.py new file mode 100644 index 00000000..6c87d792 --- /dev/null +++ b/docs_src/tutorials/synchronous/insert.py @@ -0,0 +1,39 @@ +import pprint +from pydantic_redis import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + Book.insert(Book(title="Oliver Twist", author="Charles Dickens")) + Book.insert( + Book(title="Great Expectations", author="Charles Dickens"), + life_span_seconds=1800, + ) + Book.insert( + [ + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + ] + ) + Book.insert( + [ + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + ], + life_span_seconds=3600, + ) + + response = Book.select() + pp.pprint(response) diff --git a/mkdocs.yml b/mkdocs.yml index 134cf441..34a88ea6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,6 +33,7 @@ nav: - tutorials/intro.md - Synchronous API: - tutorials/synchronous/models.md + - tutorials/synchronous/insert.md - tutorials/synchronous/nested-models.md - tutorials/synchronous/list-of-nested-models.md - tutorials/synchronous/tuple-of-nested-models.md From d492144998735926cb51a0e044cc5638230a6f01 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 11:53:46 +0300 Subject: [PATCH 10/16] Add admonition or call-outs to docs --- docs/tutorials/intro.md | 8 +++-- docs/tutorials/synchronous/insert.md | 33 +++++++++++-------- .../synchronous/list-of-nested-models.md | 32 +++++++++++------- docs/tutorials/synchronous/models.md | 14 +++++--- docs/tutorials/synchronous/nested-models.md | 33 ++++++++++++------- .../synchronous/tuple-of-nested-models.md | 31 ++++++++++------- mkdocs.yml | 1 + 7 files changed, 95 insertions(+), 57 deletions(-) diff --git a/docs/tutorials/intro.md b/docs/tutorials/intro.md index b460fdda..1eeda945 100644 --- a/docs/tutorials/intro.md +++ b/docs/tutorials/intro.md @@ -12,7 +12,8 @@ so that one can go directly to a specific topic, just like a reference. In case you are looking to use pydantic-redis without async/await, you can read the **Synchronous API** version of this tutorial. -This is the default API for pydantic-redis. +!!! info + This is the default API for pydantic-redis. ## Asynchronous API @@ -31,8 +32,9 @@ You can install python from [the official python downloads site](https://www.pyt In order to use pydantic-redis, you need a redis server instance running. You can install a local instance via [the official redis stack](https://redis.io/docs/stack/get-started/install/) instructions. -You may also need a visual client to view the data in redis. The recommended app to use -is [RedisInsight](https://redis.com/redis-enterprise/redis-insight/). +!!! info + You may also need a visual client to view the data in redis. The recommended app to use + is [RedisInsight](https://redis.com/redis-enterprise/redis-insight/). ## Run the Code diff --git a/docs/tutorials/synchronous/insert.md b/docs/tutorials/synchronous/insert.md index 7f56dc63..e62dd706 100644 --- a/docs/tutorials/synchronous/insert.md +++ b/docs/tutorials/synchronous/insert.md @@ -27,12 +27,13 @@ method To make the record added to redis temporary, add a `life_span_seconds` (Time To Live i.e. TTL) key-word argument when calling the model's `insert` method. -When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during -initialization is used. +!!! info + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. -The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. - -```Python hl_lines="20-23 13-15" +```Python hl_lines="20-23" {!../docs_src/tutorials/synchronous/insert.py!} ``` @@ -41,8 +42,9 @@ The `life_span_in_seconds` in both cases is `None` by default. This means record To add many records to the redis instance, pass a list of that model's instances as first argument to the model's `insert` method. -Adding many records at once is more performant than adding one record at a time repeatedly because less network requests -are made in the former. +!!! info + Adding many records at once is more performant than adding one record at a time repeatedly because less network requests + are made in the former. ```Python hl_lines="24-29" {!../docs_src/tutorials/synchronous/insert.py!} @@ -53,19 +55,22 @@ are made in the former. To add temporary records to redis, add a `life_span_seconds` (Time To Live i.e. TTL) argument when calling the model's `insert` method. -When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during -initialization is used. - -The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. +!!! info + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. -```Python hl_lines="30-36 13-15" +```Python hl_lines="30-36" {!../docs_src/tutorials/synchronous/insert.py!} ``` ## Run the App -Running the above code in a file `main.py` would produce -(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first):: +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
diff --git a/docs/tutorials/synchronous/list-of-nested-models.md b/docs/tutorials/synchronous/list-of-nested-models.md index 427376ad..4a1d82c5 100644 --- a/docs/tutorials/synchronous/list-of-nested-models.md +++ b/docs/tutorials/synchronous/list-of-nested-models.md @@ -29,7 +29,8 @@ Use standard Python types for all attributes. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the Model. -In this case, there can be no two `File`'s with the same `path`. +!!! example + In this case, there can be no two `File`'s with the same `path`. ```Python hl_lines="15" {!../docs_src/tutorials/synchronous/list-of-nested-models.py!} @@ -49,9 +50,10 @@ Use standard Python types for all attributes, as before. Annotate the field that is to hold the child model list with the List of child class. -In this case, the field `files` is annotated with `List[File]`. - -And the field `folders` is annotated with `"Folder"` class i.e. itself. +!!! example + In this case, the field `files` is annotated with `List[File]`. + + And the field `folders` is annotated with `"Folder"` class i.e. itself. ```Python hl_lines="23-24" {!../docs_src/tutorials/synchronous/list-of-nested-models.py!} @@ -62,7 +64,8 @@ And the field `folders` is annotated with `"Folder"` class i.e. itself. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the parent Model. -In this case, there can be no two `Folder`'s with the same `path`. +!!! example + In this case, there can be no two `Folder`'s with the same `path`. ```Python hl_lines="21" {!../docs_src/tutorials/synchronous/list-of-nested-models.py!} @@ -86,10 +89,12 @@ Then you can use the parent model class to: - `delete` from store - `select` from store -**Note: The child models will be automatically inserted, or updated if they already exist** +!!! info + The child models will be automatically inserted, or updated if they already exist -The store is connected to the Redis instance, so any changes you make will -reflect in redis itself. +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. ```Python hl_lines="38-60" {!../docs_src/tutorials/synchronous/list-of-nested-models.py!} @@ -99,8 +104,9 @@ reflect in redis itself. You can also use the child model independently. -Any mutation on the child model will also be reflected in the any parent model instances -fetched from redis after that mutation. +!!! info + Any mutation on the child model will also be reflected in the any parent model instances + fetched from redis after that mutation. ```Python hl_lines="62-67" {!../docs_src/tutorials/synchronous/list-of-nested-models.py!} @@ -120,8 +126,10 @@ If there is any new instance of the child model that has a pre-existing primary ## Run the App -Running the above code in a file `main.py` would produce -(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first): +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
diff --git a/docs/tutorials/synchronous/models.md b/docs/tutorials/synchronous/models.md index 5e5ba6c5..4635b4c4 100644 --- a/docs/tutorials/synchronous/models.md +++ b/docs/tutorials/synchronous/models.md @@ -28,7 +28,8 @@ Use standard Python types for all attributes. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the Model. -In this case, there can be no two books with the same `title`. +!!! example + In this case, there can be no two books with the same `title`. ```Python hl_lines="9" {!../docs_src/tutorials/synchronous/models.py!} @@ -52,8 +53,9 @@ Then you can use the model class to: - `delete` from store - `select` from store -The store is connected to the Redis instance, so any changes you make will -reflect in redis itself. +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. ```Python hl_lines="28-39" {!../docs_src/tutorials/synchronous/models.py!} @@ -61,8 +63,10 @@ reflect in redis itself. ## Run the App -Running the above code in a file `main.py` would produce -(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first):: +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
diff --git a/docs/tutorials/synchronous/nested-models.md b/docs/tutorials/synchronous/nested-models.md index 3b4f728c..c6416fc4 100644 --- a/docs/tutorials/synchronous/nested-models.md +++ b/docs/tutorials/synchronous/nested-models.md @@ -28,7 +28,8 @@ Use standard Python types for all attributes. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the Model. -In this case, there can be no two authors with the same `name`. +!!! example + In this case, there can be no two authors with the same `name`. ```Python hl_lines="9" {!../docs_src/tutorials/synchronous/nested-models.py!} @@ -48,7 +49,8 @@ Use standard Python types for all attributes, as before. Annotate the field that is to hold the child model with the child class. -In this case, the field `author` is annotated with `Author` class. +!!! example + In this case, the field `author` is annotated with `Author` class. ```Python hl_lines="17" {!../docs_src/tutorials/synchronous/nested-models.py!} @@ -59,7 +61,8 @@ In this case, the field `author` is annotated with `Author` class. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the parent Model. -In this case, there can be no two books with the same `title`. +!!! example + In this case, there can be no two books with the same `title`. ```Python hl_lines="15" {!../docs_src/tutorials/synchronous/nested-models.py!} @@ -83,10 +86,12 @@ Then you can use the parent model class to: - `delete` from store - `select` from store -**Note: The child model will be automatically inserted, or updated if it already exists** +!!! note + The child model will be automatically inserted, or updated if it already exists -The store is connected to the Redis instance, so any changes you make will -reflect in redis itself. +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. ```Python hl_lines="35-47" {!../docs_src/tutorials/synchronous/nested-models.py!} @@ -96,8 +101,9 @@ reflect in redis itself. You can also use the child model independently. -Any mutation on the child model will also be reflected in the any parent model instances -fetched from redis after that mutation. +!!! info + Any mutation on the child model will also be reflected in the any parent model instances + fetched from redis after that mutation. ```Python hl_lines="49-50" {!../docs_src/tutorials/synchronous/nested-models.py!} @@ -109,8 +115,9 @@ A child model can be indirectly updated via the parent model. Set the attribute containing the child model with an instance of the child model -The new instance of the child model should have the **SAME** primary key as the original -child model. +!!! note + The new instance of the child model should have the **SAME** primary key as the original + child model. ```Python hl_lines="52-56" {!../docs_src/tutorials/synchronous/nested-models.py!} @@ -118,8 +125,10 @@ child model. ## Run the App -Running the above code in a file `main.py` would produce -(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first): +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
diff --git a/docs/tutorials/synchronous/tuple-of-nested-models.md b/docs/tutorials/synchronous/tuple-of-nested-models.md index e7f336b9..45210fa2 100644 --- a/docs/tutorials/synchronous/tuple-of-nested-models.md +++ b/docs/tutorials/synchronous/tuple-of-nested-models.md @@ -29,7 +29,8 @@ Use standard Python types for all attributes. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the Model. -In this case, there can be no two `Score`'s with the same `id`. +!!! example + In this case, there can be no two `Score`'s with the same `id`. ```Python hl_lines="7" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} @@ -49,9 +50,11 @@ Use standard Python types for all attributes, as before. Annotate the field that is to hold the tuple of child models with the Tuple of child class. -In this case, the field `scores` is annotated with `Tuple[str, Score]` class. +!!! example + In this case, the field `scores` is annotated with `Tuple[str, Score]` class. -The `str` is the player's name. +!!! info + The `str` is the player's name. ```Python hl_lines="15" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} @@ -62,7 +65,8 @@ The `str` is the player's name. Set the `_primary_key_field` attribute to the name of the attribute that is to act as a unique identifier for each instance of the parent Model. -In this case, there can be no two `ScoreBoard`'s with the same `id`. +!!! example + In this case, there can be no two `ScoreBoard`'s with the same `id`. ```Python hl_lines="13" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} @@ -86,10 +90,12 @@ Then you can use the parent model class to: - `delete` from store - `select` from store -**Note: The child models will be automatically inserted, or updated if they already exist** +!!! info + The child models will be automatically inserted, or updated if they already exist -The store is connected to the Redis instance, so any changes you make will -reflect in redis itself. +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. ```Python hl_lines="25-35" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} @@ -99,8 +105,9 @@ reflect in redis itself. You can also use the child model independently. -Any mutation on the child model will also be reflected in the any parent model instances -fetched from redis after that mutation. +!!! info + Any mutation on the child model will also be reflected in the any parent model instances + fetched from redis after that mutation. ```Python hl_lines="37-38" {!../docs_src/tutorials/synchronous/tuple-of-nested-models.py!} @@ -120,8 +127,10 @@ If there is any new instance of the child model that has a pre-existing primary ## Run the App -Running the above code in a file `main.py` would produce -(Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first): +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first
diff --git a/mkdocs.yml b/mkdocs.yml index 34a88ea6..b44447c6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,6 +45,7 @@ nav: - change-log.md markdown_extensions: + - admonition - pymdownx.highlight: anchor_linenums: true - pymdownx.inlinehilite From 667485a358e436812844ddbad59b07f229484b8e Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 11:54:35 +0300 Subject: [PATCH 11/16] Add docs for updating redis synchronously --- docs/tutorials/synchronous/update.md | 83 ++++++++++++++++++++++++ docs_src/tutorials/synchronous/update.py | 46 +++++++++++++ mkdocs.yml | 1 + 3 files changed, 130 insertions(+) create mode 100644 docs/tutorials/synchronous/update.md create mode 100644 docs_src/tutorials/synchronous/update.py diff --git a/docs/tutorials/synchronous/update.md b/docs/tutorials/synchronous/update.md new file mode 100644 index 00000000..c8571a71 --- /dev/null +++ b/docs/tutorials/synchronous/update.md @@ -0,0 +1,83 @@ +# Update + +Pydantic-redis can be used to update model instances in redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="5-8 17" +{!../docs_src/tutorials/synchronous/update.py!} +``` + +## Update One Record + +To update a single record in redis, pass the primary key (`_id`) of that record and the new changes to the model's `update` +method + +```Python hl_lines="26" +{!../docs_src/tutorials/synchronous/update.py!} +``` + +## Update One Record With TTL + +To update the record's time-to-live (TTL) also, pass the `life_span_seconds` argument to the model's `update` method. + +!!! info + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. + +```Python hl_lines="27-29" +{!../docs_src/tutorials/synchronous/update.py!} +``` + +## Update/Upsert Many Records + +To update many records in redis, pass a list of that model's instances as first argument to the model's +`insert` method. + +Technically, this will insert any records that don't exist and overwrite any that exist already. + +!!! info + Updating many records at once is more performant than adding one record at a time repeatedly because less network requests + are made in the former. + +!!! warning + Calling `insert` always overwrites the time-to-live of the records updated. + + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + By default `life_span_seconds` is `None` i.e. the time-to-live is removed and the updated records never expire. + +```Python hl_lines="32-39" +{!../docs_src/tutorials/synchronous/update.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +single update: +[ Book(title='Jane Eyre', author='Daniel McKenzie'), + Book(title='Oliver Twist', author='Charlie Ickens'), + Book(title='Pride and Prejudice', author='Jane Austen')] + +multi update: +[ Book(title='Jane Eyre', author='Emiliano Bronte'), + Book(title='Oliver Twist', author='Chuck Dickens'), + Book(title='Pride and Prejudice', author='Janey Austen')] +``` +
\ No newline at end of file diff --git a/docs_src/tutorials/synchronous/update.py b/docs_src/tutorials/synchronous/update.py new file mode 100644 index 00000000..ff310e17 --- /dev/null +++ b/docs_src/tutorials/synchronous/update.py @@ -0,0 +1,46 @@ +import pprint +from pydantic_redis import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + Book.insert( + [ + Book(title="Oliver Twist", author="Charles Dickens"), + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + ] + ) + Book.update(_id="Oliver Twist", data={"author": "Charlie Ickens"}) + Book.update( + _id="Jane Eyre", data={"author": "Daniel McKenzie"}, life_span_seconds=1800 + ) + single_update_response = Book.select() + + Book.insert( + [ + Book(title="Oliver Twist", author="Chuck Dickens"), + Book(title="Jane Eyre", author="Emiliano Bronte"), + Book(title="Pride and Prejudice", author="Janey Austen"), + ], + life_span_seconds=3600, + ) + multi_update_response = Book.select() + + print("single update:") + pp.pprint(single_update_response) + + print("\nmulti update:") + pp.pprint(multi_update_response) diff --git a/mkdocs.yml b/mkdocs.yml index b44447c6..515c030c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,6 +34,7 @@ nav: - Synchronous API: - tutorials/synchronous/models.md - tutorials/synchronous/insert.md + - tutorials/synchronous/update.md - tutorials/synchronous/nested-models.md - tutorials/synchronous/list-of-nested-models.md - tutorials/synchronous/tuple-of-nested-models.md From d88023b54c04d5fe30e6dcfec06fd0c535c4c299 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 12:09:17 +0300 Subject: [PATCH 12/16] Add docs for deleting redis synchronously --- docs/tutorials/synchronous/delete.md | 45 ++++++++++++++++++++++++ docs_src/tutorials/synchronous/delete.py | 36 +++++++++++++++++++ mkdocs.yml | 1 + 3 files changed, 82 insertions(+) create mode 100644 docs/tutorials/synchronous/delete.md create mode 100644 docs_src/tutorials/synchronous/delete.py diff --git a/docs/tutorials/synchronous/delete.md b/docs/tutorials/synchronous/delete.md new file mode 100644 index 00000000..1fbacb28 --- /dev/null +++ b/docs/tutorials/synchronous/delete.md @@ -0,0 +1,45 @@ +# Delete + +Pydantic-redis can be used to delete model instances from redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="5-8 17" +{!../docs_src/tutorials/synchronous/delete.py!} +``` + +## Delete Records + +To delete many records from redis, pass a list of primary keys (`ids`) of the records to the model's `delete` method. + +```Python hl_lines="29" +{!../docs_src/tutorials/synchronous/delete.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +pre-delete: +[ Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Utah Blaine', author="Louis L'Amour"), + Book(title='Pride and Prejudice', author='Jane Austen')] + +post-delete: +[ Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Utah Blaine', author="Louis L'Amour")] +``` +
\ No newline at end of file diff --git a/docs_src/tutorials/synchronous/delete.py b/docs_src/tutorials/synchronous/delete.py new file mode 100644 index 00000000..2087a642 --- /dev/null +++ b/docs_src/tutorials/synchronous/delete.py @@ -0,0 +1,36 @@ +import pprint +from pydantic_redis import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + Book.insert( + [ + Book(title="Oliver Twist", author="Charles Dickens"), + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + Book(title="Utah Blaine", author="Louis L'Amour"), + ] + ) + pre_delete_response = Book.select() + + Book.delete(ids=["Oliver Twist", "Pride and Prejudice"]) + post_delete_response = Book.select() + + print("pre-delete:") + pp.pprint(pre_delete_response) + + print("\npost-delete:") + pp.pprint(post_delete_response) diff --git a/mkdocs.yml b/mkdocs.yml index 515c030c..643a94f0 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - tutorials/synchronous/models.md - tutorials/synchronous/insert.md - tutorials/synchronous/update.md + - tutorials/synchronous/delete.md - tutorials/synchronous/nested-models.md - tutorials/synchronous/list-of-nested-models.md - tutorials/synchronous/tuple-of-nested-models.md From 8df1fc96176ba22965d880b4b0b61577eb76ada2 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 14:52:10 +0300 Subject: [PATCH 13/16] Add docs for selecting from redis synchronously --- docs/tutorials/synchronous/select.md | 122 ++++++++++++++++++ .../tutorials/synchronous/select-records.py | 52 ++++++++ mkdocs.yml | 1 + 3 files changed, 175 insertions(+) create mode 100644 docs/tutorials/synchronous/select.md create mode 100644 docs_src/tutorials/synchronous/select-records.py diff --git a/docs/tutorials/synchronous/select.md b/docs/tutorials/synchronous/select.md new file mode 100644 index 00000000..f8ca4883 --- /dev/null +++ b/docs/tutorials/synchronous/select.md @@ -0,0 +1,122 @@ +# Select + +Pydantic-redis can be used to retrieve model instances from redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="5-8 17" +{!../docs_src/tutorials/synchronous/select-records.py!} +``` + +## Select All Records + +To select all records for the given model in redis, call the model's `select` method without any arguments. + +```Python hl_lines="28" +{!../docs_src/tutorials/synchronous/select-records.py!} +``` + +## Select Some Fields for All Records + +To select some fields for all records for the given model in redis, pass the desired fields (`columns`) to the model's +`select` method. + +!!! info + This returns dictionaries instead of Model instances. + +```Python hl_lines="31" +{!../docs_src/tutorials/synchronous/select-records.py!} +``` + +## Select Some Records + +To select some records for the given model in redis, pass a list of the primary keys (`ids`) of the desired records to +the model's `select` method. + +```Python hl_lines="29" +{!../docs_src/tutorials/synchronous/select-records.py!} +``` + +## Select Some Fields for Some Records + +We can go further and limit the fields returned for the desired records. + +We pass the desired fields (`columns`) to the model's `select` method, together with the list of the primary keys +(`ids`) of the desired records. + +!!! info + This returns dictionaries instead of Model instances. + +```Python hl_lines="32-34" +{!../docs_src/tutorials/synchronous/select-records.py!} +``` + +## Select Records Page by Page + +In order to avoid overwhelming the server's memory resources, we can get the records one page at a time i.e. pagination. + +We do this by specifying the number of records per page (`limit`) and the number of records to skip (`skip`) +when calling the model's `select` method + +!!! info + Records are ordered by timestamp of their insert into redis. + + For batch inserts, the time difference is quite small but consistent. + +!!! tip + You don't have to pass the `skip` if you wish to get the first records. `skip` defaults to 0. + + `limit`, however is mandatory. + +!!! warning + When both `ids` and `limit` are supplied, pagination is ignored. + + It wouldn't make any sense otherwise. + +```Python hl_lines="36-39" +{!../docs_src/tutorials/synchronous/select-records.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +all: +[ Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Utah Blaine', author="Louis L'Amour"), + Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Pride and Prejudice', author='Jane Austen')] + +by id: +[ Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Pride and Prejudice', author='Jane Austen')] + +some fields for all: +[ {'author': 'Charles Dickens'}, + {'author': "Louis L'Amour"}, + {'author': 'Emily Bronte'}, + {'author': 'Jane Austen'}] + +some fields for given ids: +[{'author': 'Charles Dickens'}, {'author': 'Jane Austen'}] + +paginated; skip: 0, limit: 2: +[ Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Jane Eyre', author='Emily Bronte')] + +paginated returning some fields for each; skip: 2, limit: 2: +[{'author': 'Jane Austen'}, {'author': "Louis L'Amour"}] +``` +
\ No newline at end of file diff --git a/docs_src/tutorials/synchronous/select-records.py b/docs_src/tutorials/synchronous/select-records.py new file mode 100644 index 00000000..1f4bc5fc --- /dev/null +++ b/docs_src/tutorials/synchronous/select-records.py @@ -0,0 +1,52 @@ +import pprint +from pydantic_redis import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +if __name__ == "__main__": + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + Book.insert( + [ + Book(title="Oliver Twist", author="Charles Dickens"), + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + Book(title="Utah Blaine", author="Louis L'Amour"), + ] + ) + + select_all_response = Book.select() + select_by_id_response = Book.select(ids=["Oliver Twist", "Pride and Prejudice"]) + + select_some_fields_response = Book.select(columns=["author"]) + select_some_fields_for_ids_response = Book.select( + ids=["Oliver Twist", "Pride and Prejudice"], columns=["author"] + ) + + paginated_select_all_response = Book.select(skip=0, limit=2) + paginated_select_some_fields_response = Book.select( + columns=["author"], skip=2, limit=2 + ) + + print("all:") + pp.pprint(select_all_response) + print("\nby id:") + pp.pprint(select_by_id_response) + print("\nsome fields for all:") + pp.pprint(select_some_fields_response) + print("\nsome fields for given ids:") + pp.pprint(select_some_fields_for_ids_response) + print("\npaginated; skip: 0, limit: 2:") + pp.pprint(paginated_select_all_response) + print("\npaginated returning some fields for each; skip: 2, limit: 2:") + pp.pprint(paginated_select_some_fields_response) diff --git a/mkdocs.yml b/mkdocs.yml index 643a94f0..17f190e7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,6 +36,7 @@ nav: - tutorials/synchronous/insert.md - tutorials/synchronous/update.md - tutorials/synchronous/delete.md + - tutorials/synchronous/select.md - tutorials/synchronous/nested-models.md - tutorials/synchronous/list-of-nested-models.md - tutorials/synchronous/tuple-of-nested-models.md From 6804ca94b5660fdf750e8d8577b40deb5bc26756 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 15:39:48 +0300 Subject: [PATCH 14/16] Add docs for asynchronous API --- docs/tutorials/asynchronous/delete.md | 48 +++ docs/tutorials/asynchronous/insert.md | 87 ++++++ .../asynchronous/list-of-nested-models.md | 156 ++++++++++ docs/tutorials/asynchronous/models.md | 282 +++++------------- docs/tutorials/asynchronous/nested-models.md | 152 ++++++++++ docs/tutorials/asynchronous/select.md | 125 ++++++++ .../asynchronous/tuple-of-nested-models.md | 154 ++++++++++ docs/tutorials/asynchronous/update.md | 86 ++++++ docs_src/tutorials/asynchronous/delete.py | 42 +++ docs_src/tutorials/asynchronous/insert.py | 45 +++ .../asynchronous/list-of-nested-models.py | 83 ++++++ docs_src/tutorials/asynchronous/models.py | 46 +++ .../tutorials/asynchronous/nested-models.py | 72 +++++ .../tutorials/asynchronous/select-records.py | 60 ++++ .../asynchronous/tuple-of-nested-models.py | 65 ++++ docs_src/tutorials/asynchronous/update.py | 52 ++++ mkdocs.yml | 7 + 17 files changed, 1356 insertions(+), 206 deletions(-) create mode 100644 docs/tutorials/asynchronous/delete.md create mode 100644 docs/tutorials/asynchronous/insert.md create mode 100644 docs/tutorials/asynchronous/list-of-nested-models.md create mode 100644 docs/tutorials/asynchronous/nested-models.md create mode 100644 docs/tutorials/asynchronous/select.md create mode 100644 docs/tutorials/asynchronous/tuple-of-nested-models.md create mode 100644 docs/tutorials/asynchronous/update.md create mode 100644 docs_src/tutorials/asynchronous/delete.py create mode 100644 docs_src/tutorials/asynchronous/insert.py create mode 100644 docs_src/tutorials/asynchronous/list-of-nested-models.py create mode 100644 docs_src/tutorials/asynchronous/models.py create mode 100644 docs_src/tutorials/asynchronous/nested-models.py create mode 100644 docs_src/tutorials/asynchronous/select-records.py create mode 100644 docs_src/tutorials/asynchronous/tuple-of-nested-models.py create mode 100644 docs_src/tutorials/asynchronous/update.py diff --git a/docs/tutorials/asynchronous/delete.md b/docs/tutorials/asynchronous/delete.md new file mode 100644 index 00000000..088710dd --- /dev/null +++ b/docs/tutorials/asynchronous/delete.md @@ -0,0 +1,48 @@ +# Delete + +Pydantic-redis can be used to delete model instances from redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6-9 18" +{!../docs_src/tutorials/asynchronous/delete.py!} +``` + +## Delete Records + +To delete many records from redis, pass a list of primary keys (`ids`) of the records to the model's `delete` method. + +```Python hl_lines="30" +{!../docs_src/tutorials/asynchronous/delete.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +pre-delete: +[ Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Utah Blaine', author="Louis L'Amour"), + Book(title='Pride and Prejudice', author='Jane Austen')] + +post-delete: +[ Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Utah Blaine', author="Louis L'Amour")] +``` +
\ No newline at end of file diff --git a/docs/tutorials/asynchronous/insert.md b/docs/tutorials/asynchronous/insert.md new file mode 100644 index 00000000..c562feec --- /dev/null +++ b/docs/tutorials/asynchronous/insert.md @@ -0,0 +1,87 @@ +# Insert + +Pydantic-redis can be used to insert new model instances into redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6-9 18" +{!../docs_src/tutorials/asynchronous/insert.py!} +``` + +## Insert One Record + +To add a single record to the redis instance, pass that model's instance as first argument to the model's `insert` +method + +```Python hl_lines="20" +{!../docs_src/tutorials/asynchronous/insert.py!} +``` + +## Insert One Record With TTL + +To make the record added to redis temporary, add a `life_span_seconds` (Time To Live i.e. TTL) key-word argument +when calling the model's `insert` method. + +!!! info + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. + +```Python hl_lines="21-24" +{!../docs_src/tutorials/asynchronous/insert.py!} +``` + +## Insert Many Records + +To add many records to the redis instance, pass a list of that model's instances as first argument to the model's +`insert` method. + +!!! info + Adding many records at once is more performant than adding one record at a time repeatedly because less network requests + are made in the former. + +```Python hl_lines="25-30" +{!../docs_src/tutorials/asynchronous/insert.py!} +``` + +## Insert Many Records With TTL + +To add temporary records to redis, add a `life_span_seconds` (Time To Live i.e. TTL) argument +when calling the model's `insert` method. + +!!! info + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. + +```Python hl_lines="31-37" +{!../docs_src/tutorials/asynchronous/insert.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +[ Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Great Expectations', author='Charles Dickens'), + Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Pride and Prejudice', author='Jane Austen')] +``` +
\ No newline at end of file diff --git a/docs/tutorials/asynchronous/list-of-nested-models.md b/docs/tutorials/asynchronous/list-of-nested-models.md new file mode 100644 index 00000000..c451edaf --- /dev/null +++ b/docs/tutorials/asynchronous/list-of-nested-models.md @@ -0,0 +1,156 @@ +# Lists of Nested Models + +Sometimes, one might need to have models (schemas) that have lists of other models (schemas). + +An example is a `Folder` model that can have child `Folder`'s and `File`'s. + +This can easily be pulled off with pydantic-redis. + +## Import Pydantic-redis' `Model` + +First, import `pydantic-redis.asyncio`'s `Model` + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Create the Child Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="15-18" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Child Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +!!! example + In this case, there can be no two `File`'s with the same `path`. + +```Python hl_lines="16" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Create the Parent Model + +Next, declare another model as a class that inherits from `Model`. + +Use standard Python types for all attributes, as before. + +```Python hl_lines="21-25" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Add the Nested Model List to the Parent Model + +Annotate the field that is to hold the child model list with the List of child class. + +!!! example + In this case, the field `files` is annotated with `List[File]`. + + And the field `folders` is annotated with `"Folder"` class i.e. itself. + +```Python hl_lines="24-25" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Parent Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the parent Model. + +!!! example + In this case, there can be no two `Folder`'s with the same `path`. + +```Python hl_lines="22" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Register the Models in the Store + +Then, in order for the store to know the existence of each given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="36-37" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Use the Parent Model + +Then you can use the parent model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +!!! info + The child models will be automatically inserted, or updated if they already exist + +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. + +```Python hl_lines="39-61" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Use the Child Model Independently + +You can also use the child model independently. + +!!! info + Any mutation on the child model will also be reflected in the any parent model instances + fetched from redis after that mutation. + +```Python hl_lines="63-68" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Indirectly Update Child Model + +A child model can be indirectly updated via the parent model. + +Set the attribute containing the child model list with a list of instances of the child model + +If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis. + +```Python hl_lines="63-66" +{!../docs_src/tutorials/asynchronous/list-of-nested-models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +parent folder: +[ Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=), File(path='path/to/bar.jpg', type=)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=), File(path='path/to/foo.jpg', type=)], folders=[])])] + +files: +[ File(path='path/to/foo.txt', type=), + File(path='path/to/foo.jpg', type=), + File(path='path/to/bar.txt', type=), + File(path='path/to/bar.jpg', type=)] + +indirectly updated parent folder: +[ Folder(path='path/to/parent-folder', files=[File(path='path/to/bar.txt', type=), File(path='path/to/bar.jpg', type=)], folders=[Folder(path='path/to/child-folder', files=[File(path='path/to/foo.txt', type=)], folders=[])])] + +indirectly updated files: +[File(path='path/to/foo.txt', type=)] +``` +
diff --git a/docs/tutorials/asynchronous/models.md b/docs/tutorials/asynchronous/models.md index fdfbc118..0ae6519e 100644 --- a/docs/tutorials/asynchronous/models.md +++ b/docs/tutorials/asynchronous/models.md @@ -1,210 +1,80 @@ -# Getting Started +# Models -- Install the package +The very first thing you need to create for pydantic-redis are the models (or schemas) that +the data you are to save in redis is to be based on. -```bash -pip install pydantic-redis +These models are derived from [pydantic's](https://docs.pydantic.dev/) `BaseModel`. + +## Import Pydantic-redis' `Model` + +First, import pydantic-redis.asyncio's `Model` + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6" +{!../docs_src/tutorials/asynchronous/models.py!} ``` -- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis.asyncio` and use accordingly - -```python -import asyncio -from datetime import date -from typing import Tuple, List, Optional -from pydantic_redis.asyncio import RedisConfig, Model, Store - -# The features are exactly the same as the synchronous version, -# except for the ability to return coroutines when `insert`, -# `update`, `select` or `delete` are called. - - -class Author(Model): - """ - An Author model, just like a pydantic model with appropriate - type annotations - NOTE: The `_primary_key_field` is mandatory - """ - _primary_key_field: str = 'name' - name: str - active_years: Tuple[int, int] - - -class Book(Model): - """ - A Book model. - - Models can have the following field types - - The usual i.e. float, int, dict, list, date, str, dict, Optional etc - as long as they are serializable by orjson - - Nested models e.g. `author: Author` or `author: Optional[Author]` - - List of nested models e.g. `authors: List[Author]` - or `authors: Optional[List[Author]]` - - Tuples including nested models e.g. `access_log: Tuple[Author, date]` - or `access_log: Optional[Tuple[Author, date]]]` - - NOTE: 1. Any nested model whether plain or in a list or tuple will automatically - inserted into the redis store when the parent model is inserted. - e.g. a Book with an author field, when inserted, will also insert - the author. The author can then be queried directly if that's something - one wishes to do. - - 2. When a parent model is inserted with a nested model instance that - already exists, the older nested model instance is overwritten. - This is one way of updating nested models. - All parent models that contain that nested model instance will see the change. - """ - _primary_key_field: str = 'title' - title: str - author: Author - rating: float - published_on: date - tags: List[str] = [] - in_stock: bool = True - - -class Library(Model): - """ - A library model. - - It shows a number of complicated nested models. - - About Nested Model Performance - --- - To minimize the performance penalty for nesting models, - we use REDIS EVALSHA to eagerly load the nested models - before the response is returned to the client. - This ensures that only ONE network call is made every time. - """ - _primary_key_field: str = 'name' - name: str - address: str - books: List[Book] = None - lost: Optional[List[Book]] = None - popular: Optional[Tuple[Book, Book]] = None - new: Tuple[Book, Author, int] = None - - -async def run_async(): - """The async coroutine""" - # Create the store - store = Store( - name='some_name', - redis_config=RedisConfig(db=5, host='localhost', port=6379), - life_span_in_seconds=3600) - - # register your models. DON'T FORGET TO DO THIS. - store.register_model(Book) - store.register_model(Library) - store.register_model(Author) - - # sample authors. You can create as many as you wish anywhere in the code - authors = { - "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), - "jane": Author(name="Jane Austen", active_years=(1580, 1640)), - } - - # Sample books. - books = [ - Book( - title="Oliver Twist", - author=authors["charles"], - published_on=date(year=1215, month=4, day=4), - in_stock=False, - rating=2, - tags=["Classic"], - ), - Book( - title="Great Expectations", - author=authors["charles"], - published_on=date(year=1220, month=4, day=4), - rating=5, - tags=["Classic"], - ), - Book( - title="Jane Eyre", - author=authors["charles"], - published_on=date(year=1225, month=6, day=4), - in_stock=False, - rating=3.4, - tags=["Classic", "Romance"], - ), - Book( - title="Wuthering Heights", - author=authors["jane"], - published_on=date(year=1600, month=4, day=4), - rating=4.0, - tags=["Classic", "Romance"], - ), - ] - - # Some library objects - libraries = [ - Library( - name="The Grand Library", - address="Kinogozi, Hoima, Uganda", - lost=[books[1]], - ), - Library( - name="Christian Library", - address="Buhimba, Hoima, Uganda", - new=(books[0], authors["jane"], 30), - ) - ] - - # Insert Many. You can given them a TTL (life_span_seconds). - await Book.insert(books, life_span_seconds=3600) - await Library.insert(libraries) - - # Insert One. You can also given it a TTL (life_span_seconds). - await Author.insert(Author(name="Jack Myers", active_years=(1240, 1300))) - - # Update One. You can also given it a TTL (life_span_seconds). - await Book.update(_id="Oliver Twist", data={"author": authors["jane"]}) - - # Update nested model indirectly - updated_jane = Author(**authors["jane"].dict()) - updated_jane.active_years = (1999, 2008) - await Book.update(_id="Oliver Twist", data={"author": updated_jane}) - - # Query the data - # Get all, with all fields shown. Data returned is a list of models instances. - all_books = await Book.select() - print(all_books) - # Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), - # in_stock=False), Book(...] - - # or paginate i.e. skip some books and return only upto a given number - paginated_books = await Book.select(skip=2, limit=2) - print(paginated_books) - - # Get some, with all fields shown. Data returned is a list of models instances. - some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"]) - print(some_books) - - # Note: Pagination does not work when ids are provided i.e. - assert some_books == await Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10) - - # Get all, with only a few fields shown. Data returned is a list of dictionaries. - books_with_few_fields = await Book.select(columns=["author", "in_stock"]) - print(books_with_few_fields) - # Prints [{"author": "'Charles Dickens", "in_stock": "True"},...] - - # or paginate i.e. skip some books and return only upto a given number - paginated_books_with_few_fields = await Book.select(columns=["author", "in_stock"], skip=2, limit=2) - print(paginated_books_with_few_fields) - - # Get some, with only some fields shown. Data returned is a list of dictionaries. - some_books_with_few_fields = await Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"]) - print(some_books_with_few_fields) - - # Query the nested models directly. - some_authors = await Author.select(ids=["Jane Austen"]) - print(some_authors) - - # Delete any number of items - await Library.delete(ids=["The Grand Library"]) - - -asyncio.run(run_async()) -``` \ No newline at end of file +## Create the Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="9-16" +{!../docs_src/tutorials/asynchronous/models.py!} +``` + +## Specify the `_primary_key_field` Attribute + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +!!! example + In this case, there can be no two books with the same `title`. + +```Python hl_lines="10" +{!../docs_src/tutorials/asynchronous/models.py!} +``` + +## Register the Model in the Store + +Then, in order for the store to know the existence of the given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="27" +{!../docs_src/tutorials/asynchronous/models.py!} +``` + +## Use the Model + +Then you can use the model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. + +```Python hl_lines="29-40" +{!../docs_src/tutorials/asynchronous/models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +[ Book(title='Oliver Twist', author='Charles Dickens', rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)] +``` +
\ No newline at end of file diff --git a/docs/tutorials/asynchronous/nested-models.md b/docs/tutorials/asynchronous/nested-models.md new file mode 100644 index 00000000..9955beb6 --- /dev/null +++ b/docs/tutorials/asynchronous/nested-models.md @@ -0,0 +1,152 @@ +# Nested Models + +The very first thing you need to create for pydantic-redis are the models (or schemas) that +the data you are to save in redis is to be based on. + +It is possible to refer one model in another model in a parent-child relationship. + +## Import Pydantic-redis' `Model` + +First, import `pydantic-redis.asyncio`'s `Model`. + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Create the Child Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="9-12" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Set the `_primary_key_field` of the Child Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +!!! example + In this case, there can be no two authors with the same `name`. + +```Python hl_lines="10" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Create the Parent Model + +Next, declare another model as a class that inherits from `Model`. + +Use standard Python types for all attributes, as before. + +```Python hl_lines="15-22" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Add the Nested Model to the Parent Model + +Annotate the field that is to hold the child model with the child class. + +!!! example + In this case, the field `author` is annotated with `Author` class. + +```Python hl_lines="18" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Set the `_primary_key_field` of the Parent Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the parent Model. + +!!! example + In this case, there can be no two books with the same `title`. + +```Python hl_lines="16" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Register the Models in the Store + +Then, in order for the store to know the existence of each given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="33-34" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Use the Parent Model + +Then you can use the parent model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +!!! note + The child model will be automatically inserted, or updated if it already exists + +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. + +```Python hl_lines="36-48" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Use the Child Model Independently + +You can also use the child model independently. + +!!! info + Any mutation on the child model will also be reflected in the any parent model instances + fetched from redis after that mutation. + +```Python hl_lines="50-51" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Indirectly Update Child Model + +A child model can be indirectly updated via the parent model. + +Set the attribute containing the child model with an instance of the child model + +!!! note + The new instance of the child model should have the **SAME** primary key as the original + child model. + +```Python hl_lines="53-57" +{!../docs_src/tutorials/asynchronous/nested-models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +book: +[ Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1999, 2007)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)] + +author: +[Author(name='Charles Dickens', active_years=(1999, 2007))] + +indirectly updated book: +[ Book(title='Oliver Twist', author=Author(name='Charles Dickens', active_years=(1227, 1277)), rating=2.0, published_on=datetime.date(1215, 4, 4), tags=['Classic'], in_stock=False)] + +indirectly updated author: +[Author(name='Charles Dickens', active_years=(1969, 1999))] +``` +
diff --git a/docs/tutorials/asynchronous/select.md b/docs/tutorials/asynchronous/select.md new file mode 100644 index 00000000..7d5c7d58 --- /dev/null +++ b/docs/tutorials/asynchronous/select.md @@ -0,0 +1,125 @@ +# Select + +Pydantic-redis can be used to retrieve model instances from redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store`. + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6-9 18" +{!../docs_src/tutorials/asynchronous/select-records.py!} +``` + +## Select All Records + +To select all records for the given model in redis, call the model's `select` method without any arguments. + +```Python hl_lines="29" +{!../docs_src/tutorials/asynchronous/select-records.py!} +``` + +## Select Some Fields for All Records + +To select some fields for all records for the given model in redis, pass the desired fields (`columns`) to the model's +`select` method. + +!!! info + This returns dictionaries instead of Model instances. + +```Python hl_lines="34" +{!../docs_src/tutorials/asynchronous/select-records.py!} +``` + +## Select Some Records + +To select some records for the given model in redis, pass a list of the primary keys (`ids`) of the desired records to +the model's `select` method. + +```Python hl_lines="30-32" +{!../docs_src/tutorials/asynchronous/select-records.py!} +``` + +## Select Some Fields for Some Records + +We can go further and limit the fields returned for the desired records. + +We pass the desired fields (`columns`) to the model's `select` method, together with the list of the primary keys +(`ids`) of the desired records. + +!!! info + This returns dictionaries instead of Model instances. + +```Python hl_lines="35-37" +{!../docs_src/tutorials/asynchronous/select-records.py!} +``` + +## Select Records Page by Page + +In order to avoid overwhelming the server's memory resources, we can get the records one page at a time i.e. pagination. + +We do this by specifying the number of records per page (`limit`) and the number of records to skip (`skip`) +when calling the model's `select` method + +!!! info + Records are ordered by timestamp of their insert into redis. + + For batch inserts, the time difference is quite small but consistent. + +!!! tip + You don't have to pass the `skip` if you wish to get the first records. `skip` defaults to 0. + + `limit`, however is mandatory. + +!!! warning + When both `ids` and `limit` are supplied, pagination is ignored. + + It wouldn't make any sense otherwise. + +```Python hl_lines="39-42" +{!../docs_src/tutorials/asynchronous/select-records.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +all: +[ Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Utah Blaine', author="Louis L'Amour"), + Book(title='Jane Eyre', author='Emily Bronte'), + Book(title='Pride and Prejudice', author='Jane Austen')] + +by id: +[ Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Pride and Prejudice', author='Jane Austen')] + +some fields for all: +[ {'author': 'Charles Dickens'}, + {'author': "Louis L'Amour"}, + {'author': 'Emily Bronte'}, + {'author': 'Jane Austen'}] + +some fields for given ids: +[{'author': 'Charles Dickens'}, {'author': 'Jane Austen'}] + +paginated; skip: 0, limit: 2: +[ Book(title='Oliver Twist', author='Charles Dickens'), + Book(title='Jane Eyre', author='Emily Bronte')] + +paginated returning some fields for each; skip: 2, limit: 2: +[{'author': 'Jane Austen'}, {'author': "Louis L'Amour"}] +``` +
\ No newline at end of file diff --git a/docs/tutorials/asynchronous/tuple-of-nested-models.md b/docs/tutorials/asynchronous/tuple-of-nested-models.md new file mode 100644 index 00000000..27bef053 --- /dev/null +++ b/docs/tutorials/asynchronous/tuple-of-nested-models.md @@ -0,0 +1,154 @@ +# Tuples of Nested Models + +Sometimes, one might need to have models (schemas) that have tuples of other models (schemas). + +An example is a `ScoreBoard` model that can have Tuples of player name and `Scores`'. + +This can easily be pulled off with pydantic-redis. + +## Import Pydantic-redis' `Model` + +First, import `pydantic-redis.asyncio`'s `Model`. + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="4" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Create the Child Model + +Next, declare a new model as a class that inherits from `Model`. + +Use standard Python types for all attributes. + +```Python hl_lines="7-10" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Child Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the Model. + +!!! example + In this case, there can be no two `Score`'s with the same `id`. + +```Python hl_lines="8" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Create the Parent Model + +Next, declare another model as a class that inherits from `Model`. + +Use standard Python types for all attributes, as before. + +```Python hl_lines="13-16" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Add the Nested Model Tuple to the Parent Model + +Annotate the field that is to hold the tuple of child models with the Tuple of child class. + +!!! example + In this case, the field `scores` is annotated with `Tuple[str, Score]` class. + +!!! info + The `str` is the player's name. + +```Python hl_lines="16" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Set the `_primary_key_field` of the Parent Model + +Set the `_primary_key_field` attribute to the name of the attribute +that is to act as a unique identifier for each instance of the parent Model. + +!!! example + In this case, there can be no two `ScoreBoard`'s with the same `id`. + +```Python hl_lines="14" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Register the Models in the Store + +Then, in order for the store to know the existence of each given model, +register it using the `register_model` method of `Store` + +```Python hl_lines="23-24" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Use the Parent Model + +Then you can use the parent model class to: + +- `insert` into the store +- `update` an instance of the model +- `delete` from store +- `select` from store + +!!! info + The child models will be automatically inserted, or updated if they already exist + +!!! info + The store is connected to the Redis instance, so any changes you make will + reflect in redis itself. + +```Python hl_lines="26-36" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Use the Child Model Independently + +You can also use the child model independently. + +!!! info + Any mutation on the child model will also be reflected in the any parent model instances + fetched from redis after that mutation. + +```Python hl_lines="38-39" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Indirectly Update Child Model + +A child model can be indirectly updated via the parent model. + +Set the attribute containing the child model tuple with a tuple of instances of the child model + +If there is any new instance of the child model that has a pre-existing primary key, it will be updated in redis. + +```Python hl_lines="41-50" +{!../docs_src/tutorials/asynchronous/tuple-of-nested-models.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +score board: +[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=50)))] + +scores: +[Score(id='some id', total=50)] + +indirectly updated score board: +[ScoreBoard(id='test', scores=('mark', Score(id='some id', total=78)))] + +indirectly updated score: +[Score(id='some id', total=60)] +``` +
diff --git a/docs/tutorials/asynchronous/update.md b/docs/tutorials/asynchronous/update.md new file mode 100644 index 00000000..b004c459 --- /dev/null +++ b/docs/tutorials/asynchronous/update.md @@ -0,0 +1,86 @@ +# Update + +Pydantic-redis can be used to update model instances in redis. + +## Create and register the Model + +A model is a class that inherits from `Model` with its `_primary_key_field` attribute set. + +In order for the store to know the existence of the given model, +register it using the `register_model` method of `Store`. + +!!! warning + The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis` + +```Python hl_lines="6-9 18" +{!../docs_src/tutorials/asynchronous/update.py!} +``` + +## Update One Record + +To update a single record in redis, pass the primary key (`_id`) of that record and the new changes to the model's `update` +method + +```Python hl_lines="27" +{!../docs_src/tutorials/asynchronous/update.py!} +``` + +## Update One Record With TTL + +To update the record's time-to-live (TTL) also, pass the `life_span_seconds` argument to the model's `update` method. + +!!! info + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + The `life_span_in_seconds` in both cases is `None` by default. This means records never expire by default. + +```Python hl_lines="28-30" +{!../docs_src/tutorials/asynchronous/update.py!} +``` + +## Update/Upsert Many Records + +To update many records in redis, pass a list of that model's instances as first argument to the model's +`insert` method. + +Technically, this will insert any records that don't exist and overwrite any that exist already. + +!!! info + Updating many records at once is more performant than adding one record at a time repeatedly because less network requests + are made in the former. + +!!! warning + Calling `insert` always overwrites the time-to-live of the records updated. + + When the `life_span_seconds` argument is not specified, the `life_span_in_seconds` passed to the store during + initialization is used. + + By default `life_span_seconds` is `None` i.e. the time-to-live is removed and the updated records never expire. + +```Python hl_lines="33-40" +{!../docs_src/tutorials/asynchronous/update.py!} +``` + +## Run the App + +Running the above code in a file `main.py` would produce: + +!!! tip + Probably [FLUSHALL](https://redis.io/commands/flushall/) redis first + +
+ +```console +$ python main.py +single update: +[ Book(title='Jane Eyre', author='Daniel McKenzie'), + Book(title='Oliver Twist', author='Charlie Ickens'), + Book(title='Pride and Prejudice', author='Jane Austen')] + +multi update: +[ Book(title='Jane Eyre', author='Emiliano Bronte'), + Book(title='Oliver Twist', author='Chuck Dickens'), + Book(title='Pride and Prejudice', author='Janey Austen')] +``` +
\ No newline at end of file diff --git a/docs_src/tutorials/asynchronous/delete.py b/docs_src/tutorials/asynchronous/delete.py new file mode 100644 index 00000000..2ffef33e --- /dev/null +++ b/docs_src/tutorials/asynchronous/delete.py @@ -0,0 +1,42 @@ +import asyncio +import pprint +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + await Book.insert( + [ + Book(title="Oliver Twist", author="Charles Dickens"), + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + Book(title="Utah Blaine", author="Louis L'Amour"), + ] + ) + pre_delete_response = await Book.select() + + await Book.delete(ids=["Oliver Twist", "Pride and Prejudice"]) + post_delete_response = await Book.select() + + print("pre-delete:") + pp.pprint(pre_delete_response) + + print("\npost-delete:") + pp.pprint(post_delete_response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/insert.py b/docs_src/tutorials/asynchronous/insert.py new file mode 100644 index 00000000..8690b2fd --- /dev/null +++ b/docs_src/tutorials/asynchronous/insert.py @@ -0,0 +1,45 @@ +import asyncio +import pprint +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + await Book.insert(Book(title="Oliver Twist", author="Charles Dickens")) + await Book.insert( + Book(title="Great Expectations", author="Charles Dickens"), + life_span_seconds=1800, + ) + await Book.insert( + [ + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + ] + ) + await Book.insert( + [ + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + ], + life_span_seconds=3600, + ) + + response = await Book.select() + pp.pprint(response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/list-of-nested-models.py b/docs_src/tutorials/asynchronous/list-of-nested-models.py new file mode 100644 index 00000000..e5c53497 --- /dev/null +++ b/docs_src/tutorials/asynchronous/list-of-nested-models.py @@ -0,0 +1,83 @@ +import asyncio +import pprint +from enum import Enum +from typing import List + +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class FileType(Enum): + TEXT = "text" + IMAGE = "image" + EXEC = "executable" + + +class File(Model): + _primary_key_field: str = "path" + path: str + type: FileType + + +class Folder(Model): + _primary_key_field: str = "path" + path: str + files: List[File] = [] + folders: List["Folder"] = [] + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", + redis_config=RedisConfig(db=5, host="localhost", port=6379), + life_span_in_seconds=3600, + ) + + store.register_model(File) + store.register_model(Folder) + + child_folder = Folder( + path="path/to/child-folder", + files=[ + File(path="path/to/foo.txt", type=FileType.TEXT), + File(path="path/to/foo.jpg", type=FileType.IMAGE), + ], + ) + + await Folder.insert( + Folder( + path="path/to/parent-folder", + files=[ + File(path="path/to/bar.txt", type=FileType.TEXT), + File(path="path/to/bar.jpg", type=FileType.IMAGE), + ], + folders=[child_folder], + ) + ) + + parent_folder_response = await Folder.select(ids=["path/to/parent-folder"]) + files_response = await File.select( + ids=["path/to/foo.txt", "path/to/foo.jpg", "path/to/bar.txt", "path/to/bar.jpg"] + ) + + await Folder.update( + _id="path/to/child-folder", + data={"files": [File(path="path/to/foo.txt", type=FileType.EXEC)]}, + ) + updated_parent_folder_response = await Folder.select(ids=["path/to/parent-folder"]) + updated_file_response = await File.select(ids=["path/to/foo.txt"]) + + print("parent folder:") + pp.pprint(parent_folder_response) + print("\nfiles:") + pp.pprint(files_response) + + print("\nindirectly updated parent folder:") + pp.pprint(updated_parent_folder_response) + print("\nindirectly updated files:") + pp.pprint(updated_file_response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/models.py b/docs_src/tutorials/asynchronous/models.py new file mode 100644 index 00000000..6113f3a7 --- /dev/null +++ b/docs_src/tutorials/asynchronous/models.py @@ -0,0 +1,46 @@ +import asyncio +import pprint +from datetime import date +from typing import List + +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", + redis_config=RedisConfig(db=5, host="localhost", port=6379), + life_span_in_seconds=3600, + ) + + store.register_model(Book) + + await Book.insert( + Book( + title="Oliver Twist", + author="Charles Dickens", + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ) + ) + + response = await Book.select(ids=["Oliver Twist"]) + pp.pprint(response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/nested-models.py b/docs_src/tutorials/asynchronous/nested-models.py new file mode 100644 index 00000000..c417fee8 --- /dev/null +++ b/docs_src/tutorials/asynchronous/nested-models.py @@ -0,0 +1,72 @@ +import asyncio +import pprint +from datetime import date +from typing import List, Tuple + +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class Author(Model): + _primary_key_field: str = "name" + name: str + active_years: Tuple[int, int] + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: Author + rating: float + published_on: date + tags: List[str] = [] + in_stock: bool = True + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", + redis_config=RedisConfig(db=5, host="localhost", port=6379), + life_span_in_seconds=3600, + ) + + store.register_model(Author) + store.register_model(Book) + + await Book.insert( + Book( + title="Oliver Twist", + author=Author(name="Charles Dickens", active_years=(1999, 2007)), + published_on=date(year=1215, month=4, day=4), + in_stock=False, + rating=2, + tags=["Classic"], + ) + ) + + book_response = await Book.select(ids=["Oliver Twist"]) + author_response = await Author.select(ids=["Charles Dickens"]) + + await Author.update(_id="Charles Dickens", data={"active_years": (1227, 1277)}) + updated_book_response = await Book.select(ids=["Oliver Twist"]) + + await Book.update( + _id="Oliver Twist", + data={"author": Author(name="Charles Dickens", active_years=(1969, 1999))}, + ) + updated_author_response = await Author.select(ids=["Charles Dickens"]) + + print("book:") + pp.pprint(book_response) + print("\nauthor:") + pp.pprint(author_response) + + print("\nindirectly updated book:") + pp.pprint(updated_book_response) + print("\nindirectly updated author:") + pp.pprint(updated_author_response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/select-records.py b/docs_src/tutorials/asynchronous/select-records.py new file mode 100644 index 00000000..585b09c4 --- /dev/null +++ b/docs_src/tutorials/asynchronous/select-records.py @@ -0,0 +1,60 @@ +import asyncio +import pprint +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + await Book.insert( + [ + Book(title="Oliver Twist", author="Charles Dickens"), + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + Book(title="Utah Blaine", author="Louis L'Amour"), + ] + ) + + select_all_response = await Book.select() + select_by_id_response = await Book.select( + ids=["Oliver Twist", "Pride and Prejudice"] + ) + + select_some_fields_response = await Book.select(columns=["author"]) + select_some_fields_for_ids_response = await Book.select( + ids=["Oliver Twist", "Pride and Prejudice"], columns=["author"] + ) + + paginated_select_all_response = await Book.select(skip=0, limit=2) + paginated_select_some_fields_response = await Book.select( + columns=["author"], skip=2, limit=2 + ) + + print("all:") + pp.pprint(select_all_response) + print("\nby id:") + pp.pprint(select_by_id_response) + print("\nsome fields for all:") + pp.pprint(select_some_fields_response) + print("\nsome fields for given ids:") + pp.pprint(select_some_fields_for_ids_response) + print("\npaginated; skip: 0, limit: 2:") + pp.pprint(paginated_select_all_response) + print("\npaginated returning some fields for each; skip: 2, limit: 2:") + pp.pprint(paginated_select_some_fields_response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/tuple-of-nested-models.py b/docs_src/tutorials/asynchronous/tuple-of-nested-models.py new file mode 100644 index 00000000..a9486e6c --- /dev/null +++ b/docs_src/tutorials/asynchronous/tuple-of-nested-models.py @@ -0,0 +1,65 @@ +import asyncio +import pprint +from typing import Tuple +from pydantic_redis.asyncio import RedisConfig, Model, Store + + +class Score(Model): + _primary_key_field: str = "id" + id: str + total: int + + +class ScoreBoard(Model): + _primary_key_field: str = "id" + id: str + scores: Tuple[str, Score] + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store(name="test", redis_config=RedisConfig()) + + store.register_model(Score) + store.register_model(ScoreBoard) + + await ScoreBoard.insert( + data=ScoreBoard( + id="test", + scores=( + "mark", + Score(id="some id", total=50), + ), + ) + ) + score_board_response = await ScoreBoard.select(ids=["test"]) + scores_response = await Score.select(ids=["some id"]) + + await Score.update(_id="some id", data={"total": 78}) + updated_score_board_response = await ScoreBoard.select(ids=["test"]) + + await ScoreBoard.update( + _id="test", + data={ + "scores": ( + "tom", + Score(id="some id", total=60), + ) + }, + ) + updated_score_response = await Score.select(ids=["some id"]) + + print("score board:") + pp.pprint(score_board_response) + print("\nscores:") + pp.pprint(scores_response) + + print("\nindirectly updated score board:") + pp.pprint(updated_score_board_response) + print("\nindirectly updated score:") + pp.pprint(updated_score_response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/docs_src/tutorials/asynchronous/update.py b/docs_src/tutorials/asynchronous/update.py new file mode 100644 index 00000000..1cc2cc5e --- /dev/null +++ b/docs_src/tutorials/asynchronous/update.py @@ -0,0 +1,52 @@ +import asyncio +import pprint +from pydantic_redis.asyncio import Model, Store, RedisConfig + + +class Book(Model): + _primary_key_field: str = "title" + title: str + author: str + + +async def main(): + pp = pprint.PrettyPrinter(indent=4) + store = Store( + name="some_name", redis_config=RedisConfig(), life_span_in_seconds=86400 + ) + + store.register_model(Book) + + await Book.insert( + [ + Book(title="Oliver Twist", author="Charles Dickens"), + Book(title="Jane Eyre", author="Emily Bronte"), + Book(title="Pride and Prejudice", author="Jane Austen"), + ] + ) + await Book.update(_id="Oliver Twist", data={"author": "Charlie Ickens"}) + await Book.update( + _id="Jane Eyre", data={"author": "Daniel McKenzie"}, life_span_seconds=1800 + ) + single_update_response = await Book.select() + + await Book.insert( + [ + Book(title="Oliver Twist", author="Chuck Dickens"), + Book(title="Jane Eyre", author="Emiliano Bronte"), + Book(title="Pride and Prejudice", author="Janey Austen"), + ], + life_span_seconds=3600, + ) + multi_update_response = await Book.select() + + print("single update:") + pp.pprint(single_update_response) + + print("\nmulti update:") + pp.pprint(multi_update_response) + + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/mkdocs.yml b/mkdocs.yml index 17f190e7..3f856184 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -42,6 +42,13 @@ nav: - tutorials/synchronous/tuple-of-nested-models.md - Asynchronous API: - tutorials/asynchronous/models.md + - tutorials/asynchronous/insert.md + - tutorials/asynchronous/update.md + - tutorials/asynchronous/delete.md + - tutorials/asynchronous/select.md + - tutorials/asynchronous/nested-models.md + - tutorials/asynchronous/list-of-nested-models.md + - tutorials/asynchronous/tuple-of-nested-models.md - 'Explanation': - explanation/why-use-orms.md - reference.md From c99b761609b0e2b43404d262e3a2966650ce28b1 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 15:59:24 +0300 Subject: [PATCH 15/16] Add link to documentations site link to README.md --- CONTRIBUTING.md | 44 +++++ README.md | 468 +----------------------------------------------- 2 files changed, 51 insertions(+), 461 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5126ea4..44892029 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,6 +60,50 @@ People *love* thorough bug reports. I'm not even kidding. By contributing, you agree that your contributions will be licensed under its MIT License. +## How to test + +- Clone the repo and enter its root folder + + ```bash + git clone https://github.com/sopherapps/pydantic-redis.git && cd pydantic-redis + ``` + +- Create a virtual environment and activate it + + ```bash + virtualenv -p /usr/bin/python3.6 env && source env/bin/activate + ``` + +- Install the dependencies + + ```bash + pip install -r requirements.txt + ``` + +- Run the pre-commit installation + + ```bash + pre-commit install + ``` + +- Run the tests command + + ```bash + pytest --benchmark-disable + ``` + +- Run benchmarks + + ```bash + pytest --benchmark-compare --benchmark-autosave + ``` + +- Or run to get benchmarks summary + + ```shell + pytest test/test_benchmarks.py --benchmark-columns=mean,min,max --benchmark-name=short + ``` + ## References This document was adapted from [a gist by Brian A. Danielak](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62) which diff --git a/README.md b/README.md index ab2ecbf5..a58e0b06 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ A simple declarative ORM for Redis -## Main Dependencies +--- -- [Python +3.6](https://www.python.org) -- [redis](https://pypi.org/project/redis/) -- [pydantic](https://github.com/samuelcolvin/pydantic/) +**Documentation:** [https://sopherapps.github.io/pydantic-redis](https://sopherapps.github.io/pydantic-redis) -## Most Notable Features +**Source Code:** [https://github.com/sopherapps/pydantic-redis](https://github.com/sopherapps/pydantic-redis) + +--- + +Most Notable Features are: - Define business domain objects as [pydantic](https://github.com/samuelcolvin/pydantic/) and automatically get ability to save them as is in [redis](https://pypi.org/project/redis/) with an intuitive API of `insert`, `update`, `delete`, @@ -20,462 +22,6 @@ A simple declarative ORM for Redis them when queried again from redis. - Both synchronous and asynchronous APIs available. -## Getting Started (Synchronous Version) - -- Install the package - - ```bash - pip install pydantic-redis - ``` - -- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis` and use accordingly - -```python -from datetime import date -from typing import Tuple, List, Optional -from pydantic_redis import RedisConfig, Model, Store - - -class Author(Model): - """ - An Author model, just like a pydantic model with appropriate - type annotations - NOTE: The `_primary_key_field` is mandatory - """ - _primary_key_field: str = 'name' - name: str - active_years: Tuple[int, int] - - -class Book(Model): - """ - A Book model. - - Models can have the following field types - - The usual i.e. float, int, dict, list, date, str, dict, Optional etc - as long as they are serializable by orjson - - Nested models e.g. `author: Author` or `author: Optional[Author]` - - List of nested models e.g. `authors: List[Author]` - or `authors: Optional[List[Author]]` - - Tuples including nested models e.g. `access_log: Tuple[Author, date]` - or `access_log: Optional[Tuple[Author, date]]]` - - NOTE: 1. Any nested model whether plain or in a list or tuple will automatically - inserted into the redis store when the parent model is inserted. - e.g. a Book with an author field, when inserted, will also insert - the author. The author can then be queried directly if that's something - one wishes to do. - - 2. When a parent model is inserted with a nested model instance that - already exists, the older nested model instance is overwritten. - This is one way of updating nested models. - All parent models that contain that nested model instance will see the change. - """ - _primary_key_field: str = 'title' - title: str - author: Author - rating: float - published_on: date - tags: List[str] = [] - in_stock: bool = True - - -class Library(Model): - """ - A library model. - - It shows a number of complicated nested models. - - About Nested Model Performance - --- - To minimize the performance penalty for nesting models, - we use REDIS EVALSHA to eagerly load the nested models - before the response is returned to the client. - This ensures that only ONE network call is made every time. - """ - _primary_key_field: str = 'name' - name: str - address: str - books: List[Book] = None - lost: Optional[List[Book]] = None - popular: Optional[Tuple[Book, Book]] = None - new: Tuple[Book, Author, int] = None - - -# Create the store -store = Store( - name='some_name', - redis_config=RedisConfig(db=5, host='localhost', port=6379), - life_span_in_seconds=3600) - -# register your models. DON'T FORGET TO DO THIS. -store.register_model(Book) -store.register_model(Library) -store.register_model(Author) - -# sample authors. You can create as many as you wish anywhere in the code -authors = { - "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), - "jane": Author(name="Jane Austen", active_years=(1580, 1640)), -} - -# Sample books. -books = [ - Book( - title="Oliver Twist", - author=authors["charles"], - published_on=date(year=1215, month=4, day=4), - in_stock=False, - rating=2, - tags=["Classic"], - ), - Book( - title="Great Expectations", - author=authors["charles"], - published_on=date(year=1220, month=4, day=4), - rating=5, - tags=["Classic"], - ), - Book( - title="Jane Eyre", - author=authors["charles"], - published_on=date(year=1225, month=6, day=4), - in_stock=False, - rating=3.4, - tags=["Classic", "Romance"], - ), - Book( - title="Wuthering Heights", - author=authors["jane"], - published_on=date(year=1600, month=4, day=4), - rating=4.0, - tags=["Classic", "Romance"], - ), -] - -# Some library objects -libraries = [ - Library( - name="The Grand Library", - address="Kinogozi, Hoima, Uganda", - lost=[books[1]], - ), - Library( - name="Christian Library", - address="Buhimba, Hoima, Uganda", - new=(books[0], authors["jane"], 30), - ) -] - -# Insert Many. You can given them a TTL (life_span_seconds). -Book.insert(books, life_span_seconds=3600) -Library.insert(libraries) - -# Insert One. You can also given it a TTL (life_span_seconds). -Author.insert(Author(name="Jack Myers", active_years=(1240, 1300))) - -# Update One. You can also given it a TTL (life_span_seconds). -Book.update(_id="Oliver Twist", data={"author": authors["jane"]}) - -# Update nested model indirectly -updated_jane = Author(**authors["jane"].dict()) -updated_jane.active_years = (1999, 2008) -Book.update(_id="Oliver Twist", data={"author": updated_jane}) - -# Query the data -# Get all, with all fields shown. Data returned is a list of models instances. -all_books = Book.select() -print(all_books) -# Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), -# in_stock=False), Book(...] - -# or paginate i.e. skip some books and return only upto a given number -paginated_books = Book.select(skip=2, limit=2) -print(paginated_books) - -# Get some, with all fields shown. Data returned is a list of models instances. -some_books = Book.select(ids=["Oliver Twist", "Jane Eyre"]) -print(some_books) - -# Note: Pagination does not work when ids are provided i.e. -assert some_books == Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10) - -# Get all, with only a few fields shown. Data returned is a list of dictionaries. -books_with_few_fields = Book.select(columns=["author", "in_stock"]) -print(books_with_few_fields) -# Prints [{"author": "'Charles Dickens", "in_stock": "True"},...] - -# or paginate i.e. skip some books and return only upto a given number -paginated_books_with_few_fields = Book.select(columns=["author", "in_stock"], skip=2, limit=2) -print(paginated_books_with_few_fields) - -# Get some, with only some fields shown. Data returned is a list of dictionaries. -some_books_with_few_fields = Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"]) -print(some_books_with_few_fields) - -# Query the nested models directly. -some_authors = Author.select(ids=["Jane Austen"]) -print(some_authors) - -# Delete any number of items -Library.delete(ids=["The Grand Library"]) -``` - -## Getting Started (Asynchronous Version) - -- Install the package - - ```bash - pip install pydantic-redis - ``` - -- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis.asyncio` and use accordingly - -```python -import asyncio -from datetime import date -from typing import Tuple, List, Optional -from pydantic_redis.asyncio import RedisConfig, Model, Store - -# The features are exactly the same as the synchronous version, -# except for the ability to return coroutines when `insert`, -# `update`, `select` or `delete` are called. - - -class Author(Model): - """ - An Author model, just like a pydantic model with appropriate - type annotations - NOTE: The `_primary_key_field` is mandatory - """ - _primary_key_field: str = 'name' - name: str - active_years: Tuple[int, int] - - -class Book(Model): - """ - A Book model. - - Models can have the following field types - - The usual i.e. float, int, dict, list, date, str, dict, Optional etc - as long as they are serializable by orjson - - Nested models e.g. `author: Author` or `author: Optional[Author]` - - List of nested models e.g. `authors: List[Author]` - or `authors: Optional[List[Author]]` - - Tuples including nested models e.g. `access_log: Tuple[Author, date]` - or `access_log: Optional[Tuple[Author, date]]]` - - NOTE: 1. Any nested model whether plain or in a list or tuple will automatically - inserted into the redis store when the parent model is inserted. - e.g. a Book with an author field, when inserted, will also insert - the author. The author can then be queried directly if that's something - one wishes to do. - - 2. When a parent model is inserted with a nested model instance that - already exists, the older nested model instance is overwritten. - This is one way of updating nested models. - All parent models that contain that nested model instance will see the change. - """ - _primary_key_field: str = 'title' - title: str - author: Author - rating: float - published_on: date - tags: List[str] = [] - in_stock: bool = True - - -class Library(Model): - """ - A library model. - - It shows a number of complicated nested models. - - About Nested Model Performance - --- - To minimize the performance penalty for nesting models, - we use REDIS EVALSHA to eagerly load the nested models - before the response is returned to the client. - This ensures that only ONE network call is made every time. - """ - _primary_key_field: str = 'name' - name: str - address: str - books: List[Book] = None - lost: Optional[List[Book]] = None - popular: Optional[Tuple[Book, Book]] = None - new: Tuple[Book, Author, int] = None - - -async def run_async(): - """The async coroutine""" - # Create the store - store = Store( - name='some_name', - redis_config=RedisConfig(db=5, host='localhost', port=6379), - life_span_in_seconds=3600) - - # register your models. DON'T FORGET TO DO THIS. - store.register_model(Book) - store.register_model(Library) - store.register_model(Author) - - # sample authors. You can create as many as you wish anywhere in the code - authors = { - "charles": Author(name="Charles Dickens", active_years=(1220, 1280)), - "jane": Author(name="Jane Austen", active_years=(1580, 1640)), - } - - # Sample books. - books = [ - Book( - title="Oliver Twist", - author=authors["charles"], - published_on=date(year=1215, month=4, day=4), - in_stock=False, - rating=2, - tags=["Classic"], - ), - Book( - title="Great Expectations", - author=authors["charles"], - published_on=date(year=1220, month=4, day=4), - rating=5, - tags=["Classic"], - ), - Book( - title="Jane Eyre", - author=authors["charles"], - published_on=date(year=1225, month=6, day=4), - in_stock=False, - rating=3.4, - tags=["Classic", "Romance"], - ), - Book( - title="Wuthering Heights", - author=authors["jane"], - published_on=date(year=1600, month=4, day=4), - rating=4.0, - tags=["Classic", "Romance"], - ), - ] - - # Some library objects - libraries = [ - Library( - name="The Grand Library", - address="Kinogozi, Hoima, Uganda", - lost=[books[1]], - ), - Library( - name="Christian Library", - address="Buhimba, Hoima, Uganda", - new=(books[0], authors["jane"], 30), - ) - ] - - # Insert Many. You can given them a TTL (life_span_seconds). - await Book.insert(books, life_span_seconds=3600) - await Library.insert(libraries) - - # Insert One. You can also given it a TTL (life_span_seconds). - await Author.insert(Author(name="Jack Myers", active_years=(1240, 1300))) - - # Update One. You can also given it a TTL (life_span_seconds). - await Book.update(_id="Oliver Twist", data={"author": authors["jane"]}) - - # Update nested model indirectly - updated_jane = Author(**authors["jane"].dict()) - updated_jane.active_years = (1999, 2008) - await Book.update(_id="Oliver Twist", data={"author": updated_jane}) - - # Query the data - # Get all, with all fields shown. Data returned is a list of models instances. - all_books = await Book.select() - print(all_books) - # Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), - # in_stock=False), Book(...] - - # or paginate i.e. skip some books and return only upto a given number - paginated_books = await Book.select(skip=2, limit=2) - print(paginated_books) - - # Get some, with all fields shown. Data returned is a list of models instances. - some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"]) - print(some_books) - - # Note: Pagination does not work when ids are provided i.e. - assert some_books == await Book.select(ids=["Oliver Twist", "Jane Eyre"], skip=100, limit=10) - - # Get all, with only a few fields shown. Data returned is a list of dictionaries. - books_with_few_fields = await Book.select(columns=["author", "in_stock"]) - print(books_with_few_fields) - # Prints [{"author": "'Charles Dickens", "in_stock": "True"},...] - - # or paginate i.e. skip some books and return only upto a given number - paginated_books_with_few_fields = await Book.select(columns=["author", "in_stock"], skip=2, limit=2) - print(paginated_books_with_few_fields) - - # Get some, with only some fields shown. Data returned is a list of dictionaries. - some_books_with_few_fields = await Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"]) - print(some_books_with_few_fields) - - # Query the nested models directly. - some_authors = await Author.select(ids=["Jane Austen"]) - print(some_authors) - - # Delete any number of items - await Library.delete(ids=["The Grand Library"]) - - -asyncio.run(run_async()) -``` - -## How to test - -- Clone the repo and enter its root folder - - ```bash - git clone https://github.com/sopherapps/pydantic-redis.git && cd pydantic-redis - ``` - -- Create a virtual environment and activate it - - ```bash - virtualenv -p /usr/bin/python3.6 env && source env/bin/activate - ``` - -- Install the dependencies - - ```bash - pip install -r requirements.txt - ``` - -- Run the pre-commit installation - - ```bash - pre-commit install - ``` - -- Run the tests command - - ```bash - pytest --benchmark-disable - ``` - -- Run benchmarks - - ```bash - pytest --benchmark-compare --benchmark-autosave - ``` - -- Or run to get benchmarks summary - - ```shell - pytest test/test_benchmarks.py --benchmark-columns=mean,min,max --benchmark-name=short - ``` - ## Benchmarks On an average PC ~16GB RAM, i7 Core From f397d11edd9dd37779e4f83ad29b7e7310014c7f Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 29 Dec 2022 21:50:49 +0300 Subject: [PATCH 16/16] Update docstrings --- docs/css/custom.css | 27 ++++ docs/reference.md | 4 - pydantic_redis/__init__.py | 15 +- pydantic_redis/_shared/__init__.py | 21 +-- pydantic_redis/_shared/config.py | 26 ---- pydantic_redis/_shared/lua_scripts.py | 16 +- pydantic_redis/_shared/model/__init__.py | 7 + pydantic_redis/_shared/model/base.py | 154 +++++++++++++------ pydantic_redis/_shared/model/delete_utils.py | 20 ++- pydantic_redis/_shared/model/insert_utils.py | 103 ++++++++++--- pydantic_redis/_shared/model/prop_utils.py | 64 ++++++-- pydantic_redis/_shared/model/select_utils.py | 126 +++++++++++---- pydantic_redis/_shared/store.py | 43 +++++- pydantic_redis/_shared/utils.py | 102 ++++++++++-- pydantic_redis/asyncio/__init__.py | 29 +++- pydantic_redis/asyncio/model.py | 79 ++++++++-- pydantic_redis/asyncio/store.py | 31 +++- pydantic_redis/config.py | 43 ++++++ pydantic_redis/syncio/__init__.py | 25 ++- pydantic_redis/syncio/model.py | 79 ++++++++-- pydantic_redis/syncio/store.py | 31 +++- test/test_pydantic_redis.py | 2 +- 22 files changed, 787 insertions(+), 260 deletions(-) delete mode 100644 pydantic_redis/_shared/config.py create mode 100644 pydantic_redis/config.py diff --git a/docs/css/custom.css b/docs/css/custom.css index daeeff3b..07470ee9 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -11,3 +11,30 @@ .termy [data-termynal] { white-space: pre-wrap; } + +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: .05rem solid var(--md-typeset-table-color); +} + +/* Mark external links as such. */ +a.autorefs-external::after { + /* https://primer.style/octicons/arrow-up-right-24 */ + background-image: url('data:image/svg+xml,'); + content: ' '; + + display: inline-block; + position: relative; + top: 0.1em; + margin-left: 0.2em; + margin-right: 0.1em; + + height: 1em; + width: 1em; + border-radius: 100%; + background-color: var(--md-typeset-a-color); +} +a.autorefs-external:hover::after { + background-color: var(--md-accent-fg-color); +} diff --git a/docs/reference.md b/docs/reference.md index 868bcf5c..8dcfa5cb 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -1,5 +1 @@ -This part of the project documentation includes -the technical reference for the `pydantic-redis` -package. It can be used on a need-by-need basis. - ::: pydantic_redis \ No newline at end of file diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py index ae2d8198..0d09af08 100644 --- a/pydantic_redis/__init__.py +++ b/pydantic_redis/__init__.py @@ -1,8 +1,4 @@ -""" -Pydantic-redis -============== - -A simple declarative ORM for redis based on pydantic +"""A simple declarative ORM for redis based on pydantic. Provides: @@ -13,15 +9,6 @@ to a redis instance 4. A synchronous `syncio` and an asynchronous `asyncio` interface to the above classes - -Available subpackages ---------------------- - -asyncio - Asynchronous API for the ORM - -syncio - Synchronous API for the ORM """ from pydantic_redis.syncio import Store, Model, RedisConfig diff --git a/pydantic_redis/_shared/__init__.py b/pydantic_redis/_shared/__init__.py index 09765af1..cfecf191 100644 --- a/pydantic_redis/_shared/__init__.py +++ b/pydantic_redis/_shared/__init__.py @@ -1,26 +1,7 @@ -"""Shared utilities and base classes for pydantic-redis +"""Exposes shared utilities and base classes for pydantic-redis This includes basic functionality of mutating and querying redis via the pydantic-redis ORM regardless of whether this is done asynchronously or synchronously. This is a private package. - -Available subpackages ---------------------- -model - defines the base `AbstractModel` class to be extended by async - and sync versions of the `Model` class - -Available modules ------------------ -config - defines the `RedisConfig` class to be used to make a - connection to a redis server -lua_scripts - defines the lua scripts to be used in querying redis -store - defines the base `AbstractStore` class to be extended by the - async and sync versions of the `Store` class -utils - defines utility functions used across the project """ diff --git a/pydantic_redis/_shared/config.py b/pydantic_redis/_shared/config.py deleted file mode 100644 index 6454c3ed..00000000 --- a/pydantic_redis/_shared/config.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Defines the `RedisConfig` for connecting to a redis server - -""" -from typing import Optional - -from pydantic import BaseModel - - -class RedisConfig(BaseModel): - host: str = "localhost" - port: int = 6379 - db: int = 0 - password: Optional[str] = None - ssl: bool = False - encoding: Optional[str] = "utf-8" - - @property - def redis_url(self) -> str: - """Returns a redis url to connect to""" - proto = "rediss" if self.ssl else "redis" - if self.password is None: - return f"{proto}://{self.host}:{self.port}/{self.db}" - return f"{proto}://:{self.password}@{self.host}:{self.port}/{self.db}" - - class Config: - orm_mode = True diff --git a/pydantic_redis/_shared/lua_scripts.py b/pydantic_redis/_shared/lua_scripts.py index 8221e501..433c8646 100644 --- a/pydantic_redis/_shared/lua_scripts.py +++ b/pydantic_redis/_shared/lua_scripts.py @@ -1,4 +1,18 @@ -"""Module containing the lua scripts used in select queries""" +"""Exposes the redis lua scripts to be used in select queries. + +Attributes: + SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting all records from redis + PAGINATED_SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting a slice of all records from redis, + given a `limit` maximum number of records to return and a `skip` number of records to skip. + SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT: the script for selecting some records from redis, given a bunch of `ids` + SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting all records, but returning only a subset of + the fields in each record. + PAGINATED_SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT: the script for selecting a slice of all records from redis, + given a `limit` maximum number of records to return and a `skip` number of records to skip, but returning + only a subset of the fields in each record. + SELECT_SOME_FIELDS_FOR_SOME_IDS_SCRIPT: the script for selecting some records from redis, given a bunch of `ids`, + but returning only a subset of the fields in each record. +""" SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT = """ local s_find = string.find diff --git a/pydantic_redis/_shared/model/__init__.py b/pydantic_redis/_shared/model/__init__.py index 4b43caa9..c6d48444 100644 --- a/pydantic_redis/_shared/model/__init__.py +++ b/pydantic_redis/_shared/model/__init__.py @@ -1 +1,8 @@ +"""Exposes the utilities and the base classes for models. + +This includes basic functionality of mutating and querying +redis via the pydantic-redis ORM regardless of whether this +is done asynchronously or synchronously. +""" + from .base import AbstractModel diff --git a/pydantic_redis/_shared/model/base.py b/pydantic_redis/_shared/model/base.py index f858872f..95f3c3f9 100644 --- a/pydantic_redis/_shared/model/base.py +++ b/pydantic_redis/_shared/model/base.py @@ -1,4 +1,6 @@ -"""Module containing the base model""" +"""Exposes the Base `Model` common to both async and sync APIs + +""" import typing from typing import Dict, Tuple, Any, Type, Union, List, Optional @@ -7,7 +9,7 @@ from pydantic_redis._shared.utils import ( typing_get_origin, typing_get_args, - from_any_to_str_or_bytes, + from_any_to_valid_redis_type, from_dict_to_key_value_list, from_bytes_to_str, from_str_or_bytes_to_any, @@ -18,8 +20,23 @@ class AbstractModel(BaseModel): - """ - An abstract class to help with typings for Model class + """A base class for all Models, sync and async alike. + + See the child classes for more. + + Attributes: + _primary_key_field (str): the field that can uniquely identify each record + for the current Model + _field_types (Dict[str, Any]): a mapping of the fields and their types for + the current model + _store (AbstractStore): the Store in which the current model is registered. + _nested_model_tuple_fields (Dict[str, Tuple[Any, ...]]): a mapping of + fields and their types for fields that have tuples of nested models + _nested_model_list_fields (Dict[str, Type["AbstractModel"]]): a mapping of + fields and their associated nested models for fields that have + lists of nested models + _nested_model_fields (Dict[str, Type["AbstractModel"]]): a mapping of + fields and their associated nested models for fields that have nested models """ _primary_key_field: str @@ -34,37 +51,66 @@ class Config: @classmethod def get_store(cls) -> AbstractStore: - """Returns the instance of the store for this model""" + """Gets the Store in which the current model is registered. + + Returns: + the instance of the store for this model + """ return cls._store @classmethod def get_nested_model_tuple_fields(cls): - """Returns the fields that have tuples of nested models""" + """Gets the mapping for fields that have tuples of nested models. + + Returns: + The mapping of field name and field type of a form similar to + `Tuple[str, Book, date]` + """ return cls._nested_model_tuple_fields @classmethod def get_nested_model_list_fields(cls): - """Returns the fields that have list of nested models""" + """Gets the mapping for fields that have lists of nested models. + + Returns: + The mapping of field name and model class nested in that field. + """ return cls._nested_model_list_fields @classmethod def get_nested_model_fields(cls): - """Returns the fields that have nested models""" + """Gets the mapping for fields that have nested models. + + Returns: + The mapping of field name and model class nested in that field. + """ return cls._nested_model_fields @classmethod def get_primary_key_field(cls): - """Gets the protected _primary_key_field""" + """Gets the field that can uniquely identify each record of current Model + + Returns: + the field that can be used to uniquely identify each record of current Model + """ return cls._primary_key_field @classmethod def get_field_types(cls) -> Dict[str, Any]: - """Returns the fields types of this model""" + """Gets the mapping of field and field_type for current Model. + + Returns: + the mapping of field and field_type for current Model + """ return cls._field_types @classmethod def initialize(cls): - """Initializes class-wide variables for performance's reasons e.g. it caches the nested model fields""" + """Initializes class-wide variables for performance's reasons. + + This is a performance hack that initializes an variables that are common + to all instances of the current Model e.g. the field types. + """ cls._field_types = typing.get_type_hints(cls) cls._nested_model_list_fields = {} @@ -112,21 +158,30 @@ def initialize(cls): @classmethod def serialize_partially(cls, data: Optional[Dict[str, Any]]) -> Dict[str, Any]: - """Converts non primitive data types into str""" - return {key: from_any_to_str_or_bytes(value) for key, value in data.items()} + """Casts complex data types within a given dictionary to valid redis types. + + Args: + data: the dictionary containing data with complex data types + + Returns: + the transformed dictionary + """ + return {key: from_any_to_valid_redis_type(value) for key, value in data.items()} @classmethod def deserialize_partially( cls, data: Union[List[Any], Dict[Any, Any]] = () ) -> Dict[str, Any]: - """ - Converts str or bytes to their expected data types, given a list of properties got from the - list of lists got after calling EVAL on redis. + """Casts str or bytes in a dict or flattened key-value list to expected data types. + + Converts str or bytes to their expected data types - EVAL returns a List of Lists of key, values where the value for a given key is in the position - just after the key e.g. [["foo", "bar", "head", 9]] => [{"foo": "bar", "head": 9}] + Args: + data: flattened list of key-values or dictionary of data to cast. + Keeping it as potentially a dictionary ensures backward compatibility. - Note: For backward compatibility, data can also be a dict. + Returns: + the dictionary of properly parsed key-values. """ if isinstance(data, dict): # for backward compatibility @@ -144,38 +199,45 @@ def deserialize_partially( value = from_str_or_bytes_to_any(value=data[i + 1], field_type=field_type) if key in nested_model_list_fields and value is not None: - value = deserialize_nested_model_list( - field_type=nested_model_list_fields[key], value=value - ) + value = _cast_lists(value, nested_model_list_fields[key]) elif key in nested_model_tuple_fields and value is not None: - value = deserialize_nested_model_tuple( - field_types=nested_model_tuple_fields[key], value=value - ) + value = _cast_tuples(value, nested_model_tuple_fields[key]) elif key in nested_model_fields and value is not None: - value = deserialize_nested_model( - field_type=nested_model_fields[key], value=value - ) + value = _cast_to_model(value=value, model=nested_model_fields[key]) parsed_dict[key] = value return parsed_dict -def deserialize_nested_model_list( - field_type: Type[AbstractModel], value: List[Any] -) -> List[AbstractModel]: - """Deserializes a list of key values for the given field returning a list of nested models""" - return [field_type(**field_type.deserialize_partially(item)) for item in value] +def _cast_lists(value: List[Any], _type: Type[AbstractModel]) -> List[AbstractModel]: + """Casts a list of flattened key-value lists into a list of _type. + Args: + _type: the type to cast the records to. + value: the value to convert + + Returns: + a list of records of the given _type + """ + return [_type(**_type.deserialize_partially(item)) for item in value] -def deserialize_nested_model_tuple( - field_types: Tuple[Any, ...], value: List[Any] -) -> Tuple[Any, ...]: - """Deserializes a list of key values for the given field returning a tuple of nested models among others""" + +def _cast_tuples(value: List[Any], _type: Tuple[Any, ...]) -> Tuple[Any, ...]: + """Casts a list of flattened key-value lists into a list of tuple of _type,. + + Args: + _type: the tuple signature type to cast the records to + e.g. Tuple[str, Book, int] + value: the value to convert + + Returns: + a list of records of tuple signature specified by `_type` + """ items = [] - for field_type, value in zip(field_types, value): + for field_type, value in zip(_type, value): if issubclass(field_type, AbstractModel) and value is not None: value = field_type(**field_type.deserialize_partially(value)) items.append(value) @@ -183,8 +245,14 @@ def deserialize_nested_model_tuple( return tuple(items) -def deserialize_nested_model( - field_type: Type[AbstractModel], value: List[Any] -) -> AbstractModel: - """Deserializes a list of key values for the given field returning the nested model""" - return field_type(**field_type.deserialize_partially(value)) +def _cast_to_model(value: List[Any], model: Type[AbstractModel]) -> AbstractModel: + """Converts a list of flattened key-value lists into a list of models,. + + Args: + model: the model class to cast to + value: the value to cast + + Returns: + a list of model instances of type `model` + """ + return model(**model.deserialize_partially(value)) diff --git a/pydantic_redis/_shared/model/delete_utils.py b/pydantic_redis/_shared/model/delete_utils.py index 3ac431e5..c3905590 100644 --- a/pydantic_redis/_shared/model/delete_utils.py +++ b/pydantic_redis/_shared/model/delete_utils.py @@ -1,19 +1,25 @@ -"""Module containing common functionality for deleting records""" +"""Exposes shared utilities for deleting records from redis""" from typing import Type, Union, List from redis.client import Pipeline from redis.asyncio.client import Pipeline as AioPipeline from pydantic_redis._shared.model import AbstractModel -from pydantic_redis._shared.model.prop_utils import get_primary_key, get_table_index_key +from pydantic_redis._shared.model.prop_utils import get_redis_key, get_model_index_key def delete_on_pipeline( model: Type[AbstractModel], pipeline: Union[Pipeline, AioPipeline], ids: List[str] ): - """ - Pipelines the deletion of the given ids, so that when pipeline.execute - is called later, deletion occurs + """Adds delete operations for the given ids to the redis pipeline. + + Args: + model: the Model from which the given records are to be deleted. + pipeline: the Redis pipeline on which the delete operations are + to be added. + ids: the list of primary keys of the records that are to be removed. + + Later when pipeline.execute is called later, the actual deletion occurs """ primary_keys = [] @@ -23,10 +29,10 @@ def delete_on_pipeline( primary_keys = [ids] names = [ - get_primary_key(model=model, primary_key_value=primary_key_value) + get_redis_key(model=model, primary_key_value=primary_key_value) for primary_key_value in primary_keys ] pipeline.delete(*names) # remove the primary keys from the indexz - table_index_key = get_table_index_key(model=model) + table_index_key = get_model_index_key(model=model) pipeline.zrem(table_index_key, *names) diff --git a/pydantic_redis/_shared/model/insert_utils.py b/pydantic_redis/_shared/model/insert_utils.py index 2492d6db..ef45f767 100644 --- a/pydantic_redis/_shared/model/insert_utils.py +++ b/pydantic_redis/_shared/model/insert_utils.py @@ -1,4 +1,6 @@ -"""Module containing the mixin for insert functionality in model""" +"""Exposes the utility functions for inserting records into redis. + +""" from datetime import datetime from typing import Union, Optional, Any, Dict, Tuple, List, Type @@ -6,8 +8,8 @@ from redis.client import Pipeline from .prop_utils import ( - get_primary_key, - get_table_index_key, + get_redis_key, + get_model_index_key, NESTED_MODEL_PREFIX, NESTED_MODEL_LIST_FIELD_PREFIX, NESTED_MODEL_TUPLE_FIELD_PREFIX, @@ -23,23 +25,37 @@ def insert_on_pipeline( record: Union[AbstractModel, Dict[str, Any]], life_span: Optional[Union[float, int]] = None, ) -> Any: - """ - Creates insert commands for the given record on the given pipeline but does not execute - thus the data is not yet persisted in redis - Returns the key of the created item + """Add an insert operation to the redis pipeline. + + Later when the pipeline.execute is called, the actual inserts occur. + This reduces the number of network requests to the redis server thus + improving performance. + + Args: + model: the Model whose records are to be inserted into redis. + pipeline: the Redis pipeline on which the insert operation + is to be added. + _id: the primary key of the record to be inserted in redis. + It is None when inserting, and some value when updating. + record: the model instance or dictionary to be inserted into redis. + life_span: the time-to-live in seconds for the record to be inserted. + (default: None) + + Returns: + the primary key of the record that is to be inserted. """ key = _id if _id is not None else getattr(record, model.get_primary_key_field()) - data = _get_serializable_dict( + data = _serialize_nested_models( model=model, pipeline=pipeline, record=record, life_span=life_span ) - name = get_primary_key(model=model, primary_key_value=key) + name = get_redis_key(model=model, primary_key_value=key) mapping = model.serialize_partially(data) pipeline.hset(name=name, mapping=mapping) if life_span is not None: pipeline.expire(name=name, time=life_span) # save the primary key in an index: a sorted set, whose score is current timestamp - table_index_key = get_table_index_key(model) + table_index_key = get_model_index_key(model) timestamp = datetime.utcnow().timestamp() pipeline.zadd(table_index_key, {name: timestamp}) if life_span is not None: @@ -48,23 +64,36 @@ def insert_on_pipeline( return name -def _get_serializable_dict( +def _serialize_nested_models( model: Type[AbstractModel], pipeline: Union[Pipeline, AioPipeline], record: Union[AbstractModel, Dict[str, Any]], life_span: Optional[Union[float, int]] = None, ) -> Dict[str, Any]: - """ - Returns a dictionary that can be serialized. + """Converts nested models into their primary keys. + + In order to make the record serializable, all nested models including those in + lists and tuples of nested models are converted to their primary keys, + after being their insert operations have been added to the pipeline. + A few cleanups it does include: - Upserting any nested records in `record` - - Replacing the keys of nested records with their NESTED_MODEL_PREFIX suffixed versions + - Replacing the keys of nested records with their NESTED_MODEL_PREFIX prefixed versions e.g. `__author` instead of author - - Replacing the keys of lists of nested records with their NESTED_MODEL_LIST_FIELD_PREFIX suffixed versions + - Replacing the keys of lists of nested records with their NESTED_MODEL_LIST_FIELD_PREFIX prefixed versions e.g. `__%&l_author` instead of author - - Replacing the keys of tuples of nested records with their NESTED_MODEL_TUPLE_FIELD_PREFIX suffixed versions + - Replacing the keys of tuples of nested records with their NESTED_MODEL_TUPLE_FIELD_PREFIX prefixed versions e.g. `__%&l_author` instead of author - Replacing the values of nested records with their foreign keys + + Args: + model: the model the given record belongs to. + pipeline: the redis pipeline on which the redis operations are to be done. + record: the model or dictionary whose nested models are to be serialized. + life_span: the time-to-live in seconds for the given record (default: None). + + Returns: + the partially serialized dict that has no nested models """ data = record.items() if isinstance(record, dict) else record new_data = {} @@ -77,11 +106,11 @@ def _get_serializable_dict( key, value = k, v if key in nested_model_list_fields: - key, value = _serialize_nested_model_list_field( + key, value = _serialize_list( key=key, value=value, pipeline=pipeline, life_span=life_span ) elif key in nested_model_tuple_fields: - key, value = _serialize_nested_model_tuple_field( + key, value = _serialize_tuple( key=key, value=value, pipeline=pipeline, @@ -89,7 +118,7 @@ def _get_serializable_dict( tuple_fields=nested_model_tuple_fields, ) elif key in nested_model_fields: - key, value = _serialize_nested_model_field( + key, value = _serialize_model( key=key, value=value, pipeline=pipeline, life_span=life_span ) @@ -97,14 +126,22 @@ def _get_serializable_dict( return new_data -def _serialize_nested_model_tuple_field( +def _serialize_tuple( key: str, value: Tuple[AbstractModel], pipeline: Union[Pipeline, AioPipeline], life_span: Optional[Union[float, int]], tuple_fields: Dict[str, Tuple[Any, ...]], ) -> Tuple[str, List[Any]]: - """Serializes a key-value pair for a field that has a tuple of nested models""" + """Replaces models in a tuple with strings. + + It adds insert operations for the records in the tuple onto the pipeline + and returns the tuple with the models replaced by their primary keys as value. + + Returns: + key: the original `key` prefixed with NESTED_MODEL_TUPLE_FIELD_PREFIX + value: tthe tuple with the models replaced by their primary keys + """ try: field_types = tuple_fields.get(key, ()) value = [ @@ -127,13 +164,21 @@ def _serialize_nested_model_tuple_field( return key, value -def _serialize_nested_model_list_field( +def _serialize_list( key: str, value: List[AbstractModel], pipeline: Union[Pipeline, AioPipeline], life_span: Optional[Union[float, int]], ) -> Tuple[str, List[Any]]: - """Serializes a key-value pair for a field that has a list of nested models""" + """Casts a list of models into a list of strings + + It adds insert operations for the records in the list onto the pipeline + and returns a list of their primary keys as value. + + Returns: + key: the original `key` prefixed with NESTED_MODEL_LIST_FIELD_PREFIX + value: the list of primary keys of the records to be inserted + """ try: value = [ insert_on_pipeline( @@ -153,13 +198,21 @@ def _serialize_nested_model_list_field( return key, value -def _serialize_nested_model_field( +def _serialize_model( key: str, value: AbstractModel, pipeline: Union[Pipeline, AioPipeline], life_span: Optional[Union[float, int]], ) -> Tuple[str, str]: - """Serializes a key-value pair for a field that has a nested model""" + """Casts a model into a string + + It adds an insert operation for the given model onto the pipeline + and returns its primary key as value. + + Returns: + key: the original `key` prefixed with NESTED_MODEL_PREFIX + value: the primary key of the model + """ try: value = insert_on_pipeline( model=value.__class__, diff --git a/pydantic_redis/_shared/model/prop_utils.py b/pydantic_redis/_shared/model/prop_utils.py index 335b3dda..9256a247 100644 --- a/pydantic_redis/_shared/model/prop_utils.py +++ b/pydantic_redis/_shared/model/prop_utils.py @@ -1,4 +1,10 @@ -"""Module containing utils for getting properties of the Model""" +"""Exposes utils for getting properties of the Model + +Attributes: + NESTED_MODEL_PREFIX (str): the prefix for fields with single nested models + NESTED_MODEL_LIST_FIELD_PREFIX (str): the prefix for fields with lists of nested models + NESTED_MODEL_TUPLE_FIELD_PREFIX (str): the prefix for fields with tuples of nested models +""" from typing import Type, Any @@ -9,29 +15,55 @@ NESTED_MODEL_TUPLE_FIELD_PREFIX = "____" -def get_primary_key(model: Type[AbstractModel], primary_key_value: Any): - """ - Returns the primary key value concatenated to the table name for uniqueness - """ - return f"{get_table_prefix(model)}{primary_key_value}" +def get_redis_key(model: Type[AbstractModel], primary_key_value: Any): + """Gets the key used internally in redis for the `primary_key_value` of `model`. + Args: + model: the model for which the key is to be generated + primary_key_value: the external facing primary key value -def get_table_prefix(model: Type[AbstractModel]): - """ - Returns the prefix of the all the redis keys that are associated with this table + Returns: + the primary key internally used for `primary_key_value` of `model` """ - table_name = model.__name__.lower() - return f"{table_name}_%&_" + return f"{get_redis_key_prefix(model)}{primary_key_value}" + +def get_redis_key_prefix(model: Type[AbstractModel]): + """Gets the prefix for keys used internally in redis for records of `model`. -def get_table_keys_regex(model: Type[AbstractModel]): + Args: + model: the model for which the redis key prefix is to be generated + + Returns: + the prefix of the all the redis keys that are associated with this model """ - Returns the table name regex to get all keys that belong to this table + model_name = model.__name__.lower() + return f"{model_name}_%&_" + + +def get_redis_keys_regex(model: Type[AbstractModel]): + """Gets the regex for all keys of records of `model` used internally in redis. + + Args: + model: the model for which the redis key regex is to be generated + + Returns: + the regular expression for all keys of records of `model` used internally in redis """ - return f"{get_table_prefix(model)}*" + return f"{get_redis_key_prefix(model)}*" + +def get_model_index_key(model: Type[AbstractModel]): + """Gets the key for the index set of `model` used internally in redis. -def get_table_index_key(model: Type[AbstractModel]): - """Returns the key for the set in which the primary keys of the given table have been saved""" + The index for each given model stores the primary keys for all records + that belong to the given model. + + Args: + model: the model whose index is wanted. + + Returns: + the the key for the index set of `model` used internally in redis. + """ table_name = model.__name__.lower() return f"{table_name}__index" diff --git a/pydantic_redis/_shared/model/select_utils.py b/pydantic_redis/_shared/model/select_utils.py index fe6229ff..a1f5e77f 100644 --- a/pydantic_redis/_shared/model/select_utils.py +++ b/pydantic_redis/_shared/model/select_utils.py @@ -1,13 +1,15 @@ -"""Module containing the mixin functionality for selecting""" +"""Exposes utilities for selecting records from redis using lua scripts. + +""" from typing import List, Any, Type, Union, Awaitable, Optional from pydantic_redis._shared.model.prop_utils import ( NESTED_MODEL_PREFIX, NESTED_MODEL_LIST_FIELD_PREFIX, NESTED_MODEL_TUPLE_FIELD_PREFIX, - get_table_keys_regex, - get_table_prefix, - get_table_index_key, + get_redis_keys_regex, + get_redis_key_prefix, + get_model_index_key, ) @@ -15,10 +17,17 @@ def get_select_fields(model: Type[AbstractModel], columns: List[str]) -> List[str]: - """ - Gets the fields to be used for selecting HMAP fields in Redis + """Gets the fields to be used for selecting HMAP fields in Redis. + It replaces any fields in `columns` that correspond to nested records with their - `__` suffixed versions + `__` prefixed versions. + + Args: + model: the model for which the fields for selecting are to be derived. + columns: the fields that are to be transformed into fields for selecting. + + Returns: + the fields for selecting, with nested fields being given appropriate prefixes. """ fields = [] nested_model_list_fields = model.get_nested_model_list_fields() @@ -43,18 +52,21 @@ def select_all_fields_all_ids( skip: int = 0, limit: Optional[int] = None, ) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]: - """ - Selects all items in the database, returning all their fields + """Retrieves all records of the given model in the redis database. - However, if `limit` is set, the number of items - returned will be less or equal to `limit`. - `skip` defaults to 0. It is the number of items to skip. - `skip` is only relevant when limit is specified. + Args: + model: the Model whose records are to be retrieved. + skip: the number of records to skip. + limit: the maximum number of records to return. If None, limit is infinity. + + Returns: + the list of records from redis, each record being a flattened list of key-values. + In case we are using async, an Awaitable of that list is returned instead. """ if isinstance(limit, int): return _select_all_ids_all_fields_paginated(model=model, limit=limit, skip=skip) else: - table_keys_regex = get_table_keys_regex(model=model) + table_keys_regex = get_redis_keys_regex(model=model) args = [table_keys_regex] store = model.get_store() return store.select_all_fields_for_all_ids_script(args=args) @@ -63,8 +75,17 @@ def select_all_fields_all_ids( def select_all_fields_some_ids( model: Type[AbstractModel], ids: List[str] ) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]: - """Selects some items in the database, returning all their fields""" - table_prefix = get_table_prefix(model=model) + """Retrieves some records from redis. + + Args: + model: the Model whose records are to be retrieved. + ids: the list of primary keys of the records to be retrieved. + + Returns: + the list of records where each record is a flattened key-value list. + In case we are using async, an Awaitable of that list is returned instead. + """ + table_prefix = get_redis_key_prefix(model=model) keys = [f"{table_prefix}{key}" for key in ids] store = model.get_store() return store.select_all_fields_for_some_ids_script(keys=keys) @@ -76,13 +97,17 @@ def select_some_fields_all_ids( skip: int = 0, limit: Optional[int] = None, ) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]: - """ - Selects all items in the database, returning only the specified fields. + """Retrieves records of model from redis, each as with a subset of the fields. + + Args: + model: the Model whose records are to be retrieved. + fields: the subset of fields to return for each record. + skip: the number of records to skip. + limit: the maximum number of records to return. If None, limit is infinity. - However, if `limit` is set, the number of items - returned will be less or equal to `limit`. - `skip` defaults to 0. It is the number of items to skip. - `skip` is only relevant when limit is specified. + Returns: + the list of records from redis, each record being a flattened list of key-values. + In case we are using async, an Awaitable of that list is returned instead. """ columns = get_select_fields(model=model, columns=fields) @@ -91,7 +116,7 @@ def select_some_fields_all_ids( model=model, columns=columns, limit=limit, skip=skip ) else: - table_keys_regex = get_table_keys_regex(model=model) + table_keys_regex = get_redis_keys_regex(model=model) args = [table_keys_regex, *columns] store = model.get_store() return store.select_some_fields_for_all_ids_script(args=args) @@ -100,8 +125,18 @@ def select_some_fields_all_ids( def select_some_fields_some_ids( model: Type[AbstractModel], fields: List[str], ids: List[str] ) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]: - """Selects some of items in the database, returning only the specified fields""" - table_prefix = get_table_prefix(model=model) + """Retrieves some records of current model from redis, each as with a subset of the fields. + + Args: + model: the Model whose records are to be retrieved. + fields: the subset of fields to return for each record. + ids: the list of primary keys of the records to be retrieved. + + Returns: + the list of records from redis, each record being a flattened list of key-values. + In case we are using async, an Awaitable of that list is returned instead. + """ + table_prefix = get_redis_key_prefix(model=model) keys = [f"{table_prefix}{key}" for key in ids] columns = get_select_fields(model=model, columns=fields) store = model.get_store() @@ -111,12 +146,14 @@ def select_some_fields_some_ids( def parse_select_response( model: Type[AbstractModel], response: List[List], as_models: bool ): - """ - Converts a list of lists of key-values into a list of models if `as_models` is true or leaves them as dicts - with foreign keys replaced by model instances. The list is got from calling EVAL on Redis . + """Casts a list of flattened key-value lists into a list of models or dicts. + + It replaces any foreign keys with the related model instances, + and converts the list of flattened key-value lists into a list of models or dicts. + e.g. [["foo", "bar", "head", 9]] => [{"foo": "bar", "head": 9}] - EVAL returns a List of Lists of key, values where the value for a given key is in the position - just after the key e.g. [["foo", "bar", "head", 9]] => [{"foo": "bar", "head": 9}] + Returns: + If `as_models` is true, list of models else list of dicts """ if len(response) == 0: return None @@ -134,10 +171,20 @@ def parse_select_response( def _select_all_ids_all_fields_paginated( model: Type[AbstractModel], limit: int, skip: Optional[int] ): - """Selects all fields for at most `limit` number of items after skipping `skip` items""" + """Retrieves a slice of all records of the given model in the redis database. + + Args: + model: the Model whose records are to be retrieved. + skip: the number of records to skip. + limit: the maximum number of records to return. If None, limit is infinity. + + Returns: + the list of records from redis, each record being a flattened list of key-values. + In case we are using async, an Awaitable of that list is returned instead. + """ if skip is None: skip = 0 - table_index_key = get_table_index_key(model) + table_index_key = get_model_index_key(model) args = [table_index_key, skip, limit] store = model.get_store() return store.paginated_select_all_fields_for_all_ids_script(args=args) @@ -146,10 +193,21 @@ def _select_all_ids_all_fields_paginated( def _select_some_fields_all_ids_paginated( model: Type[AbstractModel], columns: List[str], limit: int, skip: int ): - """Selects some fields for at most `limit` number of items after skipping `skip` items""" + """Retrieves a slice of all records of model from redis, each as with a subset of the fields. + + Args: + model: the Model whose records are to be retrieved. + columns: the subset of fields to return for each record. + skip: the number of records to skip. + limit: the maximum number of records to return. If None, limit is infinity. + + Returns: + the list of records from redis, each record being a flattened list of key-values. + In case we are using async, an Awaitable of that list is returned instead. + """ if skip is None: skip = 0 - table_index_key = get_table_index_key(model) + table_index_key = get_model_index_key(model) args = [table_index_key, skip, limit, *columns] store = model.get_store() return store.paginated_select_some_fields_for_all_ids_script(args=args) diff --git a/pydantic_redis/_shared/store.py b/pydantic_redis/_shared/store.py index 65e10c68..08dba776 100644 --- a/pydantic_redis/_shared/store.py +++ b/pydantic_redis/_shared/store.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from redis.commands.core import Script, AsyncScript -from .config import RedisConfig +from ..config import RedisConfig from .lua_scripts import ( SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT, SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT, @@ -23,8 +23,9 @@ class AbstractStore(BaseModel): - """ - An abstract class of a store + """An abstract class of a store. + + Check the child classes for more definition. """ name: str @@ -67,11 +68,24 @@ def __init__( self._register_lua_scripts() def _connect_to_redis(self) -> Union[Redis, AioRedis]: - """Connects the store to redis, returning a proper connection""" + """Connects the store to redis. + + Connects to the redis database basing on the `redis_config` + attribute of this instance. + + Returns: + A connection object to a redis database + """ raise NotImplementedError("implement _connect_to_redis first") def _register_lua_scripts(self): - """Registers the lua scripts for this redis instance""" + """Registers the lua scripts for this redis instance. + + In order to save on memory and bandwidth, the redis lua scripts + need to be called using EVALSHA instead of EVAL. The latter transfers + the scripts to the redis server on every invocation while the former + saves the script in redis itself and invokes it using a hashed (SHA) value. + """ self.select_all_fields_for_all_ids_script = self.redis_store.register_script( SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT ) @@ -96,7 +110,15 @@ def _register_lua_scripts(self): ) def register_model(self, model_class: Type["AbstractModel"]): - """Registers the model to this store""" + """Registers the model to this store. + + Each store manages a number of models. In order to associate + a model to a redis database, a Store must register it. + + Args: + model_class: the class which represents a given schema of + a certain type of records to be saved in redis. + """ if not isinstance(model_class.get_primary_key_field(), str): raise NotImplementedError( f"{model_class.__name__} should have a _primary_key_field" @@ -107,5 +129,12 @@ def register_model(self, model_class: Type["AbstractModel"]): self.models[model_class.__name__.lower()] = model_class def model(self, name: str) -> Type["AbstractModel"]: - """Gets a model by name: case insensitive""" + """Gets a model by name. This is case insensitive. + + Args: + name: the case-insensitive name of the model class + + Returns: + the class corresponding to the given name + """ return self.models[name.lower()] diff --git a/pydantic_redis/_shared/utils.py b/pydantic_redis/_shared/utils.py index a76de887..5ecc2130 100644 --- a/pydantic_redis/_shared/utils.py +++ b/pydantic_redis/_shared/utils.py @@ -1,4 +1,6 @@ -"""Module containing common utilities""" +"""Exposes common utilities. + +""" import typing from typing import Any, Tuple, Optional, Union, Dict, Callable, Type, List @@ -6,9 +8,16 @@ def strip_leading(word: str, substring: str) -> str: - """ - Strips the leading substring if it exists. - This is contrary to rstrip which can looks at removes each character in the substring + """Strips the leading substring if it exists. + + This is contrary to rstrip which removes each character in the substring + + Args: + word: the string to strip from + substring: the string to be stripped from the word. + + Returns: + the stripped word """ if word.startswith(substring): return word[len(substring) :] @@ -16,7 +25,14 @@ def strip_leading(word: str, substring: str) -> str: def typing_get_args(v: Any) -> Tuple[Any, ...]: - """Gets the __args__ of the annotations of a given typing""" + """Gets the __args__ of the annotations of a given typing. + + Args: + v: the typing object whose __args__ are required. + + Returns: + the __args__ of the item passed + """ try: return typing.get_args(v) except AttributeError: @@ -24,7 +40,14 @@ def typing_get_args(v: Any) -> Tuple[Any, ...]: def typing_get_origin(v: Any) -> Optional[Any]: - """Gets the __origin__ of the annotations of a given typing""" + """Gets the __origin__ of the annotations of a given typing. + + Args: + v: the typing object whose __origin__ are required. + + Returns: + the __origin__ of the item passed + """ try: return typing.get_origin(v) except AttributeError: @@ -32,14 +55,31 @@ def typing_get_origin(v: Any) -> Optional[Any]: def from_bytes_to_str(value: Union[str, bytes]) -> str: - """Converts bytes to str""" + """Converts bytes to str. + + Args: + value: the potentially bytes object to transform. + + Returns: + the string value of the argument passed + """ if isinstance(value, bytes): return str(value, "utf-8") return value def from_str_or_bytes_to_any(value: Any, field_type: Type) -> Any: - """Converts str or bytes to arbitrary data""" + """Converts str or bytes to arbitrary data. + + Converts the the `value` from a string or bytes to the `field_type`. + + Args: + value: the string or bytes to be transformed to the `field_type` + field_type: the type to which value is to be converted + + Returns: + the `field_type` version of the `value`. + """ if isinstance(value, (bytes, bytearray, memoryview)): return orjson.loads(value) elif isinstance(value, str) and field_type != str: @@ -47,25 +87,53 @@ def from_str_or_bytes_to_any(value: Any, field_type: Type) -> Any: return value -def from_any_to_str_or_bytes(value: Any) -> Union[str, bytes]: - """Converts arbitrary data into str or bytes""" +def from_any_to_valid_redis_type(value: Any) -> Union[str, bytes, List[Any]]: + """Converts arbitrary data into valid redis types + + Converts the the `value` from any type to a type that + are acceptable by redis. + By default, complex data is transformed to bytes. + + Args: + value: the value to be transformed to a valid redis data type + + Returns: + the transformed version of the `value`. + """ if isinstance(value, str): return value + elif isinstance(value, set): + return list(value) return orjson.dumps(value, default=default_json_dump) -def default_json_dump(obj): - """Default JSON dump for orjson""" +def default_json_dump(obj: Any): + """Serializes objects orjson cannot serialize. + + Args: + obj: the object to serialize + + Returns: + the bytes or string value of the object + """ if hasattr(obj, "json") and isinstance(obj.json, Callable): return obj.json() - elif isinstance(obj, set): - # Set does not exist in JSON. - # It's fine to use list instead, it becomes a Set when deserializing. - return list(obj) + return obj def from_dict_to_key_value_list(data: Dict[str, Any]) -> List[Any]: - """Converts dict to flattened list of key, values where the value after the key""" + """Converts dict to flattened list of key, values. + + {"foo": "bar", "hen": "rooster"} becomes ["foo", "bar", "hen", "rooster"] + When redis lua scripts are used, the value returned is a flattened list, + similar to that shown above. + + Args: + data: the dictionary to flatten + + Returns: + the flattened list version of `data` + """ parsed_list = [] for k, v in data.items(): diff --git a/pydantic_redis/asyncio/__init__.py b/pydantic_redis/asyncio/__init__.py index 89f94745..1da65b74 100644 --- a/pydantic_redis/asyncio/__init__.py +++ b/pydantic_redis/asyncio/__init__.py @@ -1,9 +1,34 @@ -"""Asynchronous version of pydantic-redis +"""Asynchronous API for pydantic-redis ORM. +Typical usage example: + +```python +import asyncio +from pydantic_redis.asyncio import Store, Model, RedisConfig + +class Book(Model): + _primary_key_field = 'title' + title: str + +async def main(): + store = Store(name="sample", redis_config=RedisConfig()) + store.register_model(Book) + + await Book.insert(Book(title="Oliver Twist", author="Charles Dickens")) + await Book.update( + _id="Oliver Twist", data={"author": "Jane Austen"}, life_span_seconds=3600 + ) + results = await Book.select() + await Book.delete(ids=["Oliver Twist", "Great Expectations"]) + +if __name__ == "__main__": + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) +``` """ from .model import Model from .store import Store -from .._shared.config import RedisConfig +from ..config import RedisConfig __all__ = [Model, Store, RedisConfig] diff --git a/pydantic_redis/asyncio/model.py b/pydantic_redis/asyncio/model.py index 38738335..5f31a19f 100644 --- a/pydantic_redis/asyncio/model.py +++ b/pydantic_redis/asyncio/model.py @@ -1,4 +1,8 @@ -"""Module containing the model classes""" +"""Exposes the Base `Model` class for creating custom asynchronous models. + +This module contains the `Model` class which should be inherited when +creating model's for use in the asynchronous API of pydantic-redis. +""" from typing import Optional, List, Any, Union, Dict from .._shared.model import AbstractModel @@ -16,8 +20,11 @@ class Model(AbstractModel): - """ - The section in the store that saves rows of the same kind + """The Base class for all Asynchronous models. + + Inherit this class when creating a new model. + The new model should have `_primary_key_field` defined. + Any interaction with redis is done through `Model`'s. """ _store: Store @@ -28,8 +35,18 @@ async def insert( data: Union[List[AbstractModel], AbstractModel], life_span_seconds: Optional[float] = None, ): - """ - Inserts a given row or sets of rows into the table + """Inserts a given record or list of records into the redis. + + Can add a single record or multiple records into redis. + The records must be instances of this class. i.e. a `Book` + model can only insert `Book` instances. + + Args: + data: a model instance or list of model instances to put + into the redis store + life_span_seconds: the time-to-live in seconds of the records + to be inserted. If not specified, it defaults to the `Store`'s + life_span_seconds. """ store = cls.get_store() life_span = ( @@ -61,8 +78,17 @@ async def insert( async def update( cls, _id: Any, data: Dict[str, Any], life_span_seconds: Optional[float] = None ): - """ - Updates a given row or sets of rows in the table + """Updates the record whose primary key is `_id`. + + Updates the record of this Model in redis whose primary key is equal to the `_id` provided. + The record is partially updated from the `data`. + If `life_span_seconds` is provided, it will also update the time-to-live of + the record. + + Args: + _id: the primary key of record to be updated. + data: the new changes + life_span_seconds: the new time-to-live for the record """ store = cls.get_store() life_span = ( @@ -84,8 +110,13 @@ async def update( @classmethod async def delete(cls, ids: Union[Any, List[Any]]): - """ - deletes a given row or sets of rows in the table + """Removes a list of this Model's records from redis + + Removes all the records for the current Model whose primary keys + have been included in the `ids` passed. + + Args: + ids: list of primary keys of the records to remove """ store = cls.get_store() @@ -101,17 +132,31 @@ async def select( skip: int = 0, limit: Optional[int] = None, **kwargs, - ): - """ - Selects given rows or sets of rows in the table + ) -> Union["Model", Dict[str, Any]]: + """etrieves records of this Model from redis. + + Retrieves the records for this Model from redis. + + Args: + columns: the fields to return for each record + ids: the primary keys of the records to returns + skip: the number of records to skip. (default: 0) + limit: the maximum number of records to return + + Returns: + By default, it returns all records that belong to current Model. + + If `ids` are specified, it returns only records whose primary keys + have been listed in `ids`. + If `skip` and `limit` are specified WITHOUT `ids`, a slice of + all records are returned. - However, if `limit` is set, the number of items - returned will be less or equal to `limit`. - `skip` defaults to 0. It is the number of items to skip. - `skip` is only relevant when limit is specified. + If `limit` and `ids` are specified, `limit` is ignored. - `skip` and `limit` are irrelevant when `ids` are provided. + If `columns` are specified, a list of dictionaries containing only + the fields specified in `columns` is returned. Otherwise, instances + of the current Model are returned. """ if columns is None and ids is None: response = await select_all_fields_all_ids( diff --git a/pydantic_redis/asyncio/store.py b/pydantic_redis/asyncio/store.py index 85f30b58..599bbcbd 100644 --- a/pydantic_redis/asyncio/store.py +++ b/pydantic_redis/asyncio/store.py @@ -1,4 +1,13 @@ -"""Module containing the store class for async io""" +"""Exposes the `Store` for managing a collection of asynchronous Model's. + +Stores represent a collection of different kinds of records saved in +a redis database. They only expose records whose `Model`'s have been +registered in them. Thus redis server can have multiple stores each +have a different image of the actual data in redis. + +A model must be registered with a store before it can interact with +a redis database. +""" from typing import Dict, Type, TYPE_CHECKING from redis import asyncio as redis @@ -10,14 +19,28 @@ class Store(AbstractStore): - """ - A store that allows a declarative way of querying for data in redis + """Manages a collection of Model's, connecting them to a redis database + + A Model can only interact with a redis database when it is registered + with a `Store` that is connected to that database. + + Attributes: + models (Dict[str, Type[pydantic_redis.syncio.Model]]): a mapping of registered `Model`'s, with the keys being the + Model name + name (str): the name of this Store + redis_config (pydantic_redis.syncio.RedisConfig): the configuration for connecting to a redis database + redis_store (Optional[redis.Redis]): an Redis instance associated with this store (default: None) + life_span_in_seconds (Optional[int]): the default time-to-live for the records inserted in this store + (default: None) """ models: Dict[str, Type["Model"]] = {} def _connect_to_redis(self) -> redis.Redis: - """Connects the store to redis, returning a proper connection""" + """Connects the store to redis. + + See the base class. + """ return redis.from_url( self.redis_config.redis_url, encoding=self.redis_config.encoding, diff --git a/pydantic_redis/config.py b/pydantic_redis/config.py new file mode 100644 index 00000000..b9c88676 --- /dev/null +++ b/pydantic_redis/config.py @@ -0,0 +1,43 @@ +"""Exposes the configuration for connecting to a redis database. +""" +from typing import Optional + +from pydantic import BaseModel + + +class RedisConfig(BaseModel): + """Configuration for connecting to redis database. + + Inorder to connect to a redis database, there are a number of + configurations that are needed including the server's host address + and port. `RedisConfig` computes a redis-url similar to + `redis://:password@host:self.port/db` + + Attributes: + host (str): the host address where the redis server is found (default: 'localhost'). + port (int): the port on which the redis server is running (default: 6379). + db (int): the redis database identifier (default: 0). + password (Optional[int]): the password for connecting to the + redis server (default: None). + ssl (bool): whether the connection to the redis server is to be via TLS (default: False) + encoding: (Optional[str]): the string encoding used with the redis database + (default: utf-8) + """ + + host: str = "localhost" + port: int = 6379 + db: int = 0 + password: Optional[str] = None + ssl: bool = False + encoding: Optional[str] = "utf-8" + + @property + def redis_url(self) -> str: + """a redis URL of form `redis://:password@host:port/db`. (`rediss://..` if TLS).""" + proto = "rediss" if self.ssl else "redis" + if self.password is None: + return f"{proto}://{self.host}:{self.port}/{self.db}" + return f"{proto}://:{self.password}@{self.host}:{self.port}/{self.db}" + + class Config: + orm_mode = True diff --git a/pydantic_redis/syncio/__init__.py b/pydantic_redis/syncio/__init__.py index e7efcd3b..09ab5db6 100644 --- a/pydantic_redis/syncio/__init__.py +++ b/pydantic_redis/syncio/__init__.py @@ -1,9 +1,30 @@ -"""Synchronous version of pydantic-redis ORM +"""Synchronous API for pydantic-redis ORM. +Typical usage example: + +```python +# from pydantic_redis import Store, Model, RedisConfig +from pydantic_redis.syncio import Store, Model, RedisConfig + +class Book(Model): + _primary_key_field = 'title' + title: str + +if __name__ == '__main__': + store = Store(name="sample", redis_config=RedisConfig()) + store.register_model(Book) + + Book.insert(Book(title="Oliver Twist", author="Charles Dickens")) + Book.update( + _id="Oliver Twist", data={"author": "Jane Austen"}, life_span_seconds=3600 + ) + results = Book.select() + Book.delete(ids=["Oliver Twist", "Great Expectations"]) +``` """ from .model import Model from .store import Store -from .._shared.config import RedisConfig +from ..config import RedisConfig __all__ = [Model, Store, RedisConfig] diff --git a/pydantic_redis/syncio/model.py b/pydantic_redis/syncio/model.py index e5abcf95..f525d91d 100644 --- a/pydantic_redis/syncio/model.py +++ b/pydantic_redis/syncio/model.py @@ -1,4 +1,8 @@ -"""Module containing the model classes""" +"""Exposes the Base `Model` class for creating custom synchronous models. + +This module contains the `Model` class which should be inherited when +creating model's for use in the synchronous API of pydantic-redis +""" from typing import Optional, List, Any, Union, Dict from .._shared.model import AbstractModel @@ -17,7 +21,11 @@ class Model(AbstractModel): """ - The section in the store that saves rows of the same kind + The Base class for all Synchronous models. + + Inherit this class when creating a new model. + The new model should have `_primary_key_field` defined. + Any interaction with redis is done through `Model`'s. """ _store: Store @@ -28,8 +36,18 @@ def insert( data: Union[List[AbstractModel], AbstractModel], life_span_seconds: Optional[float] = None, ): - """ - Inserts a given row or sets of rows into the table + """Inserts a given record or list of records into the redis. + + Can add a single record or multiple records into redis. + The records must be instances of this class. i.e. a `Book` + model can only insert `Book` instances. + + Args: + data: a model instance or list of model instances to put + into the redis store + life_span_seconds: the time-to-live in seconds of the records + to be inserted. If not specified, it defaults to the `Store`'s + life_span_seconds. """ store = cls.get_store() @@ -61,8 +79,17 @@ def insert( def update( cls, _id: Any, data: Dict[str, Any], life_span_seconds: Optional[float] = None ): - """ - Updates a given row or sets of rows in the table + """Updates the record whose primary key is `_id`. + + Updates the record of this Model in redis whose primary key is equal to the `_id` provided. + The record is partially updated from the `data`. + If `life_span_seconds` is provided, it will also update the time-to-live of + the record. + + Args: + _id: the primary key of record to be updated. + data: the new changes + life_span_seconds: the new time-to-live for the record """ store = cls.get_store() life_span = ( @@ -84,8 +111,13 @@ def update( @classmethod def delete(cls, ids: Union[Any, List[Any]]): - """ - deletes a given row or sets of rows in the table + """Removes a list of this Model's records from redis + + Removes all the records for the current Model whose primary keys + have been included in the `ids` passed. + + Args: + ids: list of primary keys of the records to remove """ store = cls.get_store() with store.redis_store.pipeline() as pipeline: @@ -100,16 +132,31 @@ def select( skip: int = 0, limit: Optional[int] = None, **kwargs, - ): - """ - Selects given rows or sets of rows in the table + ) -> Union["Model", Dict[str, Any]]: + """Retrieves records of this Model from redis. + + Retrieves the records for this Model from redis. + + Args: + columns: the fields to return for each record + ids: the primary keys of the records to returns + skip: the number of records to skip. (default: 0) + limit: the maximum number of records to return + + Returns: + By default, it returns all records that belong to current Model. + + If `ids` are specified, it returns only records whose primary keys + have been listed in `ids`. + + If `skip` and `limit` are specified WITHOUT `ids`, a slice of + all records are returned. - However, if `limit` is set, the number of items - returned will be less or equal to `limit`. - `skip` defaults to 0. It is the number of items to skip. - `skip` is only relevant when limit is specified. + If `limit` and `ids` are specified, `limit` is ignored. - `skip` and `limit` are irrelevant when `ids` are provided. + If `columns` are specified, a list of dictionaries containing only + the fields specified in `columns` is returned. Otherwise, instances + of the current Model are returned. """ if columns is None and ids is None: response = select_all_fields_all_ids(model=cls, skip=skip, limit=limit) diff --git a/pydantic_redis/syncio/store.py b/pydantic_redis/syncio/store.py index a4a6e4f6..8cd1b71c 100644 --- a/pydantic_redis/syncio/store.py +++ b/pydantic_redis/syncio/store.py @@ -1,4 +1,13 @@ -"""Module containing the store classes for sync io""" +"""Exposes the `Store` class for managing a collection of Model's. + +Stores represent a collection of different kinds of records saved in +a redis database. They only expose records whose `Model`'s have been +registered in them. Thus redis server can have multiple stores each +have a different image of the actual data in redis. + +A model must be registered with a store before it can interact with +a redis database. +""" from typing import Dict, Type, TYPE_CHECKING import redis @@ -10,14 +19,28 @@ class Store(AbstractStore): - """ - A store that allows a declarative way of querying for data in redis + """Manages a collection of Model's, connecting them to a redis database + + A Model can only interact with a redis database when it is registered + with a `Store` that is connected to that database. + + Attributes: + models (Dict[str, Type[pydantic_redis.syncio.Model]]): a mapping of registered `Model`'s, with the keys being the + Model name + name (str): the name of this Store + redis_config (pydantic_redis.syncio.RedisConfig): the configuration for connecting to a redis database + redis_store (Optional[redis.Redis]): an Redis instance associated with this store (default: None) + life_span_in_seconds (Optional[int]): the default time-to-live for the records inserted in this store + (default: None) """ models: Dict[str, Type["Model"]] = {} def _connect_to_redis(self) -> redis.Redis: - """Connects the store to redis, returning a proper connection""" + """Connects the store to redis. + + See base class. + """ return redis.from_url( self.redis_config.redis_url, encoding=self.redis_config.encoding, diff --git a/test/test_pydantic_redis.py b/test/test_pydantic_redis.py index 00ab4d9c..af5eec7f 100644 --- a/test/test_pydantic_redis.py +++ b/test/test_pydantic_redis.py @@ -4,7 +4,7 @@ import pytest -from pydantic_redis._shared.config import RedisConfig # noqa +from pydantic_redis.config import RedisConfig # noqa from pydantic_redis._shared.model.prop_utils import NESTED_MODEL_PREFIX # noqa from pydantic_redis._shared.utils import strip_leading # noqa from pydantic_redis.syncio.model import Model