Repository: sopherapps/pydantic-redis (language: python)

Issue:
Enable Asyncio For Pydantic Redis
The fantastic [pydantic-aioredis](https://github.com/andrewthetechie/pydantic-aioredis) package enables an async API that closely resembles pydantic-redis. However, currently, it lacks some features that are already in pydantic-redis and even as the packages mature, the divide between the two is expected to increase.

Add an async API like:

```python
import asyncio
from datetime import date, datetime
from typing import Tuple, List, Dict

from pydantic_redis.asyncio import Store, Model, 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
    last_updated: datetime = datetime.utcnow()
    tags: List[str] = []
    meta: Dict[str, str] = {}
    in_stock: bool = True


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"], meta={"isbn": "63598943843898IN"}),
    Book(title="Great Expectations", author=authors["charles"], published_on=date(year=1220, month=4, day=4), rating=5,
         tags=["Classic"], meta={"isbn": "629389285uy", "description": "Some fancy book"}),
    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"], meta={"isbn": "787873ty"}),
    Book(title="Wuthering Heights", author=authors["jane"], published_on=date(year=1600, month=4, day=4), rating=4.0,
         tags=["Classic", "Romance"], last_updated=datetime.utcnow()),
]


async def run_async():
    store = Store(name="test", redis_config=RedisConfig())
    await store.redis_store.flushall()
    store.register_model(Author)
    store.register_model(Book)

    # clear the store
    await store.clear()

    # Insert books and their associated models into redis
    await Book.insert(books)

    # Select all books to view them. A list of Model instances will be returned
    # Will print [Book(title="Oliver Twist", author="Charles Dickens", ...), Book(...]
    all_books = await Book.select()
    print(all_books)

    # Or select some books
    some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
    print(some_books)  # Will print only those two books

    # Or select some authors
    # Will print Jane Austen even though you didn't explicitly insert her in the Author's collection
    some_authors = await Author.select(ids=["Jane Austen"])
    print(some_authors)

    # Or select some columns. THIS RETURNS DICTIONARIES not MODEL Instances
    # The Dictionaries have values in string form so you might need to do some extra work
    books_with_few_fields = await Book.select(columns=["author", "in_stock"])
    print(books_with_few_fields)  # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]

    # Update any book or library
    await Book.update(_id="Oliver Twist", data={"author": authors["jane"]})
    # You could even update a given author's details by nesting their new data in a book update
    updated_jane = Author(**authors["jane"].dict())
    updated_jane.active_years = (1999, 2008)
    await Book.update(_id="Oliver Twist", data={"author": updated_jane})
    # Trying to retrieve jane directly will return her with the new details
    # All other books that have Jane Austen as author will also have their data updated. (like a real relationship)
    new_jane = await Author.select(ids=["Jane Austen"])
    print(new_jane)

    # Delete any number of items
    await Book.delete(ids=["Oliver Twist", "Jane Eyre"])


asyncio.run(run_async())
```

This is especially useful for asyncio in [FastAPI](https://fastapi.tiangolo.com/)


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index efefa8a5..985cd862 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [Unreleased]
 
+## [0.3.0] - 2022-12-15
+
+### Added
+
+- Added asyncio support, to be got from the `pydantic_redis.asyncio` module
+
+### Changed
+
+- Moved the synchronous version to the `pydantic_redis.syncio` module, but kept its contents exposed in pydantic_redis
+  for backward-compatibility
+
+### Fixed
+
 ## [0.2.0] - 2022-12-15
 
 ### Added
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..c5126ea4
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,68 @@
+# Contributing to pydantic_redis
+
+We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
+
+- Reporting a bug
+- Discussing the current state of the code
+- Submitting a fix
+- Proposing new features
+- Becoming a maintainer
+
+## We Develop with Github
+
+We use github to host code, to track issues and feature requests, as well as accept pull requests.
+
+## We Use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow), So All Code Changes Happen Through Pull Requests
+
+Pull requests are the best way to propose changes to the codebase (we
+use [Github Flow](https://docs.github.com/en/get-started/quickstart/github-flow)). We actively welcome your pull
+requests:
+
+1. Fork the repo and create your branch from `master`.
+2. If you've added code that should be tested, add tests.
+3. If you've changed APIs, update the documentation.
+4. Ensure the test suite passes.
+5. Make sure your code lints.
+6. Issue that pull request!
+
+## Any contributions you make will be under the MIT Software Licenses
+
+In short, when you submit code changes, your submissions are understood to be under the
+same [MIT License](./LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern.
+
+## Report bugs using Github's [issues](https://github.com/sopherapps/pydantic_redis/issues)
+
+We use GitHub issues to track public bugs. Report a bug
+by [opening a new issue](https://github.com/sopherapps/pydantic_redis/issues); it's that easy!
+
+## Write bug reports with detail, background, and sample code
+
+[This is an example](http://stackoverflow.com/q/12488905/180626).
+Here's [another example from Craig Hockenberry](http://www.openradar.me/11905408).
+
+**Great Bug Reports** tend to have:
+
+- A quick summary and/or background
+- Steps to reproduce
+  - Be specific!
+  - Give sample code if you can.
+- What you expected would happen
+- What actually happens
+- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
+
+People *love* thorough bug reports. I'm not even kidding.
+
+## Use a Consistent Coding Style
+
+* Use [black](https://pypi.org/project/black/)
+
+## License
+
+By contributing, you agree that your contributions will be licensed under its MIT License.
+
+## References
+
+This document was adapted from [a gist by Brian A. Danielak](https://gist.github.com/briandk/3d2e8b3ec8daf5a27a62) which
+was originally adapted from
+the open-source contribution guidelines
+for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md)
diff --git a/README.md b/README.md
index a1b6fcc2..df9256ae 100644
--- a/README.md
+++ b/README.md
@@ -4,15 +4,23 @@
 
 A simple declarative ORM for Redis
 
-*Note: For a faster ORM with similar features, consider [orredis](https://github.com/sopherapps/orredis) which, under the hood, is built in rust*
-
 ## Main Dependencies
 
 - [Python +3.6](https://www.python.org)
 - [redis](https://pypi.org/project/redis/)
 - [pydantic](https://github.com/samuelcolvin/pydantic/)
 
-## Getting Started
+## Most Notable Features
+
+- Define business domain objects as [pydantic](https://github.com/samuelcolvin/pydantic/) and automatically get ability
+  to save them as is in [redis](https://pypi.org/project/redis/) with an intuitive API of `insert`, `update`, `delete`,
+  `select`
+- Maintain simple relationships between domain objects by simply nesting them either as single objects or lists, or tuples.
+  Any direct or indirect update to a nested object will automatically reflect in all parent objects that have it nested in
+  them when queried again from redis.
+- Both synchronous and asynchronous APIs available.
+
+## Getting Started (Synchronous Version)
 
 - Install the package
 
@@ -20,31 +28,45 @@ A simple declarative ORM for Redis
   pip install pydantic-redis
   ```
 
-- Import the `Store`, the `RedisConfig` and the `Model` classes and use accordingly
+- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis` and use accordingly
 
 ```python
 from datetime import date
-from typing import Tuple, List
+from typing import Tuple, List, Optional
 from pydantic_redis import RedisConfig, Model, Store
 
 
-# Create models as you would create pydantic models i.e. using typings
-# the _primary_key_field is mandatory for ease of data querying and updating
 class Author(Model):
+  """
+  An Author model, just like a pydantic model with appropriate type annotations
+  NOTE: The `_primary_key_field` is mandatory
+  """
   _primary_key_field: str = 'name'
   name: str
   active_years: Tuple[int, int]
 
 
-# Create models as you would create pydantic models i.e. using typings
-# the _primary_key_field is mandatory for ease of data querying and updating
 class Book(Model):
+  """
+  A Book model.
+  
+  Models can have the following field types
+  - The usual i.e. float, int, dict, list, date, str, dict, Optional etc as long as they are serializable by orjson
+  - Nested models e.g. `author: Author` or `author: Optional[Author]`
+  - List of nested models e.g. `authors: List[Author]` or `authors: Optional[List[Author]]`
+  - Tuples including nested models e.g. `access_log: Tuple[Author, date]` or `access_log: Optional[Tuple[Author, date]]]`
+  
+  NOTE: 1. Any nested model whether plain or in a list or tuple will automatically inserted into the redis store
+       when the parent model is inserted. e.g. a Book with an author field, when inserted, will also insert
+       the author. The author can then be queried directly if that's something one wishes to do.
+       
+       2. When a parent model is inserted with a nested model instance that already exists, the older nested model 
+       instance is overwritten. This is one way of updating nested models. All parent models that contain that nested
+       model instance will see the change. 
+  """
   _primary_key_field: str = 'title'
   title: str
   author: Author
-  # You can even nest models. they will be automatically inserted into their own collection
-  # if they don't exist. Any update to these nested models will reflect in future data; thus no stale data.
-  # consider this as an actual child-parent relationship
   rating: float
   published_on: date
   tags: List[str] = []
@@ -52,21 +74,37 @@ class Book(Model):
 
 
 class Library(Model):
-  # the _primary_key_field is mandatory
+  """
+  A library model.
+  
+  It shows a number of complicated nested models.
+  
+  About Nested Model Performance
+  ---
+  To minimize the performance penalty for nesting models, we use REDIS EVALSHA to eagerly load the nested models
+  before the response is returned to the client. This ensures that only ONE network call is made every time.
+  """
   _primary_key_field: str = 'name'
   name: str
   address: str
+  books: List[Book] = None
+  lost: Optional[List[Book]] = None
+  popular: Optional[Tuple[Book, Book]] = None
+  new: Tuple[Book, Author, int] = None
+
 
+# Create the store
+store = Store(
+  name='some_name', 
+  redis_config=RedisConfig(db=5, host='localhost', port=6379),
+  life_span_in_seconds=3600)
 
-# Create the store and register your models
-store = Store(name='some_name', redis_config=RedisConfig(db=5, host='localhost', port=6379),
-              life_span_in_seconds=3600)
+# register your models. DON'T FORGET TO DO THIS.
 store.register_model(Book)
 store.register_model(Library)
 store.register_model(Author)
 
 # sample authors. You can create as many as you wish anywhere in the code
-
 authors = {
   "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
   "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
@@ -74,61 +112,285 @@ authors = {
 
 # Sample books.
 books = [
-  Book(title="Oliver Twist", author=authors["charles"], published_on=date(year=1215, month=4, day=4),
-       in_stock=False, rating=2, tags=["Classic"]),
-  Book(title="Great Expectations", author=authors["charles"], published_on=date(year=1220, month=4, day=4),
-       rating=5,
-       tags=["Classic"]),
-  Book(title="Jane Eyre", author=authors["charles"], published_on=date(year=1225, month=6, day=4), in_stock=False,
-       rating=3.4, tags=["Classic", "Romance"]),
-  Book(title="Wuthering Heights", author=authors["jane"], published_on=date(year=1600, month=4, day=4),
-       rating=4.0,
-       tags=["Classic", "Romance"]),
+    Book(
+        title="Oliver Twist",
+        author=authors["charles"],
+        published_on=date(year=1215, month=4, day=4),
+        in_stock=False,
+        rating=2,
+        tags=["Classic"],
+    ),
+    Book(
+        title="Great Expectations",
+        author=authors["charles"],
+        published_on=date(year=1220, month=4, day=4),
+        rating=5,
+        tags=["Classic"],
+    ),
+    Book(
+        title="Jane Eyre",
+        author=authors["charles"],
+        published_on=date(year=1225, month=6, day=4),
+        in_stock=False,
+        rating=3.4,
+        tags=["Classic", "Romance"],
+    ),
+    Book(
+        title="Wuthering Heights",
+        author=authors["jane"],
+        published_on=date(year=1600, month=4, day=4),
+        rating=4.0,
+        tags=["Classic", "Romance"],
+    ),
 ]
 
 # Some library objects
 libraries = [
-  Library(name="The Grand Library", address="Kinogozi, Hoima, Uganda"),
-  Library(name="Christian Library", address="Buhimba, Hoima, Uganda")
+  Library(
+    name="The Grand Library", 
+    address="Kinogozi, Hoima, Uganda", 
+    lost=[books[1]],
+  ),
+  Library(
+    name="Christian Library", 
+    address="Buhimba, Hoima, Uganda", 
+    new=(books[0], authors["jane"], 30),
+  )
 ]
 
-# Insert them into redis
-Book.insert(books)  # (the associated authors will be automatically inserted)
+# Insert Many. You can given them a TTL (life_span_seconds).
+Book.insert(books, life_span_seconds=3600)
 Library.insert(libraries)
 
-# Select all books to view them. A list of Model instances will be returned
+# Insert One. You can also given it a TTL (life_span_seconds).
+Author.insert(Author(name="Jack Myers", active_years=(1240, 1300)))
+
+# Update One. You can also given it a TTL (life_span_seconds).
+Book.update(_id="Oliver Twist", data={"author": authors["jane"]})
+
+# Update nested model indirectly
+updated_jane = Author(**authors["jane"].dict())
+updated_jane.active_years = (1999, 2008)
+Book.update(_id="Oliver Twist", data={"author": updated_jane})
+
+# Query the data
+# Get all, with all fields shown. Data returned is a list of models instances.
 all_books = Book.select()
-print(
-  all_books)  # Will print [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), in_stock=False), Book(...]
+print(all_books)
+# Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), 
+# in_stock=False), Book(...]
 
-# Or select some books
+# Get some, with all fields shown. Data returned is a list of models instances.
 some_books = Book.select(ids=["Oliver Twist", "Jane Eyre"])
-print(some_books)  # Will print only those two books
+print(some_books)
 
-# Or select some authors
-some_authors = Author.select(ids=["Jane Austen"])
-print(
-  some_authors)  # Will print Jane Austen even though you didn't explicitly insert her in the Author's collection
-
-# Or select some columns. THIS RETURNS DICTIONARIES not MODEL Instances
-# The Dictionaries have values in string form so you might need to do some extra work
+# Get all, with only a few fields shown. Data returned is a list of dictionaries.
 books_with_few_fields = Book.select(columns=["author", "in_stock"])
-print(books_with_few_fields)  # Will print [{"author": "'Charles Dickens", "in_stock": "True"},...]
+print(books_with_few_fields)
+# Prints [{"author": "'Charles Dickens", "in_stock": "True"},...]
 
-# Update any book or library
-Book.update(_id="Oliver Twist", data={"author": authors["jane"]})
-# You could even update a given author's details by nesting their new data in a book update
-updated_jane = Author(**authors["jane"].dict())
-updated_jane.active_years = (1999, 2008)
-Book.update(_id="Oliver Twist", data={"author": updated_jane})
-# Trying to retrieve jane directly will return her with the new details
-# All other books that have Jane Austen as author will also have their data updated. (like a real relationship)
-Author.select(ids=["Jane Austen"])
+# Get some, with only some fields shown. Data returned is a list of dictionaries.
+some_books_with_few_fields = Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"])
+print(some_books_with_few_fields)
+
+# Query the nested models directly.
+some_authors = Author.select(ids=["Jane Austen"])
+print(some_authors)
 
 # Delete any number of items
 Library.delete(ids=["The Grand Library"])
 ```
 
+## Getting Started (Asynchronous Version)
+
+- Install the package
+
+  ```bash
+  pip install pydantic-redis
+  ```
+
+- Import the `Store`, the `RedisConfig` and the `Model` classes from `pydantic_redis.asyncio` and use accordingly
+
+```python
+import asyncio
+from datetime import date
+from typing import Tuple, List, Optional
+from pydantic_redis.asyncio import RedisConfig, Model, Store
+
+# The features are exactly the same as the synchronous version, except for the ability
+# to return coroutines when `insert`, `update`, `select` or `delete` are called.
+
+
+class Author(Model):
+  """
+  An Author model, just like a pydantic model with appropriate type annotations
+  NOTE: The `_primary_key_field` is mandatory
+  """
+  _primary_key_field: str = 'name'
+  name: str
+  active_years: Tuple[int, int]
+
+
+class Book(Model):
+  """
+  A Book model.
+  
+  Models can have the following field types
+  - The usual i.e. float, int, dict, list, date, str, dict, Optional etc as long as they are serializable by orjson
+  - Nested models e.g. `author: Author` or `author: Optional[Author]`
+  - List of nested models e.g. `authors: List[Author]` or `authors: Optional[List[Author]]`
+  - Tuples including nested models e.g. `access_log: Tuple[Author, date]` or `access_log: Optional[Tuple[Author, date]]]`
+  
+  NOTE: 1. Any nested model whether plain or in a list or tuple will automatically inserted into the redis store
+       when the parent model is inserted. e.g. a Book with an author field, when inserted, will also insert
+       the author. The author can then be queried directly if that's something one wishes to do.
+       
+       2. When a parent model is inserted with a nested model instance that already exists, the older nested model 
+       instance is overwritten. This is one way of updating nested models. All parent models that contain that nested
+       model instance will see the change. 
+  """
+  _primary_key_field: str = 'title'
+  title: str
+  author: Author
+  rating: float
+  published_on: date
+  tags: List[str] = []
+  in_stock: bool = True
+
+
+class Library(Model):
+  """
+  A library model.
+  
+  It shows a number of complicated nested models.
+  
+  About Nested Model Performance
+  ---
+  To minimize the performance penalty for nesting models, we use REDIS EVALSHA to eagerly load the nested models
+  before the response is returned to the client. This ensures that only ONE network call is made every time.
+  """
+  _primary_key_field: str = 'name'
+  name: str
+  address: str
+  books: List[Book] = None
+  lost: Optional[List[Book]] = None
+  popular: Optional[Tuple[Book, Book]] = None
+  new: Tuple[Book, Author, int] = None
+
+
+async def run_async():
+  """The async coroutine"""
+  # Create the store
+  store = Store(
+    name='some_name', 
+    redis_config=RedisConfig(db=5, host='localhost', port=6379),
+    life_span_in_seconds=3600)
+  
+  # register your models. DON'T FORGET TO DO THIS.
+  store.register_model(Book)
+  store.register_model(Library)
+  store.register_model(Author)
+  
+  # sample authors. You can create as many as you wish anywhere in the code
+  authors = {
+    "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
+    "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
+  }
+  
+  # Sample books.
+  books = [
+      Book(
+          title="Oliver Twist",
+          author=authors["charles"],
+          published_on=date(year=1215, month=4, day=4),
+          in_stock=False,
+          rating=2,
+          tags=["Classic"],
+      ),
+      Book(
+          title="Great Expectations",
+          author=authors["charles"],
+          published_on=date(year=1220, month=4, day=4),
+          rating=5,
+          tags=["Classic"],
+      ),
+      Book(
+          title="Jane Eyre",
+          author=authors["charles"],
+          published_on=date(year=1225, month=6, day=4),
+          in_stock=False,
+          rating=3.4,
+          tags=["Classic", "Romance"],
+      ),
+      Book(
+          title="Wuthering Heights",
+          author=authors["jane"],
+          published_on=date(year=1600, month=4, day=4),
+          rating=4.0,
+          tags=["Classic", "Romance"],
+      ),
+  ]
+  
+  # Some library objects
+  libraries = [
+    Library(
+      name="The Grand Library", 
+      address="Kinogozi, Hoima, Uganda", 
+      lost=[books[1]],
+    ),
+    Library(
+      name="Christian Library", 
+      address="Buhimba, Hoima, Uganda", 
+      new=(books[0], authors["jane"], 30),
+    )
+  ]
+  
+  # Insert Many. You can given them a TTL (life_span_seconds).
+  await Book.insert(books, life_span_seconds=3600)
+  await Library.insert(libraries)
+  
+  # Insert One. You can also given it a TTL (life_span_seconds).
+  await Author.insert(Author(name="Jack Myers", active_years=(1240, 1300)))
+  
+  # Update One. You can also given it a TTL (life_span_seconds).
+  await Book.update(_id="Oliver Twist", data={"author": authors["jane"]})
+  
+  # Update nested model indirectly
+  updated_jane = Author(**authors["jane"].dict())
+  updated_jane.active_years = (1999, 2008)
+  await Book.update(_id="Oliver Twist", data={"author": updated_jane})
+  
+  # Query the data
+  # Get all, with all fields shown. Data returned is a list of models instances.
+  all_books = await Book.select()
+  print(all_books)
+  # Prints [Book(title="Oliver Twist", author="Charles Dickens", published_on=date(year=1215, month=4, day=4), 
+  # in_stock=False), Book(...]
+  
+  # Get some, with all fields shown. Data returned is a list of models instances.
+  some_books = await Book.select(ids=["Oliver Twist", "Jane Eyre"])
+  print(some_books)
+  
+  # Get all, with only a few fields shown. Data returned is a list of dictionaries.
+  books_with_few_fields = await Book.select(columns=["author", "in_stock"])
+  print(books_with_few_fields)
+  # Prints [{"author": "'Charles Dickens", "in_stock": "True"},...]
+  
+  # Get some, with only some fields shown. Data returned is a list of dictionaries.
+  some_books_with_few_fields = await Book.select(ids=["Oliver Twist", "Jane Eyre"], columns=["author", "in_stock"])
+  print(some_books_with_few_fields)
+  
+  # Query the nested models directly.
+  some_authors = await Author.select(ids=["Jane Austen"])
+  print(some_authors)
+  
+  # Delete any number of items
+  await Library.delete(ids=["The Grand Library"])
+
+
+asyncio.run(run_async())
+```
+
 ## How to test
 
 - Clone the repo and enter its root folder
@@ -204,6 +466,13 @@ benchmark_bulk_insert[redis_store]                         897.7862 (6.75)     8
 -----------------------------------------------------------------------------------------------------------------------
 ```
 
+## Contributions
+
+Contributions are welcome. The docs have to maintained, the code has to be made cleaner, more idiomatic and faster,
+and there might be need for someone else to take over this repo in case I move on to other things. It happens!
+
+When you are ready, look at the [CONTRIBUTIONS GUIDELINES](./CONTRIBUTING.md)
+
 ## License
 
 Copyright (c) 2020 [Martin Ahindura](https://github.com/Tinitto) Licensed under the [MIT License](./LICENSE)
diff --git a/pydantic_redis/__init__.py b/pydantic_redis/__init__.py
index ef8d8180..da72f1de 100644
--- a/pydantic_redis/__init__.py
+++ b/pydantic_redis/__init__.py
@@ -1,7 +1,8 @@
 """Entry point for redisy"""
 
-from .config import RedisConfig
-from .model import Model
-from .store import Store
+from pydantic_redis.syncio import Store, Model, RedisConfig
+import pydantic_redis.asyncio
 
-__version__ = "0.2.0"
+__all__ = [Store, RedisConfig, Model, asyncio]
+
+__version__ = "0.3.0"
diff --git a/pydantic_redis/abstract.py b/pydantic_redis/abstract.py
deleted file mode 100644
index 4cec4d12..00000000
--- a/pydantic_redis/abstract.py
+++ /dev/null
@@ -1,185 +0,0 @@
-"""Module containing the main base classes"""
-from typing import Optional, Union, Any, Dict, List, Callable, Tuple, Type
-
-import orjson
-import redis
-from pydantic import BaseModel
-from redis.commands.core import Script
-
-from pydantic_redis.config import RedisConfig
-
-
-class _AbstractStore(BaseModel):
-    """
-    An abstract class of a store
-    """
-
-    name: str
-    redis_config: RedisConfig
-    redis_store: Optional[redis.Redis] = None
-    life_span_in_seconds: Optional[int] = None
-    select_all_fields_for_all_ids_script: Optional[Script] = None
-    select_all_fields_for_some_ids_script: Optional[Script] = None
-    select_some_fields_for_all_ids_script: Optional[Script] = None
-    select_some_fields_for_some_ids_script: Optional[Script] = None
-
-    class Config:
-        arbitrary_types_allowed = True
-        orm_mode = True
-
-
-class _AbstractModel(BaseModel):
-    """
-    An abstract class to help with typings for Model class
-    """
-
-    _store: _AbstractStore
-    _primary_key_field: str
-    _nested_model_tuple_fields: Dict[str, Tuple[Any, ...]] = {}
-    _nested_model_list_fields: Dict[str, Type["_AbstractModel"]] = {}
-    _nested_model_fields: Dict[str, Type["_AbstractModel"]] = {}
-    _field_types: Dict[str, Any] = {}
-
-    class Config:
-        arbitrary_types_allowed = True
-
-    @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 = {}
-
-        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 cls._nested_model_list_fields and value is not None:
-                value = cls.__deserialize_nested_model_list(field=key, value=value)
-
-            elif key in cls._nested_model_tuple_fields and value is not None:
-                value = cls.__deserialize_nested_model_tuple(field=key, value=value)
-
-            elif key in cls._nested_model_fields and value is not None:
-                value = cls.__deserialize_nested_model(field=key, value=value)
-
-            parsed_dict[key] = value
-
-        return parsed_dict
-
-    @classmethod
-    def get_primary_key_field(cls):
-        """Gets the protected _primary_key_field"""
-        return cls._primary_key_field
-
-    @classmethod
-    def insert(cls, data: Union[List[Any], Any]):
-        raise NotImplementedError("insert should be implemented")
-
-    @classmethod
-    def update(
-        cls, primary_key_value: Union[Any, Dict[str, Any]], data: Dict[str, Any]
-    ):
-        raise NotImplementedError("update should be implemented")
-
-    @classmethod
-    def delete(cls, primary_key_value: Union[Any, Dict[str, Any]]):
-        raise NotImplementedError("delete should be implemented")
-
-    @classmethod
-    def select(cls, columns: Optional[List[str]] = None):
-        """Should later allow AND, OR"""
-        raise NotImplementedError("select should be implemented")
-
-    @classmethod
-    def __deserialize_nested_model_list(
-        cls, field: str, value: List[Any]
-    ) -> List["_AbstractModel"]:
-        """Deserializes a list of key values for the given field returning a list of nested models"""
-        field_type = cls._nested_model_list_fields.get(field)
-        return [field_type(**field_type.deserialize_partially(item)) for item in value]
-
-    @classmethod
-    def __deserialize_nested_model_tuple(
-        cls, field: str, value: List[Any]
-    ) -> Tuple[Any, ...]:
-        """Deserializes a list of key values for the given field returning a tuple of nested models among others"""
-        field_types = cls._nested_model_tuple_fields.get(field, ())
-
-        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)
-
-    @classmethod
-    def __deserialize_nested_model(
-        cls, field: str, value: List[Any]
-    ) -> "_AbstractModel":
-        """Deserializes a list of key values for the given field returning the nested model"""
-        field_type = cls._nested_model_fields.get(field)
-        return field_type(**field_type.deserialize_partially(value))
-
-
-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/asyncio/__init__.py b/pydantic_redis/asyncio/__init__.py
new file mode 100644
index 00000000..168f33ff
--- /dev/null
+++ b/pydantic_redis/asyncio/__init__.py
@@ -0,0 +1,7 @@
+"""Package containing the async version of pydantic_redis"""
+
+from .model import Model
+from .store import Store
+from ..shared.config import RedisConfig
+
+__all__ = [Model, Store, RedisConfig]
diff --git a/pydantic_redis/asyncio/model.py b/pydantic_redis/asyncio/model.py
new file mode 100644
index 00000000..76f10033
--- /dev/null
+++ b/pydantic_redis/asyncio/model.py
@@ -0,0 +1,143 @@
+"""Module containing the model classes"""
+from typing import Optional, List, Any, Union, Dict, Tuple, Type
+
+import redis.asyncio
+
+from pydantic_redis.shared.model import AbstractModel
+from pydantic_redis.shared.model.insert_utils import insert_on_pipeline
+from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key
+from pydantic_redis.shared.model.select_utils import (
+    select_all_fields_all_ids,
+    select_all_fields_some_ids,
+    select_some_fields_all_ids,
+    select_some_fields_some_ids,
+    parse_select_response,
+)
+
+from .store import Store
+
+
+class Model(AbstractModel):
+    """
+    The section in the store that saves rows of the same kind
+    """
+
+    _store: Store
+
+    @classmethod
+    async def insert(
+        cls,
+        data: Union[List[AbstractModel], AbstractModel],
+        life_span_seconds: Optional[float] = None,
+    ):
+        """
+        Inserts a given row or sets of rows into the table
+        """
+        store = cls.get_store()
+        life_span = (
+            life_span_seconds
+            if life_span_seconds is not None
+            else store.life_span_in_seconds
+        )
+
+        async with store.redis_store.pipeline(transaction=True) as pipeline:
+            data_list = []
+
+            if isinstance(data, list):
+                data_list = data
+            elif isinstance(data, AbstractModel):
+                data_list = [data]
+
+            for record in data_list:
+                insert_on_pipeline(
+                    model=cls,
+                    _id=None,
+                    pipeline=pipeline,
+                    record=record,
+                    life_span=life_span,
+                )
+
+            return await pipeline.execute()
+
+    @classmethod
+    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
+        """
+        store = cls.get_store()
+        life_span = (
+            life_span_seconds
+            if life_span_seconds is not None
+            else store.life_span_in_seconds
+        )
+        async with store.redis_store.pipeline(transaction=True) as pipeline:
+            if isinstance(data, dict):
+                insert_on_pipeline(
+                    model=cls,
+                    _id=_id,
+                    pipeline=pipeline,
+                    record=data,
+                    life_span=life_span,
+                )
+
+            return await pipeline.execute()
+
+    @classmethod
+    async def delete(cls, ids: Union[Any, List[Any]]):
+        """
+        deletes a given row or sets of rows in the table
+        """
+        store = cls.get_store()
+
+        async with store.redis_store.pipeline() as pipeline:
+            primary_keys = []
+
+            if isinstance(ids, list):
+                primary_keys = ids
+            elif ids is not None:
+                primary_keys = [ids]
+
+            names = [
+                get_primary_key(model=cls, primary_key_value=primary_key_value)
+                for primary_key_value in primary_keys
+            ]
+            pipeline.delete(*names)
+            # remove the primary keys from the index
+            table_index_key = get_table_index_key(model=cls)
+            pipeline.srem(table_index_key, *names)
+            return await pipeline.execute()
+
+    @classmethod
+    async def select(
+        cls,
+        columns: Optional[List[str]] = None,
+        ids: Optional[List[Any]] = None,
+        **kwargs,
+    ):
+        """
+        Selects given rows or sets of rows in the table
+        """
+        if columns is None and ids is None:
+            response = await select_all_fields_all_ids(model=cls)
+
+        elif columns is None and isinstance(ids, list):
+            response = await select_all_fields_some_ids(model=cls, ids=ids)
+
+        elif isinstance(columns, list) and ids is None:
+            response = await select_some_fields_all_ids(model=cls, fields=columns)
+
+        elif isinstance(columns, list) and isinstance(ids, list):
+            response = await select_some_fields_some_ids(
+                model=cls, fields=columns, ids=ids
+            )
+
+        else:
+            raise ValueError(
+                f"columns {columns}, ids: {ids} should be either None or lists"
+            )
+
+        return parse_select_response(
+            model=cls, response=response, as_models=(columns is None)
+        )
diff --git a/pydantic_redis/asyncio/store.py b/pydantic_redis/asyncio/store.py
new file mode 100644
index 00000000..4c24ecae
--- /dev/null
+++ b/pydantic_redis/asyncio/store.py
@@ -0,0 +1,25 @@
+"""Module containing the store class for async io"""
+from typing import Dict, Type, TYPE_CHECKING
+
+from redis import asyncio as redis
+
+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
+    """
+
+    models: Dict[str, Type["Model"]] = {}
+
+    def _connect_to_redis(self) -> redis.Redis:
+        """Connects the store to redis, returning a proper connection"""
+        return redis.from_url(
+            self.redis_config.redis_url,
+            encoding=self.redis_config.encoding,
+            decode_responses=True,
+        )
diff --git a/pydantic_redis/model.py b/pydantic_redis/model.py
deleted file mode 100644
index 4ae72b42..00000000
--- a/pydantic_redis/model.py
+++ /dev/null
@@ -1,421 +0,0 @@
-"""Module containing the model classes"""
-import typing
-from typing import Optional, List, Any, Union, Dict, Tuple, Type
-
-from redis.client import Pipeline
-
-from pydantic_redis.abstract import _AbstractModel
-
-NESTED_MODEL_PREFIX = "__"
-NESTED_MODEL_LIST_FIELD_PREFIX = "___"
-NESTED_MODEL_TUPLE_FIELD_PREFIX = "____"
-
-
-class Model(_AbstractModel):
-    """
-    The section in the store that saves rows of the same kind
-    """
-
-    @classmethod
-    def __get_primary_key(cls, primary_key_value: Any):
-        """
-        Returns the primary key value concatenated to the table name for uniqueness
-        """
-        return f"{cls.__get_table_prefix()}{primary_key_value}"
-
-    @classmethod
-    def __get_table_prefix(cls):
-        """
-        Returns the prefix of the all the redis keys that are associated with this table
-        """
-        table_name = cls.__name__.lower()
-        return f"{table_name}_%&_"
-
-    @classmethod
-    def __get_table_keys_regex(cls):
-        """
-        Returns the table name regex to get all keys that belong to this table
-        """
-        return f"{cls.__get_table_prefix()}*"
-
-    @classmethod
-    def get_table_index_key(cls):
-        """Returns the key in which the primary keys of the given table have been saved"""
-        table_name = cls.__name__.lower()
-        return f"{table_name}__index"
-
-    @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], Model)
-                ):
-                    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, Model) for v in typing_get_args(field_type)])
-                ):
-                    cls._nested_model_tuple_fields[field] = typing_get_args(field_type)
-
-                elif issubclass(field_type, Model):
-                    cls._nested_model_fields[field] = field_type
-
-            except (TypeError, AttributeError) as exp:
-                pass
-
-    @classmethod
-    def insert(
-        cls,
-        data: Union[List[_AbstractModel], _AbstractModel],
-        life_span_seconds: Optional[float] = None,
-    ):
-        """
-        Inserts a given row or sets of rows into the table
-        """
-        life_span = (
-            life_span_seconds
-            if life_span_seconds is not None
-            else cls._store.life_span_in_seconds
-        )
-        with cls._store.redis_store.pipeline(transaction=True) as pipeline:
-            data_list = []
-
-            if isinstance(data, list):
-                data_list = data
-            elif isinstance(data, _AbstractModel):
-                data_list = [data]
-
-            for record in data_list:
-                cls.__insert_on_pipeline(
-                    _id=None, pipeline=pipeline, record=record, life_span=life_span
-                )
-
-            return pipeline.execute()
-
-    @classmethod
-    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
-        """
-        life_span = (
-            life_span_seconds
-            if life_span_seconds is not None
-            else cls._store.life_span_in_seconds
-        )
-        with cls._store.redis_store.pipeline(transaction=True) as pipeline:
-            if isinstance(data, dict):
-                cls.__insert_on_pipeline(
-                    _id=_id, pipeline=pipeline, record=data, life_span=life_span
-                )
-
-            return pipeline.execute()
-
-    @classmethod
-    def delete(cls, ids: Union[Any, List[Any]]):
-        """
-        deletes a given row or sets of rows in the table
-        """
-        with cls._store.redis_store.pipeline() as pipeline:
-            primary_keys = []
-
-            if isinstance(ids, list):
-                primary_keys = ids
-            elif ids is not None:
-                primary_keys = [ids]
-
-            names = [
-                cls.__get_primary_key(primary_key_value=primary_key_value)
-                for primary_key_value in primary_keys
-            ]
-            pipeline.delete(*names)
-            # remove the primary keys from the index
-            table_index_key = cls.get_table_index_key()
-            pipeline.srem(table_index_key, *names)
-            return pipeline.execute()
-
-    @classmethod
-    def select(
-        cls,
-        columns: Optional[List[str]] = None,
-        ids: Optional[List[Any]] = None,
-        pipeline: Optional[Pipeline] = None,
-    ):
-        """
-        Selects given rows or sets of rows in the table
-        """
-        if columns is None and ids is None:
-            # all fields, all ids
-            table_keys_regex = cls.__get_table_keys_regex()
-            args = [table_keys_regex]
-            response = cls._store.select_all_fields_for_all_ids_script(args=args)
-        elif columns is None and isinstance(ids, list):
-            # all fields, some ids
-            table_prefix = cls.__get_table_prefix()
-            keys = [f"{table_prefix}{key}" for key in ids]
-            response = cls._store.select_all_fields_for_some_ids_script(keys=keys)
-        elif isinstance(columns, list) and ids is None:
-            # some fields, all ids
-            table_keys_regex = cls.__get_table_keys_regex()
-            columns = cls.__get_select_fields(columns)
-            args = [table_keys_regex, *columns]
-            response = cls._store.select_some_fields_for_all_ids_script(args=args)
-        elif isinstance(columns, list) and isinstance(ids, list):
-            # some fields, some ids
-            table_prefix = cls.__get_table_prefix()
-            keys = [f"{table_prefix}{key}" for key in ids]
-            columns = cls.__get_select_fields(columns)
-            response = cls._store.select_some_fields_for_all_ids_script(
-                keys=keys, args=columns
-            )
-        else:
-            raise ValueError(
-                f"columns {columns}, ids: {ids} should be either None or lists"
-            )
-
-        if len(response) == 0:
-            return None
-        elif isinstance(response, list) and columns is None:
-            return cls.__parse_list_of_lists_to_models(response)
-        elif isinstance(response, list) and columns is not None:
-            return cls.__parse_list_of_lists_to_dicts(response)
-
-        return response
-
-    @classmethod
-    def __get_select_fields(cls, 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 = []
-        for col in columns:
-
-            if col in cls._nested_model_fields:
-                col = f"{NESTED_MODEL_PREFIX}{col}"
-            elif col in cls._nested_model_list_fields:
-                col = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{col}"
-            elif col in cls._nested_model_tuple_fields:
-                col = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{col}"
-
-            fields.append(col)
-        return fields
-
-    @classmethod
-    def __parse_list_of_lists_to_models(cls, data: List[List[Any]]) -> List["Model"]:
-        """
-        Converts a list of lists of key-values into a list of models
-        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}]
-        """
-        return [
-            cls(**cls.deserialize_partially(record)) for record in data if record != []
-        ]
-
-    @classmethod
-    def __parse_list_of_lists_to_dicts(
-        cls, data: List[List[Any]]
-    ) -> List[Dict[str, Any]]:
-        """
-        Converts a list of lists of key-values into a list of normalized dictionaries
-        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}]
-        """
-        return [cls.deserialize_partially(record) for record in data if record != []]
-
-    @classmethod
-    def __insert_on_pipeline(
-        cls,
-        pipeline: Pipeline,
-        _id: Optional[Any],
-        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
-        """
-        key = _id if _id is not None else getattr(record, cls._primary_key_field)
-        data = cls.__get_serializable_dict(
-            pipeline=pipeline, record=record, life_span=life_span
-        )
-        name = cls.__get_primary_key(primary_key_value=key)
-        mapping = cls.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
-        table_index_key = cls.get_table_index_key()
-        pipeline.sadd(table_index_key, name)
-        if life_span is not None:
-            pipeline.expire(table_index_key, time=life_span)
-
-        return name
-
-    @classmethod
-    def __get_serializable_dict(
-        cls,
-        pipeline: Pipeline,
-        record: Union[_AbstractModel, Dict[str, Any]],
-        life_span: Optional[Union[float, int]] = None,
-    ) -> Dict[str, Any]:
-        """
-        Returns a dictionary that can be serialized.
-        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
-            e.g. `__author` instead of author
-          - Replacing the keys of lists of nested records with their NESTED_MODEL_LIST_FIELD_PREFIX suffixed 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
-            e.g. `__%&l_author` instead of author
-          - Replacing the values of nested records with their foreign keys
-        """
-        data = record.items() if isinstance(record, dict) else record
-        new_data = {}
-
-        for k, v in data:
-            key, value = k, v
-
-            if key in cls._nested_model_list_fields:
-                key, value = cls.__serialize_nested_model_list_field(
-                    key=key, value=value, pipeline=pipeline, life_span=life_span
-                )
-            elif key in cls._nested_model_tuple_fields:
-                key, value = cls.__serialize_nested_model_tuple_field(
-                    key=key, value=value, pipeline=pipeline, life_span=life_span
-                )
-            elif key in cls._nested_model_fields:
-                key, value = cls.__serialize_nested_model_field(
-                    key=key, value=value, pipeline=pipeline, life_span=life_span
-                )
-
-            new_data[key] = value
-        return new_data
-
-    @classmethod
-    def __serialize_nested_model_tuple_field(
-        cls,
-        key: str,
-        value: Tuple["Model"],
-        pipeline: Pipeline,
-        life_span: Optional[Union[float, int]],
-    ) -> Tuple[str, List[Any]]:
-        """Serializes a key-value pair for a field that has a tuple of nested models"""
-        try:
-            field_types = cls._nested_model_tuple_fields.get(key, ())
-            value = [
-                field_type.__insert_on_pipeline(
-                    _id=None, pipeline=pipeline, record=item, life_span=life_span
-                )
-                if issubclass(field_type, Model)
-                else item
-                for field_type, item in zip(field_types, value)
-            ]
-            key = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{key}"
-        except TypeError:
-            # In case the value is None, just ignore
-            pass
-
-        return key, value
-
-    @classmethod
-    def __serialize_nested_model_list_field(
-        cls,
-        key: str,
-        value: List["Model"],
-        pipeline: Pipeline,
-        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"""
-        try:
-            value = [
-                item.__class__.__insert_on_pipeline(
-                    _id=None, pipeline=pipeline, record=item, life_span=life_span
-                )
-                for item in value
-            ]
-            key = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{key}"
-        except TypeError:
-            # In case the value is None, just ignore
-            pass
-
-        return key, value
-
-    @classmethod
-    def __serialize_nested_model_field(
-        cls,
-        key: str,
-        value: "Model",
-        pipeline: Pipeline,
-        life_span: Optional[Union[float, int]],
-    ) -> Tuple[str, str]:
-        """Serializes a key-value pair for a field that has a nested model"""
-        try:
-            value = value.__class__.__insert_on_pipeline(
-                _id=None, pipeline=pipeline, record=value, life_span=life_span
-            )
-            key = f"{NESTED_MODEL_PREFIX}{key}"
-        except TypeError:
-            # In case the value is None, just ignore
-            pass
-
-        return key, value
-
-
-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)
diff --git a/pydantic_redis/shared/__init__.py b/pydantic_redis/shared/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/pydantic_redis/config.py b/pydantic_redis/shared/config.py
similarity index 100%
rename from pydantic_redis/config.py
rename to pydantic_redis/shared/config.py
diff --git a/pydantic_redis/store.py b/pydantic_redis/shared/lua_scripts.py
similarity index 73%
rename from pydantic_redis/store.py
rename to pydantic_redis/shared/lua_scripts.py
index 8d431e64..d1896bfe 100644
--- a/pydantic_redis/store.py
+++ b/pydantic_redis/shared/lua_scripts.py
@@ -1,11 +1,4 @@
-"""Module containing the store classes"""
-from typing import Dict, Optional, Any
-
-import redis
-
-from pydantic_redis.abstract import _AbstractStore
-from pydantic_redis.config import RedisConfig
-from pydantic_redis.model import Model
+"""Module containing the lua scripts used in select queries"""
 
 SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT = """
 local s_find = string.find
@@ -264,62 +257,3 @@
 end
 return result
 """
-
-
-class Store(_AbstractStore):
-    """
-    A store that allows a declarative way of querying for data in redis
-    """
-
-    models: Dict[str, type(Model)] = {}
-
-    def __init__(
-        self,
-        name: str,
-        redis_config: RedisConfig,
-        redis_store: Optional[redis.Redis] = None,
-        life_span_in_seconds: Optional[int] = None,
-        **data: Any,
-    ):
-        super().__init__(
-            name=name,
-            redis_config=redis_config,
-            redis_store=redis_store,
-            life_span_in_seconds=life_span_in_seconds,
-            **data,
-        )
-
-        self.redis_store = redis.from_url(
-            self.redis_config.redis_url,
-            encoding=self.redis_config.encoding,
-            decode_responses=True,
-        )
-
-        # register lua scripts
-        self.select_all_fields_for_all_ids_script = self.redis_store.register_script(
-            SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT
-        )
-        self.select_all_fields_for_some_ids_script = self.redis_store.register_script(
-            SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT
-        )
-        self.select_some_fields_for_all_ids_script = self.redis_store.register_script(
-            SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT
-        )
-        self.select_some_fields_for_some_ids_script = self.redis_store.register_script(
-            SELECT_SOME_FIELDS_FOR_SOME_IDS_SCRIPT
-        )
-
-    def register_model(self, model_class: type(Model)):
-        """Registers the model to this store"""
-        if not isinstance(model_class.get_primary_key_field(), str):
-            raise NotImplementedError(
-                f"{model_class.__name__} should have a _primary_key_field"
-            )
-
-        model_class._store = self
-        model_class.initialize()
-        self.models[model_class.__name__.lower()] = model_class
-
-    def model(self, name: str) -> Model:
-        """Gets a model by name: case insensitive"""
-        return self.models[name.lower()]
diff --git a/pydantic_redis/shared/model/__init__.py b/pydantic_redis/shared/model/__init__.py
new file mode 100644
index 00000000..4b43caa9
--- /dev/null
+++ b/pydantic_redis/shared/model/__init__.py
@@ -0,0 +1,1 @@
+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..4b68698c
--- /dev/null
+++ b/pydantic_redis/shared/model/base.py
@@ -0,0 +1,190 @@
+"""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/insert_utils.py b/pydantic_redis/shared/model/insert_utils.py
new file mode 100644
index 00000000..7c70b8f9
--- /dev/null
+++ b/pydantic_redis/shared/model/insert_utils.py
@@ -0,0 +1,174 @@
+"""Module containing the mixin for insert functionality in model"""
+from typing import Union, Optional, Any, Dict, Tuple, List, Type
+
+from redis.asyncio.client import Pipeline as AioPipeline
+from redis.client import Pipeline
+
+from .prop_utils import (
+    get_primary_key,
+    get_table_index_key,
+    NESTED_MODEL_PREFIX,
+    NESTED_MODEL_LIST_FIELD_PREFIX,
+    NESTED_MODEL_TUPLE_FIELD_PREFIX,
+)
+
+from .base import AbstractModel
+
+
+def insert_on_pipeline(
+    model: Type[AbstractModel],
+    pipeline: Union[Pipeline, AioPipeline],
+    _id: Optional[Any],
+    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
+    """
+    key = _id if _id is not None else getattr(record, model.get_primary_key_field())
+    data = _get_serializable_dict(
+        model=model, pipeline=pipeline, record=record, life_span=life_span
+    )
+    name = get_primary_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
+    table_index_key = get_table_index_key(model)
+    pipeline.sadd(table_index_key, name)
+    if life_span is not None:
+        pipeline.expire(table_index_key, time=life_span)
+
+    return name
+
+
+def _get_serializable_dict(
+    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.
+    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
+        e.g. `__author` instead of author
+      - Replacing the keys of lists of nested records with their NESTED_MODEL_LIST_FIELD_PREFIX suffixed 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
+        e.g. `__%&l_author` instead of author
+      - Replacing the values of nested records with their foreign keys
+    """
+    data = record.items() if isinstance(record, dict) else record
+    new_data = {}
+
+    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 k, v in data:
+        key, value = k, v
+
+        if key in nested_model_list_fields:
+            key, value = _serialize_nested_model_list_field(
+                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=key,
+                value=value,
+                pipeline=pipeline,
+                life_span=life_span,
+                tuple_fields=nested_model_tuple_fields,
+            )
+        elif key in nested_model_fields:
+            key, value = _serialize_nested_model_field(
+                key=key, value=value, pipeline=pipeline, life_span=life_span
+            )
+
+        new_data[key] = value
+    return new_data
+
+
+def _serialize_nested_model_tuple_field(
+    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"""
+    try:
+        field_types = tuple_fields.get(key, ())
+        value = [
+            insert_on_pipeline(
+                model=field_type,
+                _id=None,
+                pipeline=pipeline,
+                record=item,
+                life_span=life_span,
+            )
+            # FIXME: The reference to AbstractModel here. Is it right?
+            if issubclass(field_type, AbstractModel) else item
+            for field_type, item in zip(field_types, value)
+        ]
+        key = f"{NESTED_MODEL_TUPLE_FIELD_PREFIX}{key}"
+    except TypeError:
+        # In case the value is None, just ignore
+        pass
+
+    return key, value
+
+
+def _serialize_nested_model_list_field(
+    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"""
+    try:
+        value = [
+            insert_on_pipeline(
+                model=item.__class__,
+                _id=None,
+                pipeline=pipeline,
+                record=item,
+                life_span=life_span,
+            )
+            for item in value
+        ]
+        key = f"{NESTED_MODEL_LIST_FIELD_PREFIX}{key}"
+    except TypeError:
+        # In case the value is None, just ignore
+        pass
+
+    return key, value
+
+
+def _serialize_nested_model_field(
+    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"""
+    try:
+        value = insert_on_pipeline(
+            model=value.__class__,
+            _id=None,
+            pipeline=pipeline,
+            record=value,
+            life_span=life_span,
+        )
+        key = f"{NESTED_MODEL_PREFIX}{key}"
+    except TypeError:
+        # In case the value is None, just ignore
+        pass
+
+    return key, value
diff --git a/pydantic_redis/shared/model/prop_utils.py b/pydantic_redis/shared/model/prop_utils.py
new file mode 100644
index 00000000..335b3dda
--- /dev/null
+++ b/pydantic_redis/shared/model/prop_utils.py
@@ -0,0 +1,37 @@
+"""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
new file mode 100644
index 00000000..ac152ec2
--- /dev/null
+++ b/pydantic_redis/shared/model/select_utils.py
@@ -0,0 +1,102 @@
+"""Module containing the mixin functionality for selecting"""
+from typing import List, Any, Type, Union, Awaitable
+
+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,
+)
+
+
+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],
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+    """Selects all items in the database, returning all their fields"""
+    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]
+) -> Union[List[List[Any]], Awaitable[List[List[Any]]]]:
+    """Selects all items in the database, returning only the specified fields"""
+    table_keys_regex = get_table_keys_regex(model=model)
+    columns = get_select_fields(model=model, columns=fields)
+    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_all_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 != []]
diff --git a/pydantic_redis/shared/store.py b/pydantic_redis/shared/store.py
new file mode 100644
index 00000000..5d193563
--- /dev/null
+++ b/pydantic_redis/shared/store.py
@@ -0,0 +1,93 @@
+"""Module containing common base store functionality"""
+from typing import Optional, Union, Type, Dict, Any
+
+from redis import Redis
+from redis.asyncio import Redis as AioRedis
+from pydantic import BaseModel
+from redis.commands.core import Script, AsyncScript
+
+from .config import RedisConfig
+from .lua_scripts import (
+    SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT,
+    SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT,
+    SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT,
+    SELECT_SOME_FIELDS_FOR_SOME_IDS_SCRIPT,
+)
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from .model import AbstractModel
+
+
+class AbstractStore(BaseModel):
+    """
+    An abstract class of a store
+    """
+
+    name: str
+    redis_config: RedisConfig
+    redis_store: Optional[Union[Redis, AioRedis]] = None
+    life_span_in_seconds: Optional[int] = None
+    select_all_fields_for_all_ids_script: Optional[Union[AsyncScript, Script]] = None
+    select_all_fields_for_some_ids_script: Optional[Union[AsyncScript, Script]] = None
+    select_some_fields_for_all_ids_script: Optional[Union[AsyncScript, Script]] = None
+    select_some_fields_for_some_ids_script: Optional[Union[AsyncScript, Script]] = None
+    models: Dict[str, Type["AbstractModel"]] = {}
+
+    class Config:
+        arbitrary_types_allowed = True
+        orm_mode = True
+
+    def __init__(
+        self,
+        name: str,
+        redis_config: RedisConfig,
+        redis_store: Optional[Union[Redis, AioRedis]] = None,
+        life_span_in_seconds: Optional[int] = None,
+        **data: Any,
+    ):
+        super().__init__(
+            name=name,
+            redis_config=redis_config,
+            redis_store=redis_store,
+            life_span_in_seconds=life_span_in_seconds,
+            **data,
+        )
+
+        self.redis_store = self._connect_to_redis()
+        self._register_lua_scripts()
+
+    def _connect_to_redis(self) -> Union[Redis, AioRedis]:
+        """Connects the store to redis, returning a proper connection"""
+        raise NotImplementedError("implement _connect_to_redis first")
+
+    def _register_lua_scripts(self):
+        """Registers the lua scripts for this redis instance"""
+        self.select_all_fields_for_all_ids_script = self.redis_store.register_script(
+            SELECT_ALL_FIELDS_FOR_ALL_IDS_SCRIPT
+        )
+        self.select_all_fields_for_some_ids_script = self.redis_store.register_script(
+            SELECT_ALL_FIELDS_FOR_SOME_IDS_SCRIPT
+        )
+        self.select_some_fields_for_all_ids_script = self.redis_store.register_script(
+            SELECT_SOME_FIELDS_FOR_ALL_IDS_SCRIPT
+        )
+        self.select_some_fields_for_some_ids_script = self.redis_store.register_script(
+            SELECT_SOME_FIELDS_FOR_SOME_IDS_SCRIPT
+        )
+
+    def register_model(self, model_class: Type["AbstractModel"]):
+        """Registers the model to this store"""
+        if not isinstance(model_class.get_primary_key_field(), str):
+            raise NotImplementedError(
+                f"{model_class.__name__} should have a _primary_key_field"
+            )
+
+        model_class._store = self
+        model_class.initialize()
+        self.models[model_class.__name__.lower()] = model_class
+
+    def model(self, name: str) -> Type["AbstractModel"]:
+        """Gets a model by name: case insensitive"""
+        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..a76de887
--- /dev/null
+++ b/pydantic_redis/shared/utils.py
@@ -0,0 +1,75 @@
+"""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
new file mode 100644
index 00000000..2af01c3f
--- /dev/null
+++ b/pydantic_redis/syncio/__init__.py
@@ -0,0 +1,6 @@
+"""Package containing the synchronous and thus default version of pydantic_redis"""
+from .model import Model
+from .store import Store
+from ..shared.config import RedisConfig
+
+__all__ = [Model, Store, RedisConfig]
diff --git a/pydantic_redis/syncio/model.py b/pydantic_redis/syncio/model.py
new file mode 100644
index 00000000..0999e939
--- /dev/null
+++ b/pydantic_redis/syncio/model.py
@@ -0,0 +1,138 @@
+"""Module containing the model classes"""
+from typing import Optional, List, Any, Union, Dict
+
+from pydantic_redis.shared.model import AbstractModel
+from pydantic_redis.shared.model.insert_utils import insert_on_pipeline
+from pydantic_redis.shared.model.prop_utils import get_primary_key, get_table_index_key
+from pydantic_redis.shared.model.select_utils import (
+    select_all_fields_all_ids,
+    select_all_fields_some_ids,
+    select_some_fields_all_ids,
+    select_some_fields_some_ids,
+    parse_select_response,
+)
+
+from .store import Store
+
+
+class Model(AbstractModel):
+    """
+    The section in the store that saves rows of the same kind
+    """
+
+    _store: Store
+
+    @classmethod
+    def insert(
+        cls,
+        data: Union[List[AbstractModel], AbstractModel],
+        life_span_seconds: Optional[float] = None,
+    ):
+        """
+        Inserts a given row or sets of rows into the table
+        """
+        store = cls.get_store()
+
+        life_span = (
+            life_span_seconds
+            if life_span_seconds is not None
+            else store.life_span_in_seconds
+        )
+        with store.redis_store.pipeline(transaction=True) as pipeline:
+            data_list = []
+
+            if isinstance(data, list):
+                data_list = data
+            elif isinstance(data, AbstractModel):
+                data_list = [data]
+
+            for record in data_list:
+                insert_on_pipeline(
+                    model=cls,
+                    _id=None,
+                    pipeline=pipeline,
+                    record=record,
+                    life_span=life_span,
+                )
+
+            return pipeline.execute()
+
+    @classmethod
+    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
+        """
+        store = cls.get_store()
+        life_span = (
+            life_span_seconds
+            if life_span_seconds is not None
+            else store.life_span_in_seconds
+        )
+        with store.redis_store.pipeline(transaction=True) as pipeline:
+            if isinstance(data, dict):
+                insert_on_pipeline(
+                    model=cls,
+                    _id=_id,
+                    pipeline=pipeline,
+                    record=data,
+                    life_span=life_span,
+                )
+
+            return pipeline.execute()
+
+    @classmethod
+    def delete(cls, ids: Union[Any, List[Any]]):
+        """
+        deletes a given row or sets of rows in the table
+        """
+        store = cls.get_store()
+        with store.redis_store.pipeline() as pipeline:
+            primary_keys = []
+
+            if isinstance(ids, list):
+                primary_keys = ids
+            elif ids is not None:
+                primary_keys = [ids]
+
+            names = [
+                get_primary_key(model=cls, primary_key_value=primary_key_value)
+                for primary_key_value in primary_keys
+            ]
+            pipeline.delete(*names)
+            # remove the primary keys from the index
+            table_index_key = get_table_index_key(model=cls)
+            pipeline.srem(table_index_key, *names)
+            return pipeline.execute()
+
+    @classmethod
+    def select(
+        cls,
+        columns: Optional[List[str]] = None,
+        ids: Optional[List[Any]] = None,
+        **kwargs,
+    ):
+        """
+        Selects given rows or sets of rows in the table
+        """
+        if columns is None and ids is None:
+            response = select_all_fields_all_ids(model=cls)
+
+        elif columns is None and isinstance(ids, list):
+            response = select_all_fields_some_ids(model=cls, ids=ids)
+
+        elif isinstance(columns, list) and ids is None:
+            response = select_some_fields_all_ids(model=cls, fields=columns)
+
+        elif isinstance(columns, list) and isinstance(ids, list):
+            response = select_some_fields_some_ids(model=cls, fields=columns, ids=ids)
+
+        else:
+            raise ValueError(
+                f"columns {columns}, ids: {ids} should be either None or lists"
+            )
+
+        return parse_select_response(
+            model=cls, response=response, as_models=(columns is None)
+        )
diff --git a/pydantic_redis/syncio/store.py b/pydantic_redis/syncio/store.py
new file mode 100644
index 00000000..0b36cc73
--- /dev/null
+++ b/pydantic_redis/syncio/store.py
@@ -0,0 +1,25 @@
+"""Module containing the store classes for sync io"""
+from typing import Dict, Type, TYPE_CHECKING
+
+import redis
+
+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
+    """
+
+    models: Dict[str, Type["Model"]] = {}
+
+    def _connect_to_redis(self) -> redis.Redis:
+        """Connects the store to redis, returning a proper connection"""
+        return redis.from_url(
+            self.redis_config.redis_url,
+            encoding=self.redis_config.encoding,
+            decode_responses=True,
+        )
diff --git a/requirements.txt b/requirements.txt
index a2b487a8..6e52ed5f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,3 +10,4 @@ twine==3.8.0
 black==22.8.0
 pre-commit
 build
+pytest-asyncio
diff --git a/setup.py b/setup.py
index 3e1268f9..7ba54f91 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@
 # This call to setup() does all the work
 setup(
     name="pydantic-redis",
-    version="0.2.0",
+    version="0.3.0",
     description="This package provides a simple ORM for redis using pydantic-like models.",
     long_description=README,
     long_description_content_type="text/markdown",


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index da1e38b1..80fc08bb 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -3,19 +3,29 @@
 from typing import Tuple, List, Optional
 
 import pytest
+import pytest_asyncio
 import redislite
 from pytest_lazyfixture import lazy_fixture
 
-from pydantic_redis import Store, RedisConfig, Model
+from pydantic_redis import syncio as syn, asyncio as asy
 
 
-class Author(Model):
+aio_pytest_fixture = getattr(pytest_asyncio, "fixture", pytest.fixture())
+
+
+class Author(syn.Model):
+    _primary_key_field: str = "name"
+    name: str
+    active_years: Tuple[int, int]
+
+
+class AsyncAuthor(asy.Model):
     _primary_key_field: str = "name"
     name: str
     active_years: Tuple[int, int]
 
 
-class Book(Model):
+class Book(syn.Model):
     _primary_key_field: str = "title"
     title: str
     author: Author
@@ -25,7 +35,17 @@ class Book(Model):
     in_stock: bool = True
 
 
-class Library(Model):
+class AsyncBook(asy.Model):
+    _primary_key_field: str = "title"
+    title: str
+    author: AsyncAuthor
+    rating: float
+    published_on: date
+    tags: List[str] = []
+    in_stock: bool = True
+
+
+class Library(syn.Model):
     # the _primary_key_field is mandatory
     _primary_key_field: str = "name"
     name: str
@@ -36,11 +56,27 @@ class Library(Model):
     new: Tuple[Book, Author, Book, int] = None
 
 
+class AsyncLibrary(asy.Model):
+    # the _primary_key_field is mandatory
+    _primary_key_field: str = "name"
+    name: str
+    address: str
+    books: List[AsyncBook] = None
+    lost: Optional[List[AsyncBook]] = None
+    popular: Optional[Tuple[AsyncBook, AsyncBook]] = None
+    new: Tuple[AsyncBook, AsyncAuthor, AsyncBook, int] = None
+
+
 authors = {
     "charles": Author(name="Charles Dickens", active_years=(1220, 1280)),
     "jane": Author(name="Jane Austen", active_years=(1580, 1640)),
 }
 
+async_authors = {
+    "charles": AsyncAuthor(name="Charles Dickens", active_years=(1220, 1280)),
+    "jane": AsyncAuthor(name="Jane Austen", active_years=(1580, 1640)),
+}
+
 books = [
     Book(
         title="Oliver Twist",
@@ -74,10 +110,42 @@ class Library(Model):
     ),
 ]
 
-libraries = [Library(name="Babel Library", address="In a book", books=books)]
+async_books = [
+    AsyncBook(
+        title="Oliver Twist",
+        author=authors["charles"],
+        published_on=date(year=1215, month=4, day=4),
+        in_stock=False,
+        rating=2,
+        tags=["Classic"],
+    ),
+    AsyncBook(
+        title="Great Expectations",
+        author=authors["charles"],
+        published_on=date(year=1220, month=4, day=4),
+        rating=5,
+        tags=["Classic"],
+    ),
+    AsyncBook(
+        title="Jane Eyre",
+        author=authors["charles"],
+        published_on=date(year=1225, month=6, day=4),
+        in_stock=False,
+        rating=3.4,
+        tags=["Classic", "Romance"],
+    ),
+    AsyncBook(
+        title="Wuthering Heights",
+        author=authors["jane"],
+        published_on=date(year=1600, month=4, day=4),
+        rating=4.0,
+        tags=["Classic", "Romance"],
+    ),
+]
+
+# sync fixtures
 redis_store_fixture = [(lazy_fixture("redis_store"))]
 books_fixture = [(lazy_fixture("redis_store"), book) for book in books]
-librares_fixture = [(lazy_fixture("redis_store"), libraries)]
 update_books_fixture = [
     (
         lazy_fixture("redis_store"),
@@ -90,6 +158,9 @@ class Library(Model):
     (lazy_fixture("redis_store"), book.title) for book in books[-1:]
 ]
 
+# async fixtures
+async_redis_store_fixture = [(lazy_fixture("async_redis_store"))]
+
 
 @pytest.fixture()
 def unused_tcp_port():
@@ -112,9 +183,9 @@ def redis_server(unused_tcp_port):
 @pytest.fixture()
 def redis_store(redis_server):
     """Sets up a redis store using the redis_server fixture and adds the book model to it"""
-    store = Store(
+    store = syn.Store(
         name="sample",
-        redis_config=RedisConfig(port=redis_server, db=1),
+        redis_config=syn.RedisConfig(port=redis_server, db=1),
         life_span_in_seconds=3600,
     )
     store.register_model(Book)
@@ -122,3 +193,18 @@ def redis_store(redis_server):
     store.register_model(Library)
     yield store
     store.redis_store.flushall()
+
+
+@aio_pytest_fixture
+async def async_redis_store(redis_server):
+    """Sets up a redis store using the redis_server fixture and adds the book model to it"""
+    store = asy.Store(
+        name="sample",
+        redis_config=syn.RedisConfig(port=redis_server, db=1),
+        life_span_in_seconds=3600,
+    )
+    store.register_model(AsyncBook)
+    store.register_model(AsyncAuthor)
+    store.register_model(AsyncLibrary)
+    yield store
+    await store.redis_store.flushall()
diff --git a/test/test_async_pydantic_redis.py b/test/test_async_pydantic_redis.py
new file mode 100644
index 00000000..3d177ff4
--- /dev/null
+++ b/test/test_async_pydantic_redis.py
@@ -0,0 +1,395 @@
+"""Tests for the redis orm"""
+
+from typing import Dict, Any
+
+import pytest
+
+from pydantic_redis.shared.model.prop_utils import NESTED_MODEL_PREFIX
+from pydantic_redis.shared.utils import strip_leading
+from pydantic_redis.asyncio import Model, RedisConfig
+from test.conftest import (
+    async_redis_store_fixture,
+    AsyncBook,
+    async_books,
+    AsyncAuthor,
+    async_authors,
+    AsyncLibrary,
+)
+
+
+def test_redis_config_redis_url():
+    password = "password"
+    config_with_no_pass = RedisConfig()
+    config_with_ssl = RedisConfig(ssl=True)
+    config_with_pass = RedisConfig(password=password)
+    config_with_pass_ssl = RedisConfig(ssl=True, password=password)
+
+    assert config_with_no_pass.redis_url == "redis://localhost:6379/0"
+    assert config_with_ssl.redis_url == "rediss://localhost:6379/0"
+    assert config_with_pass.redis_url == f"redis://:{password}@localhost:6379/0"
+    assert config_with_pass_ssl.redis_url == f"rediss://:{password}@localhost:6379/0"
+
+
+def test_register_model_without_primary_key(async_redis_store):
+    """Throws error when a model without the _primary_key_field class variable set is registered"""
+
+    class ModelWithoutPrimaryKey(Model):
+        title: str
+
+    with pytest.raises(AttributeError, match=r"_primary_key_field"):
+        async_redis_store.register_model(ModelWithoutPrimaryKey)
+
+    ModelWithoutPrimaryKey._primary_key_field = None
+
+    with pytest.raises(Exception, match=r"should have a _primary_key_field"):
+        async_redis_store.register_model(ModelWithoutPrimaryKey)
+
+
+def test_store_model(async_redis_store):
+    """Tests the model method in store"""
+    assert async_redis_store.model("AsyncBook") == AsyncBook
+
+    with pytest.raises(KeyError):
+        async_redis_store.model("Notabook")
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_bulk_insert(store):
+    """Providing a list of Model instances to the insert method inserts the records in redis"""
+    book_keys = [f"asyncbook_%&_{book.title}" for book in async_books]
+    keys = book_keys + [
+        f"asyncauthor_%&_{author.name}" for author in async_authors.values()
+    ]
+    await store.redis_store.delete(*keys)
+
+    for key in keys:
+        item_in_redis = await store.redis_store.hgetall(name=key)
+        assert item_in_redis == {}
+
+    await AsyncBook.insert(async_books)
+
+    async with store.redis_store.pipeline() as pipeline:
+        for book_key in book_keys:
+            pipeline.hgetall(name=book_key)
+        async_books_in_redis = await pipeline.execute()
+
+    async_books_in_redis_as_models = [
+        await __deserialize_book_data(book) for book in async_books_in_redis
+    ]
+    assert async_books == async_books_in_redis_as_models
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_bulk_nested_insert(store):
+    """Providing a list of Model instances to the insert method also upserts their nested records in redis"""
+    book_keys = [f"asyncbook_%&_{book.title}" for book in async_books]
+    author_keys = [f"asyncauthor_%&_{author.name}" for author in async_authors.values()]
+    keys = book_keys + author_keys
+
+    for key in keys:
+        item_in_redis = await store.redis_store.hgetall(name=key)
+        assert item_in_redis == {}
+
+    await AsyncBook.insert(async_books)
+
+    async with store.redis_store.pipeline() as pipeline:
+        for key in author_keys:
+            pipeline.hgetall(name=key)
+        async_authors_in_redis = await pipeline.execute()
+    async_authors_in_redis_as_models = sorted(
+        [
+            AsyncAuthor(**AsyncAuthor.deserialize_partially(author))
+            for author in async_authors_in_redis
+        ],
+        key=lambda x: x.name,
+    )
+    expected = sorted(async_authors.values(), key=lambda x: x.name)
+    assert expected == async_authors_in_redis_as_models
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_insert_single(store):
+    """
+    Providing a single Model instance inserts that record in redis
+    """
+    key = f"asyncbook_%&_{async_books[0].title}"
+    book = await store.redis_store.hgetall(name=key)
+    assert book == {}
+
+    await AsyncBook.insert(async_books[0])
+
+    book = await store.redis_store.hgetall(name=key)
+    book_as_model = await __deserialize_book_data(book)
+    assert async_books[0] == book_as_model
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_insert_single_nested(store):
+    """
+    Providing a single Model instance upserts also any nested model into redis
+    """
+    key = f"asyncauthor_%&_{async_books[0].author.name}"
+    author = await store.redis_store.hgetall(name=key)
+    assert author == {}
+
+    await AsyncBook.insert(async_books[0])
+
+    author = await store.redis_store.hgetall(name=key)
+    author_as_model = AsyncAuthor(**AsyncAuthor.deserialize_partially(author))
+    assert async_books[0].author == author_as_model
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_update_nested_list_of_models(store):
+    data = [
+        AsyncLibrary(name="Babel AsyncLibrary", address="In a book", books=async_books)
+    ]
+    await AsyncLibrary.insert(data)
+    # the list of nested models is automatically inserted
+    got = sorted(await AsyncBook.select(), key=lambda x: x.title)
+    expected = sorted(async_books, key=lambda x: x.title)
+    assert expected == got
+
+    got = sorted(await AsyncLibrary.select(), key=lambda x: x.name)
+    expected = sorted(data, key=lambda x: x.name)
+    assert got == expected
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_update_optional_nested_list_of_models(store):
+    data = [
+        AsyncLibrary(name="Babel AsyncLibrary", address="In a book", lost=async_books)
+    ]
+    await AsyncLibrary.insert(data)
+    # the list of nested models is automatically inserted
+    got = sorted(await AsyncBook.select(), key=lambda x: x.title)
+    expected = sorted(async_books, key=lambda x: x.title)
+    assert expected == got
+
+    got = sorted(await AsyncLibrary.select(), key=lambda x: x.name)
+    expected = sorted(data, key=lambda x: x.name)
+    assert got == expected
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_update_nested_tuple_of_models(store):
+    jane = async_authors["jane"]
+    new_stuff = (async_books[0], jane, async_books[1], 8)
+    data = [AsyncLibrary(name="Babel AsyncLibrary", address="In a book", new=new_stuff)]
+    await AsyncLibrary.insert(data)
+    # the tuple of nested models is automatically inserted
+    got = sorted(await AsyncBook.select(), key=lambda x: x.title)
+    expected = sorted([async_books[0], async_books[1]], key=lambda x: x.title)
+    assert expected == got
+
+    got = sorted(await AsyncAuthor.select(), key=lambda x: x.name)
+    expected = sorted([async_books[0].author, jane], key=lambda x: x.name)
+    assert expected == got
+
+    got = sorted(await AsyncLibrary.select(), key=lambda x: x.name)
+    expected = sorted(data, key=lambda x: x.name)
+    assert got == expected
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_update_optional_nested_tuple_of_models(store):
+    popular_async_books = (async_books[0], async_books[2])
+    data = [
+        AsyncLibrary(
+            name="Babel AsyncLibrary", address="In a book", popular=popular_async_books
+        )
+    ]
+    await AsyncLibrary.insert(data)
+    # the tuple of nested models is automatically inserted
+    got = sorted(await AsyncBook.select(), key=lambda x: x.title)
+    expected = sorted(popular_async_books, key=lambda x: x.title)
+    assert expected == got
+
+    got = sorted(await AsyncLibrary.select(), key=lambda x: x.name)
+    expected = sorted(data, key=lambda x: x.name)
+    assert got == expected
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_select_default(store):
+    """Selecting without arguments returns all the book models"""
+    await AsyncBook.insert(async_books)
+    response = await AsyncBook.select()
+    sorted_async_books = sorted(async_books, key=lambda x: x.title)
+    sorted_response = sorted(response, key=lambda x: x.title)
+    assert sorted_async_books == sorted_response
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_select_some_columns(store):
+    """
+    Selecting some columns returns a list of dictionaries of all async_books models with only those columns
+    """
+    await AsyncBook.insert(async_books)
+    async_books_dict = {book.title: book for book in async_books}
+    columns = ["title", "author", "in_stock"]
+    response = await AsyncBook.select(columns=["title", "author", "in_stock"])
+    response_dict = {book["title"]: book for book in response}
+
+    for title, book in async_books_dict.items():
+        book_in_response = response_dict[title]
+        assert isinstance(book_in_response, dict)
+        assert sorted(book_in_response.keys()) == sorted(columns)
+
+        for column in columns:
+            if column == "author":
+                assert book_in_response[column] == getattr(book, column)
+            else:
+                assert f"{book_in_response[column]}" == f"{getattr(book, column)}"
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_select_some_ids(store):
+    """
+    Selecting some ids returns only those elements with the given ids
+    """
+    await AsyncBook.insert(async_books)
+    ids = [book.title for book in async_books[:2]]
+    response = await AsyncBook.select(ids=ids)
+    assert response == async_books[:2]
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_update(store):
+    """
+    Updating an item of a given primary key updates it in redis
+    """
+    await AsyncBook.insert(async_books)
+    title = async_books[0].title
+    new_in_stock = not async_books[0].in_stock
+    new_author = AsyncAuthor(name="John Doe", active_years=(2000, 2009))
+    book_key = f"asyncbook_%&_{title}"
+    new_author_key = f"asyncauthor_%&_{new_author.name}"
+    old_book_data = await store.redis_store.hgetall(name=book_key)
+    old_book = await __deserialize_book_data(old_book_data)
+    assert old_book == async_books[0]
+    assert old_book.author != new_author
+
+    await AsyncBook.update(
+        _id=title, data={"author": new_author, "in_stock": new_in_stock}
+    )
+
+    book_data = await store.redis_store.hgetall(name=book_key)
+    book = await __deserialize_book_data(book_data)
+    author_data = await store.redis_store.hgetall(name=new_author_key)
+    author = AsyncAuthor(**AsyncAuthor.deserialize_partially(author_data))
+    assert book.author == new_author
+    assert author == new_author
+    assert book.title == old_book.title
+    assert book.in_stock == new_in_stock
+    assert book.published_on == old_book.published_on
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_update_nested_model(store):
+    """
+    Updating a nested model, without changing its primary key, also updates it its collection in redis
+    """
+    await AsyncBook.insert(async_books)
+
+    new_in_stock = not async_books[0].in_stock
+    updated_author = AsyncAuthor(**async_books[0].author.dict())
+    updated_author.active_years = (2020, 2045)
+    book_key = f"asyncbook_%&_{async_books[0].title}"
+    author_key = f"asyncauthor_%&_{updated_author.name}"
+
+    old_author_data = await store.redis_store.hgetall(name=author_key)
+    old_author = AsyncAuthor(**AsyncAuthor.deserialize_partially(old_author_data))
+    old_book_data = await store.redis_store.hgetall(name=book_key)
+    old_book = await __deserialize_book_data(old_book_data)
+    assert old_book == async_books[0]
+    assert old_author == async_books[0].author
+    assert old_author != updated_author
+
+    await AsyncBook.update(
+        _id=async_books[0].title,
+        data={"author": updated_author, "in_stock": new_in_stock},
+    )
+
+    book_data = await store.redis_store.hgetall(name=book_key)
+    book = await __deserialize_book_data(book_data)
+    author_data = await store.redis_store.hgetall(name=author_key)
+    author = AsyncAuthor(**AsyncAuthor.deserialize_partially(author_data))
+    assert book.author == updated_author
+    assert author == updated_author
+    assert book.title == old_book.title
+    assert book.in_stock == new_in_stock
+    assert book.published_on == old_book.published_on
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("store", async_redis_store_fixture)
+async def test_delete_multiple(store):
+    """
+    Providing a list of ids to the delete function will remove the items from redis,
+    but leave the nested models intact
+    """
+    await AsyncBook.insert(async_books)
+    async_books_to_delete = async_books[:2]
+    async_books_left_in_db = async_books[2:]
+
+    ids_to_delete = [book.title for book in async_books_to_delete]
+    ids_to_leave_intact = [book.title for book in async_books_left_in_db]
+
+    keys_to_delete = [f"asyncbook_%&_{_id}" for _id in ids_to_delete]
+    book_keys_to_leave_intact = [f"asyncbook_%&_{_id}" for _id in ids_to_leave_intact]
+    author_keys_to_leave_intact = [
+        f"asyncauthor_%&_{author.name}" for author in async_authors.values()
+    ]
+
+    await AsyncBook.delete(ids=ids_to_delete)
+
+    for key in keys_to_delete:
+        deleted_book_in_redis = await store.redis_store.hgetall(name=key)
+        assert deleted_book_in_redis == {}
+
+    async with store.redis_store.pipeline() as pipeline:
+        for key in book_keys_to_leave_intact:
+            pipeline.hgetall(name=key)
+        async_books_in_redis = await pipeline.execute()
+        async_books_in_redis_as_models = [
+            await __deserialize_book_data(book) for book in async_books_in_redis
+        ]
+        assert async_books_left_in_db == async_books_in_redis_as_models
+
+        for key in author_keys_to_leave_intact:
+            pipeline.hgetall(name=key)
+        async_authors_in_redis = await pipeline.execute()
+        async_authors_in_redis_as_models = sorted(
+            [
+                AsyncAuthor(**AsyncAuthor.deserialize_partially(author))
+                for author in async_authors_in_redis
+            ],
+            key=lambda x: x.name,
+        )
+        expected = sorted(async_authors.values(), key=lambda x: x.name)
+        assert expected == async_authors_in_redis_as_models
+
+
+async def __deserialize_book_data(raw_book_data: Dict[str, Any]) -> AsyncBook:
+    """Deserializes the raw book data returning a book instance"""
+    author_id = raw_book_data.pop(f"{NESTED_MODEL_PREFIX}author")
+    author_id = strip_leading(author_id, "asyncauthor_%&_")
+
+    data = AsyncBook.deserialize_partially(raw_book_data)
+
+    data["author"] = (await AsyncAuthor.select(ids=[author_id]))[0]
+    return AsyncBook(**data)
diff --git a/test/test_pydantic_redis.py b/test/test_pydantic_redis.py
index 1e944362..d8d52eaf 100644
--- a/test/test_pydantic_redis.py
+++ b/test/test_pydantic_redis.py
@@ -1,11 +1,13 @@
 """Tests for the redis orm"""
 
-from typing import Dict, Any, List
+from typing import Dict, Any
 
 import pytest
 
-from pydantic_redis.config import RedisConfig
-from pydantic_redis.model import Model, strip_leading, NESTED_MODEL_PREFIX
+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.syncio.model import Model
 from test.conftest import (
     redis_store_fixture,
     Book,
@@ -13,7 +15,6 @@
     Author,
     authors,
     Library,
-    libraries,
 )