+
+```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!}
+```
+
+#### 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
+ * @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/reference.md b/docs/reference.md
new file mode 100644
index 00000000..8dcfa5cb
--- /dev/null
+++ b/docs/reference.md
@@ -0,0 +1 @@
+::: pydantic_redis
\ No newline at end of file
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
new file mode 100644
index 00000000..0ae6519e
--- /dev/null
+++ b/docs/tutorials/asynchronous/models.md
@@ -0,0 +1,80 @@
+# 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.asyncio's `Model`
+
+!!! warning
+ The imports are from `pydantic_redis.asyncio` NOT `pydantic_redis`
+
+```Python hl_lines="6"
+{!../docs_src/tutorials/asynchronous/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="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/tutorials/intro.md b/docs/tutorials/intro.md
new file mode 100644
index 00000000..1eeda945
--- /dev/null
+++ b/docs/tutorials/intro.md
@@ -0,0 +1,65 @@
+# 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.
+
+!!! info
+ 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.
+
+## 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.
+
+!!! 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
+
+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/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/tutorials/synchronous/insert.md b/docs/tutorials/synchronous/insert.md
new file mode 100644
index 00000000..e62dd706
--- /dev/null
+++ b/docs/tutorials/synchronous/insert.md
@@ -0,0 +1,84 @@
+# 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.
+
+!!! 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="20-23"
+{!../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.
+
+!!! 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!}
+```
+
+## 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="30-36"
+{!../docs_src/tutorials/synchronous/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/synchronous/list-of-nested-models.md b/docs/tutorials/synchronous/list-of-nested-models.md
new file mode 100644
index 00000000..4a1d82c5
--- /dev/null
+++ b/docs/tutorials/synchronous/list-of-nested-models.md
@@ -0,0 +1,153 @@
+# 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.
+
+!!! 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!}
+```
+
+## 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.
+
+!!! 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!}
+```
+
+## 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="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
+
+!!! 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="38-60"
+{!../docs_src/tutorials/synchronous/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="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:
+
+!!! 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/synchronous/models.md b/docs/tutorials/synchronous/models.md
new file mode 100644
index 00000000..4635b4c4
--- /dev/null
+++ b/docs/tutorials/synchronous/models.md
@@ -0,0 +1,77 @@
+# 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.
+
+!!! example
+ 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
+
+!!! 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!}
+```
+
+## 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/synchronous/nested-models.md b/docs/tutorials/synchronous/nested-models.md
new file mode 100644
index 00000000..c6416fc4
--- /dev/null
+++ b/docs/tutorials/synchronous/nested-models.md
@@ -0,0 +1,149 @@
+# 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.
+
+!!! 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!}
+```
+
+## 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.
+
+!!! example
+ 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.
+
+!!! 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!}
+```
+
+## 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
+
+!!! 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!}
+```
+
+## 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="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
+
+!!! 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!}
+```
+
+## 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/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/tutorials/synchronous/tuple-of-nested-models.md b/docs/tutorials/synchronous/tuple-of-nested-models.md
new file mode 100644
index 00000000..45210fa2
--- /dev/null
+++ b/docs/tutorials/synchronous/tuple-of-nested-models.md
@@ -0,0 +1,151 @@
+# 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.
+
+!!! 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!}
+```
+
+## 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.
+
+!!! 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="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.
+
+!!! 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!}
+```
+
+## 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
+
+!!! 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="25-35"
+{!../docs_src/tutorials/synchronous/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="37-38"
+{!../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="40-49"
+{!../docs_src/tutorials/synchronous/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/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/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/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/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/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/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/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/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/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/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
new file mode 100644
index 00000000..3f856184
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,71 @@
+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:
+ - tutorials/intro.md
+ - Synchronous API:
+ - tutorials/synchronous/models.md
+ - 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
+ - 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
+ - change-log.md
+
+markdown_extensions:
+ - admonition
+ - pymdownx.highlight:
+ anchor_linenums: true
+ - pymdownx.inlinehilite
+ - pymdownx.snippets
+ - 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 8873263a..0d09af08 100644
--- a/pydantic_redis/__init__.py
+++ b/pydantic_redis/__init__.py
@@ -1,4 +1,15 @@
-"""Entry point for redisy"""
+"""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
+"""
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..cfecf191
--- /dev/null
+++ b/pydantic_redis/_shared/__init__.py
@@ -0,0 +1,7 @@
+"""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.
+"""
diff --git a/pydantic_redis/shared/lua_scripts.py b/pydantic_redis/_shared/lua_scripts.py
similarity index 89%
rename from pydantic_redis/shared/lua_scripts.py
rename to 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
new file mode 100644
index 00000000..c6d48444
--- /dev/null
+++ b/pydantic_redis/_shared/model/__init__.py
@@ -0,0 +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
new file mode 100644
index 00000000..95f3c3f9
--- /dev/null
+++ b/pydantic_redis/_shared/model/base.py
@@ -0,0 +1,258 @@
+"""Exposes the Base `Model` common to both async and sync APIs
+
+"""
+import typing
+from typing import Dict, Tuple, Any, Type, Union, List, Optional
+
+from pydantic import BaseModel
+
+from pydantic_redis._shared.utils import (
+ typing_get_origin,
+ typing_get_args,
+ from_any_to_valid_redis_type,
+ from_dict_to_key_value_list,
+ from_bytes_to_str,
+ from_str_or_bytes_to_any,
+)
+
+
+from ..store import AbstractStore
+
+
+class AbstractModel(BaseModel):
+ """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
+ _field_types: Dict[str, Any] = {}
+ _store: AbstractStore
+ _nested_model_tuple_fields: Dict[str, Tuple[Any, ...]] = {}
+ _nested_model_list_fields: Dict[str, Type["AbstractModel"]] = {}
+ _nested_model_fields: Dict[str, Type["AbstractModel"]] = {}
+
+ class Config:
+ arbitrary_types_allowed = True
+
+ @classmethod
+ def get_store(cls) -> AbstractStore:
+ """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):
+ """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):
+ """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):
+ """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 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]:
+ """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.
+
+ 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 = {}
+ cls._nested_model_tuple_fields = {}
+ cls._nested_model_fields = {}
+
+ for field, field_type in cls._field_types.items():
+ try:
+ # In case the annotation is Optional, an alias of Union[X, None], extract the X
+ is_generic = hasattr(field_type, "__origin__")
+ if (
+ is_generic
+ and typing_get_origin(field_type) == Union
+ and typing_get_args(field_type)[-1] == None.__class__
+ ):
+ field_type = typing_get_args(field_type)[0]
+ is_generic = hasattr(field_type, "__origin__")
+
+ if (
+ is_generic
+ and typing_get_origin(field_type) in (List, list)
+ and issubclass(typing_get_args(field_type)[0], AbstractModel)
+ ):
+ cls._nested_model_list_fields[field] = typing_get_args(field_type)[
+ 0
+ ]
+
+ elif (
+ is_generic
+ and typing_get_origin(field_type) in (Tuple, tuple)
+ and any(
+ [
+ issubclass(v, AbstractModel)
+ for v in typing_get_args(field_type)
+ ]
+ )
+ ):
+ cls._nested_model_tuple_fields[field] = typing_get_args(field_type)
+
+ elif issubclass(field_type, AbstractModel):
+ cls._nested_model_fields[field] = field_type
+
+ except (TypeError, AttributeError):
+ pass
+
+ @classmethod
+ def serialize_partially(cls, data: Optional[Dict[str, Any]]) -> Dict[str, Any]:
+ """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]:
+ """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
+
+ Args:
+ data: flattened list of key-values or dictionary of data to cast.
+ Keeping it as potentially a dictionary ensures backward compatibility.
+
+ Returns:
+ the dictionary of properly parsed key-values.
+ """
+ if isinstance(data, dict):
+ # for backward compatibility
+ data = from_dict_to_key_value_list(data)
+
+ parsed_dict = {}
+
+ nested_model_list_fields = cls.get_nested_model_list_fields()
+ nested_model_tuple_fields = cls.get_nested_model_tuple_fields()
+ nested_model_fields = cls.get_nested_model_fields()
+
+ for i in range(0, len(data), 2):
+ key = from_bytes_to_str(data[i])
+ field_type = cls._field_types.get(key)
+ 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 = _cast_lists(value, nested_model_list_fields[key])
+
+ elif key in nested_model_tuple_fields and value is not None:
+ value = _cast_tuples(value, nested_model_tuple_fields[key])
+
+ elif key in nested_model_fields and value is not None:
+ value = _cast_to_model(value=value, model=nested_model_fields[key])
+
+ parsed_dict[key] = value
+
+ return parsed_dict
+
+
+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 _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(_type, value):
+ if issubclass(field_type, AbstractModel) and value is not None:
+ value = field_type(**field_type.deserialize_partially(value))
+ items.append(value)
+
+ return tuple(items)
+
+
+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
new file mode 100644
index 00000000..c3905590
--- /dev/null
+++ b/pydantic_redis/_shared/model/delete_utils.py
@@ -0,0 +1,38 @@
+"""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_redis_key, get_model_index_key
+
+
+def delete_on_pipeline(
+ model: Type[AbstractModel], pipeline: Union[Pipeline, AioPipeline], ids: List[str]
+):
+ """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 = []
+
+ if isinstance(ids, list):
+ primary_keys = ids
+ elif ids is not None:
+ primary_keys = [ids]
+
+ names = [
+ 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_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
similarity index 60%
rename from pydantic_redis/shared/model/insert_utils.py
rename to 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
new file mode 100644
index 00000000..9256a247
--- /dev/null
+++ b/pydantic_redis/_shared/model/prop_utils.py
@@ -0,0 +1,69 @@
+"""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
+
+from .base import AbstractModel
+
+NESTED_MODEL_PREFIX = "__"
+NESTED_MODEL_LIST_FIELD_PREFIX = "___"
+NESTED_MODEL_TUPLE_FIELD_PREFIX = "____"
+
+
+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
+
+ Returns:
+ the primary key internally used for `primary_key_value` of `model`
+ """
+ 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`.
+
+ 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
+ """
+ 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_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.
+
+ 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
new file mode 100644
index 00000000..a1f5e77f
--- /dev/null
+++ b/pydantic_redis/_shared/model/select_utils.py
@@ -0,0 +1,213 @@
+"""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_redis_keys_regex,
+ get_redis_key_prefix,
+ get_model_index_key,
+)
+
+
+from .base import AbstractModel
+
+
+def get_select_fields(model: Type[AbstractModel], columns: List[str]) -> List[str]:
+ """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
+ `__` 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()
+ nested_model_tuple_fields = model.get_nested_model_tuple_fields()
+ nested_model_fields = model.get_nested_model_fields()
+
+ for col in columns:
+
+ if col in nested_model_fields:
+ col = f"{NESTED_MODEL_PREFIX}{col}"
+ elif col in nested_model_list_fields:
+ col = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{col}"
+ elif col in nested_model_tuple_fields:
+ col = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{col}"
+
+ fields.append(col)
+ return fields
+
+
+def select_all_fields_all_ids(
+ model: Type[AbstractModel],
+ skip: int = 0,
+ limit: Optional[int] = None,
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+ """Retrieves 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 isinstance(limit, int):
+ return _select_all_ids_all_fields_paginated(model=model, limit=limit, skip=skip)
+ else:
+ 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)
+
+
+def select_all_fields_some_ids(
+ model: Type[AbstractModel], ids: List[str]
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+ """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)
+
+
+def select_some_fields_all_ids(
+ model: Type[AbstractModel],
+ fields: List[str],
+ skip: int = 0,
+ limit: Optional[int] = None,
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+ """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.
+
+ 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)
+
+ if isinstance(limit, int):
+ return _select_some_fields_all_ids_paginated(
+ model=model, columns=columns, limit=limit, skip=skip
+ )
+ else:
+ 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)
+
+
+def select_some_fields_some_ids(
+ model: Type[AbstractModel], fields: List[str], ids: List[str]
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+ """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()
+ return store.select_some_fields_for_some_ids_script(keys=keys, args=columns)
+
+
+def parse_select_response(
+ model: Type[AbstractModel], response: List[List], as_models: bool
+):
+ """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}]
+
+ Returns:
+ If `as_models` is true, list of models else list of dicts
+ """
+ if len(response) == 0:
+ return None
+
+ if as_models:
+ return [
+ model(**model.deserialize_partially(record))
+ for record in response
+ if record != []
+ ]
+
+ return [model.deserialize_partially(record) for record in response if record != []]
+
+
+def _select_all_ids_all_fields_paginated(
+ model: Type[AbstractModel], limit: int, skip: Optional[int]
+):
+ """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_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)
+
+
+def _select_some_fields_all_ids_paginated(
+ model: Type[AbstractModel], columns: List[str], limit: int, skip: int
+):
+ """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_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
similarity index 74%
rename from pydantic_redis/shared/store.py
rename to 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
new file mode 100644
index 00000000..5ecc2130
--- /dev/null
+++ b/pydantic_redis/_shared/utils.py
@@ -0,0 +1,143 @@
+"""Exposes common utilities.
+
+"""
+import typing
+from typing import Any, Tuple, Optional, Union, Dict, Callable, Type, List
+
+import orjson
+
+
+def strip_leading(word: str, substring: str) -> str:
+ """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) :]
+ return word
+
+
+def typing_get_args(v: Any) -> Tuple[Any, ...]:
+ """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:
+ return getattr(v, "__args__", ()) if v is not typing.Generic else typing.Generic
+
+
+def typing_get_origin(v: Any) -> Optional[Any]:
+ """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:
+ return getattr(v, "__origin__", None)
+
+
+def from_bytes_to_str(value: Union[str, bytes]) -> 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 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:
+ return orjson.loads(value)
+ return value
+
+
+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: 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()
+ return obj
+
+
+def from_dict_to_key_value_list(data: Dict[str, Any]) -> List[Any]:
+ """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():
+ parsed_list.append(k)
+ parsed_list.append(v)
+
+ return parsed_list
diff --git a/pydantic_redis/asyncio/__init__.py b/pydantic_redis/asyncio/__init__.py
index 168f33ff..1da65b74 100644
--- a/pydantic_redis/asyncio/__init__.py
+++ b/pydantic_redis/asyncio/__init__.py
@@ -1,7 +1,34 @@
-"""Package containing the async 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 ea04ddea..5f31a19f 100644
--- a/pydantic_redis/asyncio/model.py
+++ b/pydantic_redis/asyncio/model.py
@@ -1,12 +1,13 @@
-"""Module containing the model classes"""
-from typing import Optional, List, Any, Union, Dict, Tuple, Type
+"""Exposes the Base `Model` class for creating custom asynchronous models.
-import redis.asyncio
+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 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,12 +16,15 @@
)
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):
- """
- 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
@@ -31,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 = (
@@ -64,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 = (
@@ -87,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()
@@ -104,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 4c24ecae..599bbcbd 100644
--- a/pydantic_redis/asyncio/store.py
+++ b/pydantic_redis/asyncio/store.py
@@ -1,23 +1,46 @@
-"""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
-from ..shared.store import AbstractStore
+from .._shared.store import AbstractStore
if TYPE_CHECKING:
from .model import Model
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/shared/__init__.py b/pydantic_redis/shared/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/pydantic_redis/shared/config.py b/pydantic_redis/shared/config.py
deleted file mode 100644
index 73081cb0..00000000
--- a/pydantic_redis/shared/config.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""Module containing the main config classes"""
-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/model/__init__.py b/pydantic_redis/shared/model/__init__.py
deleted file mode 100644
index 4b43caa9..00000000
--- a/pydantic_redis/shared/model/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .base import AbstractModel
diff --git a/pydantic_redis/shared/model/base.py b/pydantic_redis/shared/model/base.py
deleted file mode 100644
index 4b68698c..00000000
--- a/pydantic_redis/shared/model/base.py
+++ /dev/null
@@ -1,190 +0,0 @@
-"""Module containing the base model"""
-import typing
-from typing import Dict, Tuple, Any, Type, Union, List, Optional
-
-from pydantic import BaseModel
-
-from pydantic_redis.shared.utils import (
- typing_get_origin,
- typing_get_args,
- from_any_to_str_or_bytes,
- from_dict_to_key_value_list,
- from_bytes_to_str,
- from_str_or_bytes_to_any,
-)
-
-
-from ..store import AbstractStore
-
-
-class AbstractModel(BaseModel):
- """
- An abstract class to help with typings for Model class
- """
-
- _primary_key_field: str
- _field_types: Dict[str, Any] = {}
- _store: AbstractStore
- _nested_model_tuple_fields: Dict[str, Tuple[Any, ...]] = {}
- _nested_model_list_fields: Dict[str, Type["AbstractModel"]] = {}
- _nested_model_fields: Dict[str, Type["AbstractModel"]] = {}
-
- class Config:
- arbitrary_types_allowed = True
-
- @classmethod
- def get_store(cls) -> AbstractStore:
- """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"""
- return cls._nested_model_tuple_fields
-
- @classmethod
- def get_nested_model_list_fields(cls):
- """Returns the fields that have list of nested models"""
- return cls._nested_model_list_fields
-
- @classmethod
- def get_nested_model_fields(cls):
- """Returns the fields that have nested models"""
- return cls._nested_model_fields
-
- @classmethod
- def get_primary_key_field(cls):
- """Gets the protected _primary_key_field"""
- return cls._primary_key_field
-
- @classmethod
- def get_field_types(cls) -> Dict[str, Any]:
- """Returns the fields types of this 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"""
- cls._field_types = typing.get_type_hints(cls)
-
- cls._nested_model_list_fields = {}
- cls._nested_model_tuple_fields = {}
- cls._nested_model_fields = {}
-
- for field, field_type in cls._field_types.items():
- try:
- # In case the annotation is Optional, an alias of Union[X, None], extract the X
- is_generic = hasattr(field_type, "__origin__")
- if (
- is_generic
- and typing_get_origin(field_type) == Union
- and typing_get_args(field_type)[-1] == None.__class__
- ):
- field_type = typing_get_args(field_type)[0]
- is_generic = hasattr(field_type, "__origin__")
-
- if (
- is_generic
- and typing_get_origin(field_type) in (List, list)
- and issubclass(typing_get_args(field_type)[0], AbstractModel)
- ):
- cls._nested_model_list_fields[field] = typing_get_args(field_type)[
- 0
- ]
-
- elif (
- is_generic
- and typing_get_origin(field_type) in (Tuple, tuple)
- and any(
- [
- issubclass(v, AbstractModel)
- for v in typing_get_args(field_type)
- ]
- )
- ):
- cls._nested_model_tuple_fields[field] = typing_get_args(field_type)
-
- elif issubclass(field_type, AbstractModel):
- cls._nested_model_fields[field] = field_type
-
- except (TypeError, AttributeError):
- pass
-
- @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()}
-
- @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.
-
- 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}]
-
- Note: For backward compatibility, data can also be a dict.
- """
- if isinstance(data, dict):
- # for backward compatibility
- data = from_dict_to_key_value_list(data)
-
- parsed_dict = {}
-
- nested_model_list_fields = cls.get_nested_model_list_fields()
- nested_model_tuple_fields = cls.get_nested_model_tuple_fields()
- nested_model_fields = cls.get_nested_model_fields()
-
- for i in range(0, len(data), 2):
- key = from_bytes_to_str(data[i])
- field_type = cls._field_types.get(key)
- 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
- )
-
- 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
- )
-
- elif key in nested_model_fields and value is not None:
- value = deserialize_nested_model(
- field_type=nested_model_fields[key], value=value
- )
-
- 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 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"""
- items = []
- for field_type, value in zip(field_types, value):
- if issubclass(field_type, AbstractModel) and value is not None:
- value = field_type(**field_type.deserialize_partially(value))
- items.append(value)
-
- 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))
diff --git a/pydantic_redis/shared/model/delete_utils.py b/pydantic_redis/shared/model/delete_utils.py
deleted file mode 100644
index 19709111..00000000
--- a/pydantic_redis/shared/model/delete_utils.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Module containing common functionality for deleting records"""
-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
-
-
-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
- """
- primary_keys = []
-
- if isinstance(ids, list):
- primary_keys = ids
- elif ids is not None:
- primary_keys = [ids]
-
- names = [
- get_primary_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)
- pipeline.zrem(table_index_key, *names)
diff --git a/pydantic_redis/shared/model/prop_utils.py b/pydantic_redis/shared/model/prop_utils.py
deleted file mode 100644
index 335b3dda..00000000
--- a/pydantic_redis/shared/model/prop_utils.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""Module containing utils for getting properties of the Model"""
-
-from typing import Type, Any
-
-from .base import AbstractModel
-
-NESTED_MODEL_PREFIX = "__"
-NESTED_MODEL_LIST_FIELD_PREFIX = "___"
-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_table_prefix(model: Type[AbstractModel]):
- """
- Returns the prefix of the all the redis keys that are associated with this table
- """
- table_name = model.__name__.lower()
- return f"{table_name}_%&_"
-
-
-def get_table_keys_regex(model: Type[AbstractModel]):
- """
- Returns the table name regex to get all keys that belong to this table
- """
- return f"{get_table_prefix(model)}*"
-
-
-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"""
- 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
deleted file mode 100644
index e0b21416..00000000
--- a/pydantic_redis/shared/model/select_utils.py
+++ /dev/null
@@ -1,155 +0,0 @@
-"""Module containing the mixin functionality for selecting"""
-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,
-)
-
-
-from .base import AbstractModel
-
-
-def get_select_fields(model: Type[AbstractModel], columns: List[str]) -> List[str]:
- """
- 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
- """
- fields = []
- nested_model_list_fields = model.get_nested_model_list_fields()
- nested_model_tuple_fields = model.get_nested_model_tuple_fields()
- nested_model_fields = model.get_nested_model_fields()
-
- for col in columns:
-
- if col in nested_model_fields:
- col = f"{NESTED_MODEL_PREFIX}{col}"
- elif col in nested_model_list_fields:
- col = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{col}"
- elif col in nested_model_tuple_fields:
- col = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{col}"
-
- fields.append(col)
- return fields
-
-
-def select_all_fields_all_ids(
- model: Type[AbstractModel],
- 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
-
- 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 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)
- args = [table_keys_regex]
- store = model.get_store()
- return store.select_all_fields_for_all_ids_script(args=args)
-
-
-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)
- keys = [f"{table_prefix}{key}" for key in ids]
- store = model.get_store()
- return store.select_all_fields_for_some_ids_script(keys=keys)
-
-
-def select_some_fields_all_ids(
- model: Type[AbstractModel],
- fields: List[str],
- 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.
-
- 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.
- """
- columns = get_select_fields(model=model, columns=fields)
-
- if isinstance(limit, int):
- return _select_some_fields_all_ids_paginated(
- model=model, columns=columns, limit=limit, skip=skip
- )
- else:
- table_keys_regex = get_table_keys_regex(model=model)
- args = [table_keys_regex, *columns]
- store = model.get_store()
- return store.select_some_fields_for_all_ids_script(args=args)
-
-
-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)
- keys = [f"{table_prefix}{key}" for key in ids]
- columns = get_select_fields(model=model, columns=fields)
- store = model.get_store()
- return store.select_some_fields_for_some_ids_script(keys=keys, args=columns)
-
-
-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 .
-
- 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}]
- """
- if len(response) == 0:
- return None
-
- if as_models:
- return [
- model(**model.deserialize_partially(record))
- for record in response
- if record != []
- ]
-
- return [model.deserialize_partially(record) for record in response if record != []]
-
-
-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"""
- if skip is None:
- skip = 0
- table_index_key = get_table_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)
-
-
-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"""
- if skip is None:
- skip = 0
- table_index_key = get_table_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/utils.py b/pydantic_redis/shared/utils.py
deleted file mode 100644
index a76de887..00000000
--- a/pydantic_redis/shared/utils.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""Module containing common utilities"""
-import typing
-from typing import Any, Tuple, Optional, Union, Dict, Callable, Type, List
-
-import orjson
-
-
-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
- """
- if word.startswith(substring):
- return word[len(substring) :]
- return word
-
-
-def typing_get_args(v: Any) -> Tuple[Any, ...]:
- """Gets the __args__ of the annotations of a given typing"""
- try:
- return typing.get_args(v)
- except AttributeError:
- return getattr(v, "__args__", ()) if v is not typing.Generic else typing.Generic
-
-
-def typing_get_origin(v: Any) -> Optional[Any]:
- """Gets the __origin__ of the annotations of a given typing"""
- try:
- return typing.get_origin(v)
- except AttributeError:
- return getattr(v, "__origin__", None)
-
-
-def from_bytes_to_str(value: Union[str, bytes]) -> str:
- """Converts bytes to str"""
- 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"""
- if isinstance(value, (bytes, bytearray, memoryview)):
- return orjson.loads(value)
- elif isinstance(value, str) and field_type != str:
- return orjson.loads(value)
- return value
-
-
-def from_any_to_str_or_bytes(value: Any) -> Union[str, bytes]:
- """Converts arbitrary data into str or bytes"""
- if isinstance(value, str):
- return value
- return orjson.dumps(value, default=default_json_dump)
-
-
-def default_json_dump(obj):
- """Default JSON dump for orjson"""
- 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)
-
-
-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"""
- parsed_list = []
-
- for k, v in data.items():
- parsed_list.append(k)
- parsed_list.append(v)
-
- return parsed_list
diff --git a/pydantic_redis/syncio/__init__.py b/pydantic_redis/syncio/__init__.py
index 2af01c3f..09ab5db6 100644
--- a/pydantic_redis/syncio/__init__.py
+++ b/pydantic_redis/syncio/__init__.py
@@ -1,6 +1,30 @@
-"""Package containing the synchronous and thus default version of pydantic_redis"""
+"""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 25cbe545..f525d91d 100644
--- a/pydantic_redis/syncio/model.py
+++ b/pydantic_redis/syncio/model.py
@@ -1,10 +1,13 @@
-"""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 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,12 +16,16 @@
)
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):
"""
- 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
@@ -29,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()
@@ -62,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 = (
@@ -85,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:
@@ -101,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 0b36cc73..8cd1b71c 100644
--- a/pydantic_redis/syncio/store.py
+++ b/pydantic_redis/syncio/store.py
@@ -1,23 +1,46 @@
-"""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
-from ..shared.store import AbstractStore
+from .._shared.store import AbstractStore
if TYPE_CHECKING:
from .model import Model
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/requirements.txt b/requirements.txt
index 6e52ed5f..d023eccf 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -11,3 +11,7 @@ black==22.8.0
pre-commit
build
pytest-asyncio
+mkdocs
+mkdocstrings
+mkdocs-material
+mdx_include
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 a3715769..af5eec7f 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.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,