WIP on README
This commit is contained in:
parent
09c91fb756
commit
7c0bea751b
7 changed files with 319 additions and 49 deletions
156
README.md
156
README.md
|
@ -44,9 +44,118 @@ This *preview release* includes our first major component: a **declarative model
|
|||
|
||||
### Object Mapping
|
||||
|
||||
With Redis OM, you get powerful data modeling, validation, and query expressions with a small amount of code.
|
||||
With Redis OM, you get powerful data modeling, extensible data validation with [Pydantic](pydantic-url), and rich query expressions with a small amount of code.
|
||||
|
||||
Check out this example of data modeling and validation. First, we're going to create a `Customer` model that we can use to save data to Redis.
|
||||
|
||||
```python
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import EmailStr
|
||||
|
||||
from redis_om.model import (
|
||||
HashModel,
|
||||
)
|
||||
|
||||
|
||||
class Customer(HashModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: EmailStr
|
||||
join_date: datetime.date
|
||||
age: int
|
||||
bio: Optional[str]
|
||||
```
|
||||
|
||||
Here, we've defined a `Customer` model with the `HashModel` class from redis-om. This model will save data in Redis as a [Redis Hash](https://redis.io/topics/data-types).
|
||||
|
||||
Next, let's see how Redis OM makes it easy to save and retrieve `Customer` data in Redis.
|
||||
|
||||
```python
|
||||
# We can create a new Customer object:
|
||||
andrew = Customer(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="andrew.brookins@example.com",
|
||||
join_date=datetime.date.today(),
|
||||
age=38,
|
||||
bio="Python developer, works at Redis, Inc."
|
||||
)
|
||||
|
||||
# The model generates a globally unique primary key automatically without
|
||||
# needing to talk to Redis.
|
||||
print(andrew.pk)
|
||||
# '01FJM6PH661HCNNRC884H6K30C'
|
||||
|
||||
# We can save the model to Redis.
|
||||
andrew.save()
|
||||
|
||||
# Now, we can retrieve this customer with its primary key:
|
||||
other_andrew = Customer.get('01FJM6PH661HCNNRC884H6K30C')
|
||||
|
||||
# The original model and this one pass an equality check.
|
||||
assert other_andrew == andrew
|
||||
```
|
||||
|
||||
Now, let's talk about **validation**. Did you notice the type annotation for the `email` field was `EmailStr`?
|
||||
|
||||
`EmailStr` is a [Pydantic field validator](https://pydantic-docs.helpmanual.io/usage/types/). Because every Redis OM model is also a Pydantic model, you can use Pydantic validators like `EmailStr`, `Pattern`, and many more!
|
||||
|
||||
Let's see what happens if we try to instantiate our `Customer` class with an invalid email address.
|
||||
|
||||
```python
|
||||
# We'll get a validation error if we try to use an invalid email address!
|
||||
Customer(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="Not an email address!",
|
||||
join_date=datetime.date.today(),
|
||||
age=38,
|
||||
bio="Python developer, works at Redis, Inc."
|
||||
)
|
||||
# Traceback:
|
||||
# pydantic.error_wrappers.ValidationError: 1 validation error for Customer
|
||||
# email
|
||||
# value is not a valid email address (type=value_error.email)
|
||||
|
||||
# We'll also get a validation error if we try to save a model
|
||||
# instance with an invalid email.
|
||||
andrew = Customer(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="andrew.brookins@example.com",
|
||||
join_date=datetime.date.today(),
|
||||
age=38,
|
||||
bio="Python developer, works at Redis, Inc."
|
||||
)
|
||||
|
||||
# Sometime later...
|
||||
andrew.email = "Not valid"
|
||||
andrew.save()
|
||||
|
||||
# Traceback:
|
||||
# pydantic.error_wrappers.ValidationError: 1 validation error for Customer
|
||||
# email
|
||||
# value is not a valid email address (type=value_error.email)
|
||||
```
|
||||
|
||||
Data modeling, validation, and persistent to Redis all work regardless of where you run Redis. But can we do more?
|
||||
|
||||
Yes, we can! Next, we'll talk about the **rich query expressions** and **embedded models** that Redis OM gives you when you're using the RediSearch and RedisJSON Redis modules.
|
||||
|
||||
### Querying
|
||||
Querying uses a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee.
|
||||
|
||||
The example code defines `Address` and `Customer` models for use with a Redis database with the [RedisJSON](redis-json-url) module installed.
|
||||
|
||||
With these two classes defined, you can now:
|
||||
|
||||
* Validate data based on the model's type annotations using Pydantic
|
||||
* Persist model instances to Redis as JSON
|
||||
* Instantiate model instances from Redis by primary key (a client-generated [ULID](ulid-url))
|
||||
* Query on any indexed fields in the models
|
||||
|
||||
Check out this example:
|
||||
|
||||
```python
|
||||
import datetime
|
||||
|
@ -80,18 +189,6 @@ class Customer(JsonModel):
|
|||
address: Address
|
||||
```
|
||||
|
||||
The example code defines `Address` and `Customer` models for use with a Redis database with the [RedisJSON](redis-json-url) module installed.
|
||||
|
||||
With these two classes defined, you can now:
|
||||
|
||||
* Validate data based on the model's type annotations using [Pydantic](pydantic-url)
|
||||
* Persist model instances to Redis as JSON
|
||||
* Instantiate model instances from Redis by primary key (a client-generated [ULID](ulid-url))
|
||||
* Query on any indexed fields in the models
|
||||
|
||||
### Querying
|
||||
Querying uses a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee.
|
||||
|
||||
Here are a few example queries that use the models we defined earlier:
|
||||
|
||||
```python
|
||||
|
@ -139,34 +236,9 @@ hit us up on the [Redis Discord Server](http://discord.gg/redis).
|
|||
|
||||
## ✨ RediSearch and RedisJSON
|
||||
|
||||
Redis OM relies on core features from two source available Redis modules: **RediSearch** and **RedisJSON**.
|
||||
Some advanced features of Redis OM rely on core features from two source available Redis modules: **RediSearch** and **RedisJSON**.
|
||||
|
||||
These modules are the "magic" behind the scenes:
|
||||
|
||||
* RediSearch adds querying, indexing, and full-text search to Redis
|
||||
* RedisJSON adds the JSON data type to Redis
|
||||
|
||||
### Why this is important
|
||||
|
||||
Without RediSearch or RedisJSON installed, you can still use Redis OM to create declarative models backed by Redis.
|
||||
|
||||
We'll store your model data in Redis as Hashes, and you can retrieve models using their primary keys. You'll also get all the validation features from Pydantic.
|
||||
|
||||
So, what won't work without these modules?
|
||||
|
||||
1. Without RedisJSON, you won't be able to nest models inside each other, like we did with the example model of a `Customer` model that has an `Address` embedded inside it.
|
||||
2. Without RediSearch, you won't be able to use our expressive queries to find models -- just primary keys.
|
||||
|
||||
### So how do you get RediSearch and RedisJSON?
|
||||
|
||||
You can use RediSearch and RedisJSON with your self-hosted Redis deployment. Just follow the instructions on installing the binary versions of the modules in their Quick Start Guides:
|
||||
|
||||
- [RedisJSON Quick Start - Running Binaries](https://oss.redis.com/redisjson/#download-and-running-binaries)
|
||||
- [RediSearch Quick Start - Running Binaries](https://oss.redis.com/redisearch/Quick_Start/#download_and_running_binaries)
|
||||
|
||||
**NOTE**: Both Quick Start Guides also have instructions on how to run these modules in Redis with Docker.
|
||||
|
||||
Don't want to run Redis yourself? RediSearch and RedisJSON are also available on Redis Cloud. [Get started here.](https://redis.com/try-free/)
|
||||
To learn more, read [our documentation](docs/redis_modules.md).
|
||||
|
||||
## ❤️ Contributing
|
||||
|
||||
|
@ -178,7 +250,7 @@ You can also **contribute documentation** -- or just let us know if something ne
|
|||
|
||||
## License
|
||||
|
||||
Redis OM is [MIT licensed][license-url].
|
||||
Redis OM uses the [BSD 3-Clause license][license-url].
|
||||
|
||||
<!-- Badges -->
|
||||
|
||||
|
|
72
demo.py
Normal file
72
demo.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import EmailStr
|
||||
|
||||
from redis_om.model import (
|
||||
HashModel
|
||||
)
|
||||
|
||||
|
||||
class Customer(HashModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: EmailStr
|
||||
join_date: datetime.date
|
||||
age: int
|
||||
bio: Optional[str]
|
||||
|
||||
|
||||
# Now we can create new Customer objects:
|
||||
andrew = Customer(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="andrew.brookins@example.com",
|
||||
join_date=datetime.date.today(),
|
||||
age=38,
|
||||
bio="Python developer, works at Redis, Inc."
|
||||
)
|
||||
|
||||
# The model generates a globally unique primary key automatically.
|
||||
print(andrew.pk)
|
||||
# '01FJM6PH661HCNNRC884H6K30C'
|
||||
|
||||
# You can save the model to Redis.
|
||||
andrew.save()
|
||||
|
||||
# Later, you can retrieve this customer with its primary key:
|
||||
other_andrew = Customer.get('01FJM6PH661HCNNRC884H6K30C')
|
||||
|
||||
# The original model and this one pass an equality check.
|
||||
assert other_andrew == andrew
|
||||
|
||||
|
||||
# We'll get a validation error if we try to use an invalid email address!
|
||||
Customer(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="Not an email address!",
|
||||
join_date=datetime.date.today(),
|
||||
age=38,
|
||||
bio="Python developer, works at Redis, Inc."
|
||||
)
|
||||
|
||||
# We'll also get a validation error if we try to save a model
|
||||
# instance with an invalid email.
|
||||
andrew = Customer(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="andrew.brookins@example.com",
|
||||
join_date=datetime.date.today(),
|
||||
age=38,
|
||||
bio="Python developer, works at Redis, Inc."
|
||||
)
|
||||
|
||||
# Sometime later...
|
||||
andrew.email = "Not valid"
|
||||
andrew.save()
|
||||
|
||||
# Traceback:
|
||||
# pydantic.error_wrappers.ValidationError: 1 validation error for Customer
|
||||
# email
|
||||
# value is not a valid email address (type=value_error.email)
|
30
docs/redis_modules.md
Normal file
30
docs/redis_modules.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Redis Modules
|
||||
|
||||
Some advanced features of Redis OM, like rich query expressions and saving data as JSON, rely on core features from two source available Redis modules: **RediSearch** and **RedisJSON**.
|
||||
|
||||
These modules are the "magic" behind the scenes:
|
||||
|
||||
* RediSearch adds querying, indexing, and full-text search to Redis
|
||||
* RedisJSON adds the JSON data type to Redis
|
||||
|
||||
## Why this is important
|
||||
|
||||
Without RediSearch or RedisJSON installed, you can still use Redis OM to create declarative models backed by Redis.
|
||||
|
||||
We'll store your model data in Redis as Hashes, and you can retrieve models using their primary keys. You'll also get all the validation features from Pydantic.
|
||||
|
||||
So, what won't work without these modules?
|
||||
|
||||
1. Without RedisJSON, you won't be able to nest models inside each other, like we did with the example model of a `Customer` model that has an `Address` embedded inside it.
|
||||
2. Without RediSearch, you won't be able to use our expressive queries to find models -- just primary keys.
|
||||
|
||||
## So how do you get RediSearch and RedisJSON?
|
||||
|
||||
You can use RediSearch and RedisJSON with your self-hosted Redis deployment. Just follow the instructions on installing the binary versions of the modules in their Quick Start Guides:
|
||||
|
||||
- [RedisJSON Quick Start - Running Binaries](https://oss.redis.com/redisjson/#download-and-running-binaries)
|
||||
- [RediSearch Quick Start - Running Binaries](https://oss.redis.com/redisearch/Quick_Start/#download_and_running_binaries)
|
||||
|
||||
**NOTE**: Both Quick Start Guides also have instructions on how to run these modules in Redis with Docker.
|
||||
|
||||
Don't want to run Redis yourself? RediSearch and RedisJSON are also available on Redis Cloud. [Get started here.](https://redis.com/try-free/)
|
37
poetry.lock
generated
37
poetry.lock
generated
|
@ -217,6 +217,21 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.1.0"
|
||||
description = "DNS toolkit"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
dnssec = ["cryptography (>=2.6)"]
|
||||
doh = ["requests", "requests-toolbelt"]
|
||||
idna = ["idna (>=2.1)"]
|
||||
curio = ["curio (>=1.2)", "sniffio (>=1.1)"]
|
||||
trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.17.1"
|
||||
|
@ -225,6 +240,18 @@ category = "dev"
|
|||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "1.1.3"
|
||||
description = "A robust email syntax and deliverability validation library for Python 2.x/3.x."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
dnspython = ">=1.15.0"
|
||||
idna = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "1.9.0"
|
||||
|
@ -1013,7 +1040,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
|
|||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "6b31dc9e814263cba72d68f76b04bc3af635c685c6281a29e962d53cb05b2d0f"
|
||||
content-hash = "de30b2382aaeb2fe0675658bce5a3e5bc21a14e85c66d94a90054bc73a7831cd"
|
||||
|
||||
[metadata.files]
|
||||
aioredis = [
|
||||
|
@ -1185,10 +1212,18 @@ decorator = [
|
|||
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
|
||||
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
||||
]
|
||||
dnspython = [
|
||||
{file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"},
|
||||
{file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"},
|
||||
]
|
||||
docutils = [
|
||||
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
|
||||
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
|
||||
]
|
||||
email-validator = [
|
||||
{file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"},
|
||||
{file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"},
|
||||
]
|
||||
execnet = [
|
||||
{file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"},
|
||||
{file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"},
|
||||
|
|
|
@ -32,6 +32,7 @@ coverage = "^6.0.2"
|
|||
pytest-cov = "^3.0.0"
|
||||
pytest-xdist = "^2.4.0"
|
||||
twine = "^3.4.2"
|
||||
email-validator = "^1.1.3"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
migrate = "redis_om.model.cli.migrate:migrate"
|
||||
|
|
|
@ -31,7 +31,7 @@ import redis
|
|||
from pydantic import BaseModel, validator
|
||||
from pydantic.fields import FieldInfo as PydanticFieldInfo
|
||||
from pydantic.fields import ModelField, Undefined, UndefinedType
|
||||
from pydantic.main import ModelMetaclass
|
||||
from pydantic.main import ModelMetaclass, validate_model
|
||||
from pydantic.typing import NoArgAnyCallable
|
||||
from pydantic.utils import Representation
|
||||
from redis.client import Pipeline
|
||||
|
@ -1162,15 +1162,16 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
# TODO: Add transaction support
|
||||
return [model.save() for model in models]
|
||||
|
||||
@classmethod
|
||||
def values(cls):
|
||||
"""Return raw values from Redis instead of model instances."""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def redisearch_schema(cls):
|
||||
raise NotImplementedError
|
||||
|
||||
def check(self):
|
||||
"""Run all validations."""
|
||||
*_, validation_error = validate_model(self.__class__, self.__dict__)
|
||||
if validation_error:
|
||||
raise validation_error
|
||||
|
||||
|
||||
class HashModel(RedisModel, abc.ABC):
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
|
@ -1190,6 +1191,7 @@ class HashModel(RedisModel, abc.ABC):
|
|||
)
|
||||
|
||||
def save(self, pipeline: Optional[Pipeline] = None) -> "HashModel":
|
||||
self.check()
|
||||
if pipeline is None:
|
||||
db = self.db()
|
||||
else:
|
||||
|
@ -1226,6 +1228,12 @@ class HashModel(RedisModel, abc.ABC):
|
|||
schema_parts = [schema_prefix] + cls.schema_for_fields()
|
||||
return " ".join(schema_parts)
|
||||
|
||||
def update(self, **field_values):
|
||||
validate_model_fields(self.__class__, field_values)
|
||||
for field, value in field_values.items():
|
||||
setattr(self, field, value)
|
||||
self.save()
|
||||
|
||||
@classmethod
|
||||
def schema_for_fields(cls):
|
||||
schema_parts = []
|
||||
|
@ -1312,6 +1320,7 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
cls.redisearch_schema()
|
||||
|
||||
def save(self, pipeline: Optional[Pipeline] = None) -> "JsonModel":
|
||||
self.check()
|
||||
if pipeline is None:
|
||||
db = self.db()
|
||||
else:
|
||||
|
@ -1320,6 +1329,7 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
return self
|
||||
|
||||
def update(self, **field_values):
|
||||
# TODO: Better support for embedded field models.
|
||||
validate_model_fields(self.__class__, field_values)
|
||||
for field, value in field_values.items():
|
||||
setattr(self, field, value)
|
||||
|
|
50
tests/test_pydantic_integrations.py
Normal file
50
tests/test_pydantic_integrations.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
import abc
|
||||
import datetime
|
||||
from collections import namedtuple
|
||||
|
||||
import pytest
|
||||
from pydantic import EmailStr, ValidationError
|
||||
|
||||
from redis_om.model import HashModel, Field
|
||||
from redis_om.model.migrations.migrator import Migrator
|
||||
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def m(key_prefix):
|
||||
class BaseHashModel(HashModel, abc.ABC):
|
||||
class Meta:
|
||||
global_key_prefix = key_prefix
|
||||
|
||||
class Member(BaseHashModel):
|
||||
first_name: str
|
||||
last_name: str
|
||||
email: EmailStr = Field(index=True)
|
||||
join_date: datetime.date
|
||||
age: int
|
||||
|
||||
Migrator().run()
|
||||
|
||||
return namedtuple("Models", ["Member"])(Member)
|
||||
|
||||
|
||||
def test_email_str(m):
|
||||
with pytest.raises(ValidationError):
|
||||
m.Member(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="not an email!",
|
||||
age=38,
|
||||
join_date=today,
|
||||
)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
m.Member(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="andrew@bad-domain",
|
||||
age=38,
|
||||
join_date=today,
|
||||
)
|
Loading…
Reference in a new issue