From dc451401ca1db16f63bfde0590318354402e114b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Oct 2021 00:27:54 +0000 Subject: [PATCH 01/49] Bump actions/checkout from 2.3.4 to 2.3.5 Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2.3.4...v2.3.5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2de3183..42aea55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Setup Python 3.9 uses: actions/setup-python@v2 with: @@ -99,7 +99,7 @@ jobs: INSTALL_DIR: ${{ github.workspace }}/redis steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Setup Python ${{ matrix.pyver }} uses: actions/setup-python@v2 with: @@ -153,7 +153,7 @@ jobs: if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2.3.5 - name: Setup Python 3.9 uses: actions/setup-python@v2 with: From 9b18dae2eb32c8555e6c9d720598f0e47277b3a9 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 06:34:59 -0700 Subject: [PATCH 02/49] Change to BSD 3-clause license --- LICENSE | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/LICENSE b/LICENSE index 3c1d366..f829336 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,26 @@ -The MIT License (MIT) +Copyright 2021 Redis, Inc. -Copyright (c) 2021-present Redis, Inc. +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file From c9967b0d407b55c9bafe800d0e7fb3aea6873657 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 06:36:15 -0700 Subject: [PATCH 03/49] Rename to redis_om --- Makefile | 2 +- README.md | 18 +++++++++--------- pyproject.toml | 4 ++-- {redis_developer => redis_om}/__init__.py | 0 {redis_developer => redis_om}/connections.py | 0 .../model/__init__.py | 0 .../model/cli/__init__.py | 0 .../model/cli/migrate.py | 4 ++-- .../model/encoders.py | 0 .../model/migrations/__init__.py | 0 .../model/migrations/migrator.py | 4 ++-- {redis_developer => redis_om}/model/model.py | 2 +- {redis_developer => redis_om}/model/models.py | 6 +++--- .../model/query_resolver.py | 2 +- .../model/render_tree.py | 0 .../model/token_escaper.py | 0 tests/conftest.py | 4 ++-- tests/test_hash_model.py | 6 +++--- tests/test_json_model.py | 6 +++--- 19 files changed, 29 insertions(+), 29 deletions(-) rename {redis_developer => redis_om}/__init__.py (100%) rename {redis_developer => redis_om}/connections.py (100%) rename {redis_developer => redis_om}/model/__init__.py (100%) rename {redis_developer => redis_om}/model/cli/__init__.py (100%) rename {redis_developer => redis_om}/model/cli/migrate.py (72%) rename {redis_developer => redis_om}/model/encoders.py (100%) rename {redis_developer => redis_om}/model/migrations/__init__.py (100%) rename {redis_developer => redis_om}/model/migrations/migrator.py (97%) rename {redis_developer => redis_om}/model/model.py (99%) rename {redis_developer => redis_om}/model/models.py (75%) rename {redis_developer => redis_om}/model/query_resolver.py (97%) rename {redis_developer => redis_om}/model/render_tree.py (100%) rename {redis_developer => redis_om}/model/token_escaper.py (100%) diff --git a/Makefile b/Makefile index fa06195..9c88b0b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -NAME := redis_developer +NAME := redis_om INSTALL_STAMP := .install.stamp POETRY := $(shell command -v poetry 2> /dev/null) diff --git a/README.md b/README.md index 246dfd4..950ad06 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Check out this example: import datetime from typing import Optional -from redis_developer.model import ( +from redis_om.model import ( EmbeddedJsonModel, JsonModel, Field, @@ -172,9 +172,9 @@ Don't want to run Redis yourself? RediSearch and RedisJSON are also available on We'd love your contributions! -**Bug reports** are especially helpful at this stage of the project. [You can open a bug report on GitHub](https://github.com/redis-developer/redis-developer-python/issues/new). +**Bug reports** are especially helpful at this stage of the project. [You can open a bug report on GitHub](https://github.com/redis-om/redis-om-python/issues/new). -You can also **contribute documentation** -- or just let us know if something needs more detail. [Open an issue on GitHub](https://github.com/redis-developer/redis-developer-python/issues/new) to get started. +You can also **contribute documentation** -- or just let us know if something needs more detail. [Open an issue on GitHub](https://github.com/redis-om/redis-om-python/issues/new) to get started. ## License @@ -184,17 +184,17 @@ Redis OM is [MIT licensed][license-url]. [version-svg]: https://img.shields.io/pypi/v/redis-om?style=flat-square [package-url]: https://pypi.org/project/redis-om/ -[ci-svg]: https://img.shields.io/github/workflow/status/redis-developer/redis-developer-python/python?style=flat-square -[ci-url]: https://github.com/redis-developer/redis-developer-python/actions/workflows/build.yml +[ci-svg]: https://img.shields.io/github/workflow/status/redis-om/redis-om-python/python?style=flat-square +[ci-url]: https://github.com/redis-om/redis-om-python/actions/workflows/build.yml [license-image]: http://img.shields.io/badge/license-MIT-green.svg?style=flat-square [license-url]: LICENSE -[redis-developer-website]: https://developer.redis.com -[redis-om-js]: https://github.com/redis-developer/redis-om-js -[redis-om-dotnet]: https://github.com/redis-developer/redis-om-dotnet -[redis-om-spring]: https://github.com/redis-developer/redis-om-spring +[redis-om-website]: https://developer.redis.com +[redis-om-js]: https://github.com/redis-om/redis-om-js +[redis-om-dotnet]: https://github.com/redis-om/redis-om-dotnet +[redis-om-spring]: https://github.com/redis-om/redis-om-spring [redisearch-url]: https://oss.redis.com/redisearch/ [redis-json-url]: https://oss.redis.com/redisjson/ [pydantic-url]: https://github.com/samuelcolvin/pydantic diff --git a/pyproject.toml b/pyproject.toml index 4f56fe3..601cf08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "redis-developer" +name = "redis-om" version = "0.1.0" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] @@ -33,7 +33,7 @@ pytest-xdist = "^2.4.0" [tool.poetry.scripts] -migrate = "redis_developer.orm.cli.migrate:migrate" +migrate = "redis_om.orm.cli.migrate:migrate" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/redis_developer/__init__.py b/redis_om/__init__.py similarity index 100% rename from redis_developer/__init__.py rename to redis_om/__init__.py diff --git a/redis_developer/connections.py b/redis_om/connections.py similarity index 100% rename from redis_developer/connections.py rename to redis_om/connections.py diff --git a/redis_developer/model/__init__.py b/redis_om/model/__init__.py similarity index 100% rename from redis_developer/model/__init__.py rename to redis_om/model/__init__.py diff --git a/redis_developer/model/cli/__init__.py b/redis_om/model/cli/__init__.py similarity index 100% rename from redis_developer/model/cli/__init__.py rename to redis_om/model/cli/__init__.py diff --git a/redis_developer/model/cli/migrate.py b/redis_om/model/cli/migrate.py similarity index 72% rename from redis_developer/model/cli/migrate.py rename to redis_om/model/cli/migrate.py index 28880c0..5c3c442 100644 --- a/redis_developer/model/cli/migrate.py +++ b/redis_om/model/cli/migrate.py @@ -1,10 +1,10 @@ import click -from redis_developer.model.migrations.migrator import Migrator +from redis_om.model.migrations.migrator import Migrator @click.command() -@click.option("--module", default="redis_developer") +@click.option("--module", default="redis_om") def migrate(module): migrator = Migrator(module) diff --git a/redis_developer/model/encoders.py b/redis_om/model/encoders.py similarity index 100% rename from redis_developer/model/encoders.py rename to redis_om/model/encoders.py diff --git a/redis_developer/model/migrations/__init__.py b/redis_om/model/migrations/__init__.py similarity index 100% rename from redis_developer/model/migrations/__init__.py rename to redis_om/model/migrations/__init__.py diff --git a/redis_developer/model/migrations/migrator.py b/redis_om/model/migrations/migrator.py similarity index 97% rename from redis_developer/model/migrations/migrator.py rename to redis_om/model/migrations/migrator.py index 027a6c3..daf4c47 100644 --- a/redis_developer/model/migrations/migrator.py +++ b/redis_om/model/migrations/migrator.py @@ -6,8 +6,8 @@ from typing import Optional from redis import ResponseError -from redis_developer.connections import get_redis_connection -from redis_developer.model.model import model_registry +from redis_om.connections import get_redis_connection +from redis_om.model.model import model_registry redis = get_redis_connection() diff --git a/redis_developer/model/model.py b/redis_om/model/model.py similarity index 99% rename from redis_developer/model/model.py rename to redis_om/model/model.py index 168f49c..84f6c9c 100644 --- a/redis_developer/model/model.py +++ b/redis_om/model/model.py @@ -521,7 +521,7 @@ class FindQuery: # this is not going to work. log.warning( "Your query against the field %s is for a single character, %s, " - "that is used internally by redis-developer-python. We must ignore " + "that is used internally by redis-om-python. We must ignore " "this portion of the query. Please review your query to find " "an alternative query that uses a string containing more than " "just the character %s.", diff --git a/redis_developer/model/models.py b/redis_om/model/models.py similarity index 75% rename from redis_developer/model/models.py rename to redis_om/model/models.py index c6c1a5c..81655e5 100644 --- a/redis_developer/model/models.py +++ b/redis_om/model/models.py @@ -1,17 +1,17 @@ import abc from typing import Optional -from redis_developer.model.model import HashModel, JsonModel +from redis_om.model.model import HashModel, JsonModel class BaseJsonModel(JsonModel, abc.ABC): class Meta: - global_key_prefix = "redis-developer" + global_key_prefix = "redis-om" class BaseHashModel(HashModel, abc.ABC): class Meta: - global_key_prefix = "redis-developer" + global_key_prefix = "redis-om" # class AddressJson(BaseJsonModel): diff --git a/redis_developer/model/query_resolver.py b/redis_om/model/query_resolver.py similarity index 97% rename from redis_developer/model/query_resolver.py rename to redis_om/model/query_resolver.py index 8616c92..f27fc36 100644 --- a/redis_developer/model/query_resolver.py +++ b/redis_om/model/query_resolver.py @@ -1,7 +1,7 @@ from collections import Sequence from typing import Any, Dict, List, Mapping, Union -from redis_developer.model.model import Expression +from redis_om.model.model import Expression class LogicalOperatorForListOfExpressions(Expression): diff --git a/redis_developer/model/render_tree.py b/redis_om/model/render_tree.py similarity index 100% rename from redis_developer/model/render_tree.py rename to redis_om/model/render_tree.py diff --git a/redis_developer/model/token_escaper.py b/redis_om/model/token_escaper.py similarity index 100% rename from redis_developer/model/token_escaper.py rename to redis_om/model/token_escaper.py diff --git a/tests/conftest.py b/tests/conftest.py index 807deb3..5269b4c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import random import pytest from redis import Redis -from redis_developer.connections import get_redis_connection +from redis_om.connections import get_redis_connection @pytest.fixture @@ -21,7 +21,7 @@ def _delete_test_keys(prefix: str, conn: Redis): @pytest.fixture def key_prefix(redis): - key_prefix = f"redis-developer:{random.random()}" + key_prefix = f"redis-om:{random.random()}" yield key_prefix _delete_test_keys(key_prefix, redis) diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index 3705a84..e67a42e 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -8,9 +8,9 @@ from unittest import mock import pytest from pydantic import ValidationError -from redis_developer.model import Field, HashModel -from redis_developer.model.migrations.migrator import Migrator -from redis_developer.model.model import ( +from redis_om.model import Field, HashModel +from redis_om.model.migrations.migrator import Migrator +from redis_om.model.model import ( NotFoundError, QueryNotSupportedError, RedisModelError, diff --git a/tests/test_json_model.py b/tests/test_json_model.py index ffa6594..5cd20af 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -8,9 +8,9 @@ from unittest import mock import pytest from pydantic import ValidationError -from redis_developer.model import EmbeddedJsonModel, Field, JsonModel -from redis_developer.model.migrations.migrator import Migrator -from redis_developer.model.model import ( +from redis_om.model import EmbeddedJsonModel, Field, JsonModel +from redis_om.model.migrations.migrator import Migrator +from redis_om.model.model import ( NotFoundError, QueryNotSupportedError, RedisModelError, From 09c91fb756d86a77f1564f3c8ab0b4dbc1620357 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 06:53:39 -0700 Subject: [PATCH 04/49] Add building, linting for PyPI upload --- .github/workflows/ci.yml | 13 +- .gitignore | 3 + .install.stamp | 0 Makefile | 8 +- poetry.lock | 447 ++++++++++++++++++++++++++++++++++++++- pyproject.toml | 11 +- tests/test_hash_model.py | 6 +- tests/test_json_model.py | 6 +- 8 files changed, 470 insertions(+), 24 deletions(-) delete mode 100644 .install.stamp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f92a7fe..cf51ba1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,13 +63,12 @@ jobs: - name: Run linter run: | make lint -# - name: Prepare twine checker -# run: | -# pip install -U twine wheel -# python setup.py sdist bdist_wheel -# - name: Run twine checker -# run: | -# twine check dist/* + - name: Prepare twine checker + run: | + poetry build + - name: Run twine checker + run: | + twine check dist/* test-unix: name: Test Unix diff --git a/.gitignore b/.gitignore index 1d4e9fe..112a7e6 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ dmypy.json # Pyre type checker .pyre/ data + +# Makefile install checker +.install.stamp diff --git a/.install.stamp b/.install.stamp deleted file mode 100644 index e69de29..0000000 diff --git a/Makefile b/Makefile index 9c88b0b..8ddd2be 100644 --- a/Makefile +++ b/Makefile @@ -28,14 +28,20 @@ $(INSTALL_STAMP): pyproject.toml poetry.lock clean: find . -type d -name "__pycache__" | xargs rm -rf {}; rm -rf $(INSTALL_STAMP) .coverage .mypy_cache + -rm -r dist + +.PHONY: dist +dist: clean + $(POETRY) build .PHONY: lint -lint: $(INSTALL_STAMP) +lint: $(INSTALL_STAMP) dist $(POETRY) run isort --profile=black --lines-after-imports=2 ./tests/ $(NAME) $(POETRY) run black ./tests/ $(NAME) $(POETRY) run flake8 --ignore=W503,E501,F401,E731 ./tests/ $(NAME) $(POETRY) run mypy ./tests/ $(NAME) --ignore-missing-imports $(POETRY) run bandit -r $(NAME) -s B608 + $(POETRY) run twine check dist/* .PHONY: format format: $(INSTALL_STAMP) diff --git a/poetry.lock b/poetry.lock index ae97c84..c6d54c5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -114,6 +114,49 @@ jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] python2 = ["typed-ast (>=1.4.2)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bleach" +version = "4.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "dev" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + [[package]] name = "click" version = "8.0.3" @@ -147,6 +190,25 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "35.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "decorator" version = "5.1.0" @@ -155,6 +217,14 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "execnet" version = "1.9.0" @@ -202,6 +272,30 @@ python-versions = ">=3.7" gitdb = ">=4.0.1,<5" typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""} +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.8.1" +description = "Read metadata from Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] + [[package]] name = "iniconfig" version = "1.1.1" @@ -284,6 +378,36 @@ parso = ">=0.8.0,<0.9.0" qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] +[[package]] +name = "jeepney" +version = "0.7.1" +description = "Low-level, pure Python DBus protocol wrapper." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"] +trio = ["trio", "async-generator"] + +[[package]] +name = "keyring" +version = "23.2.1" +description = "Store and access your passwords safely." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = ">=3.6" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"] + [[package]] name = "lazy-object-proxy" version = "1.6.0" @@ -394,6 +518,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "pkginfo" +version = "1.7.1" +description = "Query metadatdata from sdists / bdists / installed packages." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +testing = ["nose", "coverage"] + [[package]] name = "platformdirs" version = "2.4.0" @@ -461,6 +596,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pycparser" +version = "2.20" +description = "C parser in Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pydantic" version = "1.8.2" @@ -602,6 +745,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "6.0" @@ -610,6 +761,22 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "readme-renderer" +version = "30.0" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +bleach = ">=2.1.0" +docutils = ">=0.13.1" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.5.0,<0.7.0)"] + [[package]] name = "redis" version = "3.5.3" @@ -629,6 +796,58 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "requests" +version = "2.26.0" +description = "Python HTTP for Humans." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "secretstorage" +version = "3.3.1" +description = "Python bindings to FreeDesktop.org Secret Service API" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "six" version = "1.16.0" @@ -672,6 +891,22 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "tqdm" +version = "4.62.3" +description = "Fast, Extensible Progress Meter" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["py-make (>=0.1.0)", "twine", "wheel"] +notebook = ["ipywidgets (>=6)"] +telegram = ["requests"] + [[package]] name = "traitlets" version = "5.1.0" @@ -683,6 +918,25 @@ python-versions = ">=3.7" [package.extras] test = ["pytest"] +[[package]] +name = "twine" +version = "3.4.2" +description = "Collection of utilities for publishing packages on PyPI" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = ">=0.4.3" +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.4.2" +readme-renderer = ">=21.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +tqdm = ">=4.14" + [[package]] name = "types-redis" version = "3.5.15" @@ -707,6 +961,19 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "urllib3" +version = "1.26.7" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + [[package]] name = "wcwidth" version = "0.2.5" @@ -715,6 +982,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "wrapt" version = "1.13.2" @@ -723,10 +998,22 @@ category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +[[package]] +name = "zipp" +version = "3.6.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] + [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "56b381dd9b79bd082e978019124176491c63f09dd5ce90e5f8ab642a7f79480f" +content-hash = "6b31dc9e814263cba72d68f76b04bc3af635c685c6281a29e962d53cb05b2d0f" [metadata.files] aioredis = [ @@ -765,6 +1052,70 @@ black = [ {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, ] +bleach = [ + {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, + {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, + {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, +] click = [ {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, @@ -808,10 +1159,36 @@ coverage = [ {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, ] +cryptography = [ + {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, + {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"}, + {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"}, + {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"}, + {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"}, + {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"}, + {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"}, + {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"}, + {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"}, +] decorator = [ {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, @@ -828,6 +1205,14 @@ gitpython = [ {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, {file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"}, ] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.8.1-py3-none-any.whl", hash = "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15"}, + {file = "importlib_metadata-4.8.1.tar.gz", hash = "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -847,6 +1232,14 @@ jedi = [ {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, ] +jeepney = [ + {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"}, + {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"}, +] +keyring = [ + {file = "keyring-23.2.1-py3-none-any.whl", hash = "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e"}, + {file = "keyring-23.2.1.tar.gz", hash = "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe"}, +] lazy-object-proxy = [ {file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"}, {file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"}, @@ -932,6 +1325,10 @@ pickleshare = [ {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +pkginfo = [ + {file = "pkginfo-1.7.1-py2.py3-none-any.whl", hash = "sha256:37ecd857b47e5f55949c41ed061eb51a0bee97a87c969219d144c0e023982779"}, + {file = "pkginfo-1.7.1.tar.gz", hash = "sha256:e7432f81d08adec7297633191bbf0bd47faf13cd8724c3a13250e51d542635bd"}, +] platformdirs = [ {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, @@ -959,6 +1356,10 @@ pycodestyle = [ {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] +pycparser = [ + {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, + {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, +] pydantic = [ {file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"}, {file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"}, @@ -1023,6 +1424,10 @@ python-ulid = [ {file = "python-ulid-1.0.3.tar.gz", hash = "sha256:5dd8b969312a40e2212cec9c1ad63f25d4b6eafd92ee3195883e0287b6e9d19e"}, {file = "python_ulid-1.0.3-py3-none-any.whl", hash = "sha256:8704dc20f547f531fe3a41d4369842d737a0f275403b909d0872e7ea0fe8d6f2"}, ] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, @@ -1058,6 +1463,10 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +readme-renderer = [ + {file = "readme_renderer-30.0-py2.py3-none-any.whl", hash = "sha256:3286806450d9961d6e3b5f8a59f77e61503799aca5155c8d8d40359b4e1e1adc"}, + {file = "readme_renderer-30.0.tar.gz", hash = "sha256:8299700d7a910c304072a7601eafada6712a5b011a20139417e1b1e9f04645d8"}, +] redis = [ {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, @@ -1111,6 +1520,22 @@ regex = [ {file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"}, {file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"}, ] +requests = [ + {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, + {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, +] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +secretstorage = [ + {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"}, + {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1131,10 +1556,18 @@ tomli = [ {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, ] +tqdm = [ + {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, + {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, +] traitlets = [ {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, ] +twine = [ + {file = "twine-3.4.2-py3-none-any.whl", hash = "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218"}, + {file = "twine-3.4.2.tar.gz", hash = "sha256:4caec0f1ed78dc4c9b83ad537e453d03ce485725f2aea57f1bb3fdde78dae936"}, +] types-redis = [ {file = "types-redis-3.5.15.tar.gz", hash = "sha256:e52be0077ca1189d8cce813a20c2a70e9e577f34ab898371c6cbed696a88bdee"}, {file = "types_redis-3.5.15-py3-none-any.whl", hash = "sha256:e617c08bff88449b52f6dbdaa9bb81a806f27c89fd30bbf98fe9683ed5d1046a"}, @@ -1148,10 +1581,18 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] +urllib3 = [ + {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] wrapt = [ {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, @@ -1198,3 +1639,7 @@ wrapt = [ {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, ] +zipp = [ + {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, + {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, +] diff --git a/pyproject.toml b/pyproject.toml index 601cf08..477edad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [tool.poetry] name = "redis-om" -version = "0.1.0" +version = "0.0.1" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." -authors = ["Andrew Brookins "] -license = "MIT" +authors = ["Andrew Brookins "] +license = "BSD-3-Clause" +readme = "README.md" [tool.poetry.dependencies] python = "^3.8" @@ -30,10 +31,10 @@ bandit = "^1.7.0" coverage = "^6.0.2" pytest-cov = "^3.0.0" pytest-xdist = "^2.4.0" - +twine = "^3.4.2" [tool.poetry.scripts] -migrate = "redis_om.orm.cli.migrate:migrate" +migrate = "redis_om.model.cli.migrate:migrate" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index e67a42e..e2a3882 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -10,11 +10,7 @@ from pydantic import ValidationError from redis_om.model import Field, HashModel from redis_om.model.migrations.migrator import Migrator -from redis_om.model.model import ( - NotFoundError, - QueryNotSupportedError, - RedisModelError, -) +from redis_om.model.model import NotFoundError, QueryNotSupportedError, RedisModelError today = datetime.date.today() diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 5cd20af..779da78 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -10,11 +10,7 @@ from pydantic import ValidationError from redis_om.model import EmbeddedJsonModel, Field, JsonModel from redis_om.model.migrations.migrator import Migrator -from redis_om.model.model import ( - NotFoundError, - QueryNotSupportedError, - RedisModelError, -) +from redis_om.model.model import NotFoundError, QueryNotSupportedError, RedisModelError today = datetime.date.today() From 7c0bea751b029a699ec9f5d20f8a64f240f7b7d7 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 08:31:08 -0700 Subject: [PATCH 05/49] WIP on README --- README.md | 156 ++++++++++++++++++++-------- demo.py | 72 +++++++++++++ docs/redis_modules.md | 30 ++++++ poetry.lock | 37 ++++++- pyproject.toml | 1 + redis_om/model/model.py | 22 ++-- tests/test_pydantic_integrations.py | 50 +++++++++ 7 files changed, 319 insertions(+), 49 deletions(-) create mode 100644 demo.py create mode 100644 docs/redis_modules.md create mode 100644 tests/test_pydantic_integrations.py diff --git a/README.md b/README.md index 950ad06..63ae933 100644 --- a/README.md +++ b/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]. diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..b3cef61 --- /dev/null +++ b/demo.py @@ -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) diff --git a/docs/redis_modules.md b/docs/redis_modules.md new file mode 100644 index 0000000..cd110dc --- /dev/null +++ b/docs/redis_modules.md @@ -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/) \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c6d54c5..0ef59fd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 477edad..8859537 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/redis_om/model/model.py b/redis_om/model/model.py index 84f6c9c..3031bf9 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -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) diff --git a/tests/test_pydantic_integrations.py b/tests/test_pydantic_integrations.py new file mode 100644 index 0000000..d2cf647 --- /dev/null +++ b/tests/test_pydantic_integrations.py @@ -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, + ) \ No newline at end of file From 1921a490596d502a8360f8bbd3719429ec7d30f8 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 09:19:06 -0700 Subject: [PATCH 06/49] Fix linting --- tests/test_pydantic_integrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_pydantic_integrations.py b/tests/test_pydantic_integrations.py index d2cf647..131e5e7 100644 --- a/tests/test_pydantic_integrations.py +++ b/tests/test_pydantic_integrations.py @@ -5,7 +5,7 @@ from collections import namedtuple import pytest from pydantic import EmailStr, ValidationError -from redis_om.model import HashModel, Field +from redis_om.model import Field, HashModel from redis_om.model.migrations.migrator import Migrator @@ -47,4 +47,4 @@ def test_email_str(m): email="andrew@bad-domain", age=38, join_date=today, - ) \ No newline at end of file + ) From 2f8ea2b9cbafaeff2dcd9d92b23afdc74765fcf6 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 10:35:32 -0700 Subject: [PATCH 07/49] Fix twine checker in CI --- .github/workflows/ci.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf51ba1..4e86f3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,13 +62,8 @@ jobs: #---------------------------------------------- - name: Run linter run: | + make dist make lint - - name: Prepare twine checker - run: | - poetry build - - name: Run twine checker - run: | - twine check dist/* test-unix: name: Test Unix From fa293e3f881a907921c882ced8f2b9e4b86c9e2b Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 12:05:45 -0700 Subject: [PATCH 08/49] Attempt to set the redis entrypoint in CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e86f3e..271c8b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,7 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + --entrypoint="redis-server --loadmodule /usr/lib/redis/modules/rejson.so --loadmodule /usr/lib/redis/modules/redisearch.so" runs-on: ${{ matrix.os }} timeout-minutes: 15 env: From 58e760dcc4c99c526ca2af3dcaf86e08f60fc82d Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 12:14:05 -0700 Subject: [PATCH 09/49] Try unquoted entrypoint --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 271c8b5..569852a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,7 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - --entrypoint="redis-server --loadmodule /usr/lib/redis/modules/rejson.so --loadmodule /usr/lib/redis/modules/redisearch.so" + --entrypoint=redis-server --loadmodule /usr/lib/redis/modules/rejson.so --loadmodule /usr/lib/redis/modules/redisearch.so runs-on: ${{ matrix.os }} timeout-minutes: 15 env: From 5a655fcc1f003155c1c567743f0aacbd76eb604c Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 12:19:13 -0700 Subject: [PATCH 10/49] I give up, CI is broken until redismod:edge works --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 569852a..4e86f3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,7 +86,6 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - --entrypoint=redis-server --loadmodule /usr/lib/redis/modules/rejson.so --loadmodule /usr/lib/redis/modules/redisearch.so runs-on: ${{ matrix.os }} timeout-minutes: 15 env: From d7dd9d361e4b3d0f40c2fd93eba78c18395717c4 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 12:20:44 -0700 Subject: [PATCH 11/49] Remove edge image from testing matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e86f3e..ce69bbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: matrix: os: [ubuntu-latest] pyver: [3.6, 3.7, 3.8, 3.9, pypy3] - redismod: ["edge", "preview"] + redismod: ["preview"] # Removed "edge" because it's broken currently fail-fast: false services: redis: From fd81e546605c145d0acca675d5dd0f541be6b837 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 22 Oct 2021 17:05:10 -0700 Subject: [PATCH 12/49] WIP on README --- README.md | 102 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 63ae933..7d33226 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

Redis OM

- Objecting mapping and more, for Redis. + Objecting mapping, and more, for Redis.

@@ -11,7 +11,7 @@ [![License][license-image]][license-url] [![Build Status][ci-svg]][ci-url] -Redis OM is a library that helps you build modern Python applications with Redis. +**Redis OM Python** makes it easy to model Redis data in your Python applications. **Redis OM Python** | [Redis OM Node.js][redis-om-js] | [Redis OM Spring][redis-om-spring] | [Redis OM .NET][redis-om-dotnet] @@ -21,32 +21,41 @@ Redis OM is a library that helps you build modern Python applications with Redis - -- [Why Redis OM?](#why) -- [Getting started](#getting-started) -- [Installation](#installation) -- [Documentation](#documentation) -- [Troubleshooting](#troubleshooting) -- [Contributing](#contributing) -- [License](#license) + - [💡 Why Redis OM?](#-why-redis-om) + - [Starting Redis](#starting-redis) + - [📇 Modeling your domain (and indexing it!)](#-modeling-your-domain-and-indexing-it) + - [🔎 Querying](#-querying) + - [Why this is important](#why-this-is-important) + - [So how do you get RediSearch and RedisJSON?](#so-how-do-you-get-redisearch-and-redisjson) + - [Why Redis OM?](#why) + - [Getting started](#getting-started) + - [Installation](#installation) + - [Documentation](#documentation) + - [Troubleshooting](#troubleshooting) + - [Contributing](#contributing) + - [License](#license) -## ➡ Why Redis OM? +## 💡 Why Redis OM? -Redis OM is a library of high-level tools that help you build modern Python applications with Redis. +Redis OM provides high-level abstractions for using Redis in Python, making it easy to model and query your Redis domain objects. -This *preview release* includes our first major component: a **declarative model class** backed by Redis. +This **preview** release contains the following features: + +* Declarative object mapping for Redis objects +* Declarative secondary-index generation +* Fluent APIs for querying Redis ## 🏁 Getting started ### Object Mapping -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. +With Redis OM, you get powerful data modeling, extensible data validation with [Pydantic](pydantic-url), and rich query expressions. -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. +Check out this example of how we'd model customer data with Redis OM. First, we're going to create a `Customer` model: ```python import datetime @@ -68,12 +77,13 @@ class Customer(HashModel): 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). +**NOTE**: Redis OM uses Python type annotations for data validation. See the _Data Validation_ section of this README for more details. -Next, let's see how Redis OM makes it easy to save and retrieve `Customer` data in Redis. +Now that we have a `Customer` model, let's use it to save customer data to Redis. + +First, we create a new `Customer` object: ```python -# We can create a new Customer object: andrew = Customer( first_name="Andrew", last_name="Brookins", @@ -82,25 +92,47 @@ andrew = Customer( age=38, bio="Python developer, works at Redis, Inc." ) +``` +The model generates a globally unique primary key automatically without needing to talk to Redis. -# The model generates a globally unique primary key automatically without -# needing to talk to Redis. +```python 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 +'01FJM6PH661HCNNRC884H6K30C' ``` -Now, let's talk about **validation**. Did you notice the type annotation for the `email` field was `EmailStr`? +We can save the model to Redis by calling `save()`: -`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! +```python +andrew.save() +``` + +To retrieve this customer with its primary key, we use `Customer.get()`: + +```python +other_andrew = Customer.get('01FJM6PH661HCNNRC884H6K30C') +``` + +Now let's see how Redis OM makes data validation a snap, thanks to [Pydantic](https://pydantic-docs.helpmanual.io/). + +### Data Validation + +When you create a model with Redis OM, you define fields and give them type annotations. As a refresher, take a look at the `Customer` model we already built: + +```python +class Customer(HashModel): + first_name: str + last_name: str + email: EmailStr + join_date: datetime.date + age: int + bio: Optional[str] +``` + +Redis OM uses [Pydantic](pydantic-url) behind the scenes to validate data at runtime, based on the model's type annotations. + +This validation works for basic cases, like ensuring that `first_name` is always a string. But every Redis OM model is also a Pydantic model, so you can use existing Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validation! + +#### A Demo Let's see what happens if we try to instantiate our `Customer` class with an invalid email address. @@ -140,9 +172,11 @@ andrew.save() # 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? +Data modeling, validation, and persisting to Redis all work regardless of how you run Redis. -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. +However, Redis OM will take your Python applications to the next level if you're using the RediSearch and RedisJSON modules in your Redis deployment. Next, we'll talk about the **rich query expressions** and **embedded models** that Redis OM gives you with those Redis modules. + +**TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the *RediSearch and RedisJSON* section of this README. ### Querying Querying uses a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee. From 491db96311815226cc0b63cafb205822b3ff0bdc Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 08:48:48 -0700 Subject: [PATCH 13/49] Tweak CI badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d33226..d481b6b 100644 --- a/README.md +++ b/README.md @@ -290,8 +290,8 @@ Redis OM uses the [BSD 3-Clause license][license-url]. [version-svg]: https://img.shields.io/pypi/v/redis-om?style=flat-square [package-url]: https://pypi.org/project/redis-om/ -[ci-svg]: https://img.shields.io/github/workflow/status/redis-om/redis-om-python/python?style=flat-square -[ci-url]: https://github.com/redis-om/redis-om-python/actions/workflows/build.yml +[ci-svg]: https://img.shields.io/github/workflow/status/redis-developer/redis-om-python/ci?style=flat-square +[ci-url]: https://github.com/redis-developer/redis-om-python/actions/workflows/ci.yml [license-image]: http://img.shields.io/badge/license-MIT-green.svg?style=flat-square [license-url]: LICENSE From b8aa295809c71fddb7dd3c10764e4f8f75ac9c0d Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 10:03:37 -0700 Subject: [PATCH 14/49] Update license badge to BSD 3-clause --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d481b6b..3ff6506 100644 --- a/README.md +++ b/README.md @@ -292,7 +292,7 @@ Redis OM uses the [BSD 3-Clause license][license-url]. [package-url]: https://pypi.org/project/redis-om/ [ci-svg]: https://img.shields.io/github/workflow/status/redis-developer/redis-om-python/ci?style=flat-square [ci-url]: https://github.com/redis-developer/redis-om-python/actions/workflows/ci.yml -[license-image]: http://img.shields.io/badge/license-MIT-green.svg?style=flat-square +[license-image]: http://img.shields.io/badge/license-BSD_3--Clause-green.svg?style=flat-square [license-url]: LICENSE From 95643f86f341dc465e9a7010162d65591fb182b3 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 13:26:40 -0700 Subject: [PATCH 15/49] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ff6506..15c3c0f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ This **preview** release contains the following features: With Redis OM, you get powerful data modeling, extensible data validation with [Pydantic](pydantic-url), and rich query expressions. -Check out this example of how we'd model customer data with Redis OM. First, we're going to create a `Customer` model: +Check out this example of how we'd model customer data with Redis OM. First, we create a `Customer` model: ```python import datetime From 77030ccaf54eec6ef8fec3b3303b8187ccb86b64 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:19:59 -0700 Subject: [PATCH 16/49] Make examples shorter, split Querying and Embedded Models --- docs/embedded.md | 0 docs/querying.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/embedded.md create mode 100644 docs/querying.md diff --git a/docs/embedded.md b/docs/embedded.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/querying.md b/docs/querying.md new file mode 100644 index 0000000..e69de29 From 42592013ba81b597a8e85ebd183f3e68b9d377a0 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:21:39 -0700 Subject: [PATCH 17/49] More changes --- README.md | 140 ++++++++++++++++++++++----------------------- docs/embedded.md | 45 +++++++++++++++ docs/querying.md | 64 +++++++++++++++++++++ docs/validation.md | 71 +++++++++++++++++++++++ 4 files changed, 249 insertions(+), 71 deletions(-) create mode 100644 docs/validation.md diff --git a/README.md b/README.md index 15c3c0f..ff59ce7 100644 --- a/README.md +++ b/README.md @@ -112,32 +112,19 @@ To retrieve this customer with its primary key, we use `Customer.get()`: other_andrew = Customer.get('01FJM6PH661HCNNRC884H6K30C') ``` -Now let's see how Redis OM makes data validation a snap, thanks to [Pydantic](https://pydantic-docs.helpmanual.io/). +**Ready to learn more?** Check out the [getting started](docs/getting_started.md) guide. + +Or, continue reading to see how Redis OM makes data validation a snap. ### Data Validation -When you create a model with Redis OM, you define fields and give them type annotations. As a refresher, take a look at the `Customer` model we already built: +Redis OM uses [Pydantic](pydantic-url) to validate data based on the type annotations you assign to fields in a model class. + +This validation ensures that fields like `first_name`, which the `Customer` model marked as a `str`, are always string. **But every Redis OM model is also a Pydantic model**, so you can use Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validation! + +As an example, because we used the `EmailStr` validator, we'll get a validation error if we try to save a `Customer` with an invalid email address: ```python -class Customer(HashModel): - first_name: str - last_name: str - email: EmailStr - join_date: datetime.date - age: int - bio: Optional[str] -``` - -Redis OM uses [Pydantic](pydantic-url) behind the scenes to validate data at runtime, based on the model's type annotations. - -This validation works for basic cases, like ensuring that `first_name` is always a string. But every Redis OM model is also a Pydantic model, so you can use existing Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validation! - -#### A Demo - -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", @@ -146,50 +133,71 @@ Customer( 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 persisting to Redis all work regardless of how you run Redis. +This code generates a validation error: -However, Redis OM will take your Python applications to the next level if you're using the RediSearch and RedisJSON modules in your Redis deployment. Next, we'll talk about the **rich query expressions** and **embedded models** that Redis OM gives you with those Redis modules. +``` + Traceback: + pydantic.error_wrappers.ValidationError: 1 validation error for Customer + email + value is not a valid email address (type=value_error.email) +``` -**TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the *RediSearch and RedisJSON* section of this README. +What's great about this is **any existing Pydantic validator should work** as a drop-in type annotation with a Redis OM model. You can also write arbitrarily complex custom validations! + +To learn more, see the [documentation on data validation](docs/validation.md). + + +#### Rich Queries and Embedded Models + +Data modeling, validation, and saving models to Redis all work regardless of how you run Redis. + +Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch](redisearch-url) and [RedisJSON](redis-json-url) modules are installed in your Redis deployment. + +**TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the "RediSearch and RedisJSON" section of this README. ### 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. +Let's make a small change to the `Customer` model we defined earlier to let Redis OM know that we want to query using the `last_name` and `age` fields: -With these two classes defined, you can now: +```python +class Customer(HashModel): + first_name: str + last_name: str = Field(index=True) + email: EmailStr + join_date: datetime.date + age: int = Field(index=True) + bio: Optional[str] +``` -* 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 +Now, if we use this model with a Redis deployment that has the [RediSearch module](redisearch-url) installed, we can run queries like the following: +```python +# Find all customers with the last name "Brookins" +Customer.find(Customer.last_name == "Brookins").all() + +# Find all customers that do NOT have the last name "Brookins" +Customer.find(Customer.last_name != "Brookins").all() + +# Find all customers whose last name is "Brookins" OR whose age is +# 100 AND whose last name is "Smith" +Customer.find((Customer.last_name == "Brookins") | ( + Customer.age == 100 +) & (Customer.last_name == "Smith")).all() +``` + +These queries -- and more! -- are possible because **Redis OM manages indexes for you automatically**. + +Querying with this index features a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee. We think you'll enjoy it! + +To see more example queries, see the [documentation on querying](docs/querying.md). + +### Embedded Models + +Redis OM can store and query **nested models** like any document database, with the speed and power you get from Redis. Let's see how this works. + +In the next example, we'll define a new `Address` model and embed it within the `Customer` model. ```python import datetime @@ -223,27 +231,15 @@ class Customer(JsonModel): address: Address ``` -Here are a few example queries that use the models we defined earlier: +With these two models and a Redis deployment with the RedisJSON module installed, we can run queries like the following: ```python -# Find all customers with the last name "Brookins" -Customer.find(Customer.last_name == "Brookins").all() - -# Find all customers that do NOT have the last name "Brookins" -Customer.find(Customer.last_name != "Brookins").all() - -# Find all customers whose last name is "Brookins" OR whose age is -# 100 AND whose last name is "Smith" -Customer.find((Customer.last_name == "Brookins") | ( - Customer.age == 100 -) & (Customer.last_name == "Smith")).all() - # Find all customers who live in San Antonio, TX Customer.find(Customer.address.city == "San Antonio", Customer.address.state == "TX") ``` -Ready to learn more? Read the [getting started](docs/getting_started.md) guide or check out how to [add Redis OM to your FastAPI project](docs/integrating.md). +To learn more, read the [documentation on embedded models](docs/embedded.md). ## 💻 Installation @@ -259,7 +255,7 @@ $ poetry add redis-om ## 📚 Documentation -Documentation is available [here](docs/index.md). +The Redis OM documentation is available [here](docs/index.md). ## ⛏️ Troubleshooting @@ -270,7 +266,9 @@ hit us up on the [Redis Discord Server](http://discord.gg/redis). ## ✨ RediSearch and RedisJSON -Some advanced features of Redis OM rely 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](redisearch-url) and [RedisJSON](redis-json-url). + +You can run these modules in your self-hosted Redis deployment, or you can use Redis Enterprise, which includes both modules. To learn more, read [our documentation](docs/redis_modules.md). diff --git a/docs/embedded.md b/docs/embedded.md index e69de29..2d1fe3c 100644 --- a/docs/embedded.md +++ b/docs/embedded.md @@ -0,0 +1,45 @@ +# Embedded Models + +Redis OM can store and query **nested models** like any document database, with the speed and power you get from Redis. Let's see how this works. + +In the next example, we'll define a new `Address` model and embed it within the `Customer` model. + +```python +import datetime +from typing import Optional + +from redis_om.model import ( + EmbeddedJsonModel, + JsonModel, + Field, +) + +class Address(EmbeddedJsonModel): + address_line_1: str + address_line_2: Optional[str] + city: str = Field(index=True) + state: str = Field(index=True) + country: str + postal_code: str = Field(index=True) + + +class Customer(JsonModel): + first_name: str = Field(index=True) + last_name: str = Field(index=True) + email: str = Field(index=True) + join_date: datetime.date + age: int = Field(index=True) + bio: Optional[str] = Field(index=True, full_text_search=True, + default="") + + # Creates an embedded model. + address: Address +``` + +With these two models and a Redis deployment with the RedisJSON module installed, we can run queries like the following: + +```python +# Find all customers who live in San Antonio, TX +Customer.find(Customer.address.city == "San Antonio", + Customer.address.state == "TX") +``` \ No newline at end of file diff --git a/docs/querying.md b/docs/querying.md index e69de29..745faeb 100644 --- a/docs/querying.md +++ b/docs/querying.md @@ -0,0 +1,64 @@ +# 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 + + +```python +import datetime +from typing import Optional + +from redis_om.model import ( + EmbeddedJsonModel, + JsonModel, + Field, +) + +class Address(EmbeddedJsonModel): + address_line_1: str + address_line_2: Optional[str] + city: str = Field(index=True) + state: str = Field(index=True) + country: str + postal_code: str = Field(index=True) + + +class Customer(JsonModel): + first_name: str = Field(index=True) + last_name: str = Field(index=True) + email: str = Field(index=True) + join_date: datetime.date + age: int = Field(index=True) + bio: Optional[str] = Field(index=True, full_text_search=True, + default="") + + # Creates an embedded model. + address: Address +``` + +Here are a few example queries that use the models we defined earlier: + +```python +# Find all customers with the last name "Brookins" +Customer.find(Customer.last_name == "Brookins").all() + +# Find all customers that do NOT have the last name "Brookins" +Customer.find(Customer.last_name != "Brookins").all() + +# Find all customers whose last name is "Brookins" OR whose age is +# 100 AND whose last name is "Smith" +Customer.find((Customer.last_name == "Brookins") | ( + Customer.age == 100 +) & (Customer.last_name == "Smith")).all() + +# Find all customers who live in San Antonio, TX +Customer.find(Customer.address.city == "San Antonio", + Customer.address.state == "TX") +``` \ No newline at end of file diff --git a/docs/validation.md b/docs/validation.md new file mode 100644 index 0000000..5b59a94 --- /dev/null +++ b/docs/validation.md @@ -0,0 +1,71 @@ +# Validation + +Redis OM uses [Pydantic](pydantic-url) behind the scenes to validate data at runtime, based on the model's type annotations. + +## Basic Type Validation + +Validation works for basic type annotations like `str`. Thus, given the following model: + +```python +class Customer(HashModel): + first_name: str + last_name: str + email: EmailStr + join_date: datetime.date + age: int + bio: Optional[str] +``` + +... Redis OM will ensure that `first_name` is always a string. + +But every Redis OM model is also a Pydantic model, so you can use existing Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validation! + +## Complex Validation + +Let's see what happens if we try to create a `Customer` object 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." +) +``` + +This code generates the following error: + +``` + 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 change a field on a model instance to an invalid value and then try to save it: + +```python +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." +) + +andrew.email = "Not valid" +andrew.save() +``` + +Once again, we get the valiation error: + +``` + Traceback: + pydantic.error_wrappers.ValidationError: 1 validation error for Customer + email + value is not a valid email address (type=value_error.email) +``` \ No newline at end of file From c6820ed7e481d4bd522f19b0c5cfb44eaded4362 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:43:48 -0700 Subject: [PATCH 18/49] More tweaks --- README.md | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index ff59ce7..7b76a93 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,13 @@ - [💡 Why Redis OM?](#-why-redis-om) - - [Starting Redis](#starting-redis) - - [📇 Modeling your domain (and indexing it!)](#-modeling-your-domain-and-indexing-it) - - [🔎 Querying](#-querying) - - [Why this is important](#why-this-is-important) + - [📇 Modeling Your Data](#-modeling-your-data) + - [✓ Validating Data With Your Model](#-validating-data-with-your-model) + - [🔎 Rich Queries and Embedded Models](#-rich-queries-and-embedded-models) + - [💻 Installation](#installation) + - [📚 Documentation](#documentation) + - [⛏️ Troubleshooting](#troubleshooting) - [So how do you get RediSearch and RedisJSON?](#so-how-do-you-get-redisearch-and-redisjson) - - [Why Redis OM?](#why) - - [Getting started](#getting-started) - - [Installation](#installation) - - [Documentation](#documentation) - - [Troubleshooting](#troubleshooting) - [Contributing](#contributing) - [License](#license) @@ -41,7 +38,7 @@ ## 💡 Why Redis OM? -Redis OM provides high-level abstractions for using Redis in Python, making it easy to model and query your Redis domain objects. +Redis OM provides high-level abstractions that make it easy to model and query data in Redis with modern Python applications. This **preview** release contains the following features: @@ -49,13 +46,11 @@ This **preview** release contains the following features: * Declarative secondary-index generation * Fluent APIs for querying Redis -## 🏁 Getting started +## 📇 Modeling Your Data -### Object Mapping +Redis OM contains powerful declarative models that give you data validation, serialization, and persistence to Redis. -With Redis OM, you get powerful data modeling, extensible data validation with [Pydantic](pydantic-url), and rich query expressions. - -Check out this example of how we'd model customer data with Redis OM. First, we create a `Customer` model: +Check out this example of modeling customer data with Redis OM. First, we create a `Customer` model: ```python import datetime @@ -77,8 +72,6 @@ class Customer(HashModel): bio: Optional[str] ``` -**NOTE**: Redis OM uses Python type annotations for data validation. See the _Data Validation_ section of this README for more details. - Now that we have a `Customer` model, let's use it to save customer data to Redis. First, we create a new `Customer` object: @@ -116,13 +109,13 @@ other_andrew = Customer.get('01FJM6PH661HCNNRC884H6K30C') Or, continue reading to see how Redis OM makes data validation a snap. -### Data Validation +## ✓ Validating Data With Your Model Redis OM uses [Pydantic](pydantic-url) to validate data based on the type annotations you assign to fields in a model class. -This validation ensures that fields like `first_name`, which the `Customer` model marked as a `str`, are always string. **But every Redis OM model is also a Pydantic model**, so you can use Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validation! +This validation ensures that fields like `first_name`, which the `Customer` model marked as a `str`, are always strings. **But every Redis OM model is also a Pydantic model**, so you can use Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validations! -As an example, because we used the `EmailStr` validator, we'll get a validation error if we try to save a `Customer` with an invalid email address: +For example, because we used the `EmailStr` type for the `email` field, we'll get a validation error if we try to save a `Customer` with an invalid email address: ```python Customer( @@ -135,7 +128,7 @@ Customer( ) ``` -This code generates a validation error: +This code generates the following validation error: ``` Traceback: @@ -144,16 +137,16 @@ This code generates a validation error: value is not a valid email address (type=value_error.email) ``` -What's great about this is **any existing Pydantic validator should work** as a drop-in type annotation with a Redis OM model. You can also write arbitrarily complex custom validations! +**Any existing Pydantic validator should work** as a drop-in type annotation with a Redis OM model. You can also write arbitrarily complex custom validations! To learn more, see the [documentation on data validation](docs/validation.md). -#### Rich Queries and Embedded Models +## 🔎 Rich Queries and Embedded Models Data modeling, validation, and saving models to Redis all work regardless of how you run Redis. -Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch](redisearch-url) and [RedisJSON](redis-json-url) modules are installed in your Redis deployment. +Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch](redisearch-url) and [RedisJSON](redis-json-url) modules are installed in your Redis deployment or you're using [Redis Enterprise](redis-enterprise-url). **TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the "RediSearch and RedisJSON" section of this README. @@ -264,11 +257,11 @@ If you run into trouble or have any questions, we're here to help! First, check the [FAQ](docs/faq.md). If you don't find the answer there, hit us up on the [Redis Discord Server](http://discord.gg/redis). -## ✨ RediSearch and RedisJSON +## ✨ So How Do You Get RediSearch and RedisJSON? Some advanced features of Redis OM rely on core features from two source available Redis modules: [RediSearch](redisearch-url) and [RedisJSON](redis-json-url). -You can run these modules in your self-hosted Redis deployment, or you can use Redis Enterprise, which includes both modules. +You can run these modules in your self-hosted Redis deployment, or you can use [Redis Enterprise](redis-enterprise-url), which includes both modules. To learn more, read [our documentation](docs/redis_modules.md). @@ -303,4 +296,5 @@ Redis OM uses the [BSD 3-Clause license][license-url]. [redis-json-url]: https://oss.redis.com/redisjson/ [pydantic-url]: https://github.com/samuelcolvin/pydantic [ulid-url]: https://github.com/ulid/spec +[redis-enterprise-url]: https://redis.com/try-free/ From 62f9e244eded8f1701ba9345fb62ca83fb0f046c Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:45:34 -0700 Subject: [PATCH 19/49] Fix word --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b76a93..b25c768 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ Redis OM uses [Pydantic](pydantic-url) to validate data based on the type annota This validation ensures that fields like `first_name`, which the `Customer` model marked as a `str`, are always strings. **But every Redis OM model is also a Pydantic model**, so you can use Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validations! -For example, because we used the `EmailStr` type for the `email` field, we'll get a validation error if we try to save a `Customer` with an invalid email address: +For example, because we used the `EmailStr` type for the `email` field, we'll get a validation error if we try to create a `Customer` with an invalid email address: ```python Customer( From 8168f84b4fdc5886ab851bad3d65bab18e28e02e Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:48:02 -0700 Subject: [PATCH 20/49] Fix section links --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b25c768..10a05e7 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - [💻 Installation](#installation) - [📚 Documentation](#documentation) - [⛏️ Troubleshooting](#troubleshooting) - - [So how do you get RediSearch and RedisJSON?](#so-how-do-you-get-redisearch-and-redisjson) + - [So how do you get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) - [Contributing](#contributing) - [License](#license) @@ -146,9 +146,9 @@ To learn more, see the [documentation on data validation](docs/validation.md). Data modeling, validation, and saving models to Redis all work regardless of how you run Redis. -Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch](redisearch-url) and [RedisJSON](redis-json-url) modules are installed in your Redis deployment or you're using [Redis Enterprise](redis-enterprise-url). +Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch](redisearch-url) and [RedisJSON](redis-json-url) modules are installed in your Redis deployment, or you're using [Redis Enterprise](redis-enterprise-url). -**TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the "RediSearch and RedisJSON" section of this README. +**TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the [So, How Do You Get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) section of this README. ### Querying From 9409dbde3bd43e26ff5fc64fcc315ec3e7ed74c2 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:49:24 -0700 Subject: [PATCH 21/49] More emojis --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 10a05e7..d07c797 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ - [💻 Installation](#installation) - [📚 Documentation](#documentation) - [⛏️ Troubleshooting](#troubleshooting) - - [So how do you get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) - - [Contributing](#contributing) - - [License](#license) + - [✨ So how do you get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) + - [❤️ Contributing](#contributing) + - [📝 License](#license) @@ -273,7 +273,7 @@ We'd love your contributions! You can also **contribute documentation** -- or just let us know if something needs more detail. [Open an issue on GitHub](https://github.com/redis-om/redis-om-python/issues/new) to get started. -## License +## 📝 License Redis OM uses the [BSD 3-Clause license][license-url]. From 40c21c2f4e136f1c88980a2f2056490f56e99e66 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 25 Oct 2021 14:50:01 -0700 Subject: [PATCH 22/49] Fix more section links --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d07c797..40638a2 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ - [📇 Modeling Your Data](#-modeling-your-data) - [✓ Validating Data With Your Model](#-validating-data-with-your-model) - [🔎 Rich Queries and Embedded Models](#-rich-queries-and-embedded-models) - - [💻 Installation](#installation) - - [📚 Documentation](#documentation) - - [⛏️ Troubleshooting](#troubleshooting) + - [💻 Installation](#-installation) + - [📚 Documentation](#-documentation) + - [⛏️ Troubleshooting](#-troubleshooting) - [✨ So how do you get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) - - [❤️ Contributing](#contributing) - - [📝 License](#license) + - [❤️ Contributing](#-contributing) + - [📝 License](#-license) From ad6a323fb8035fc712f31ea52e84877d20752c3d Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Wed, 27 Oct 2021 17:33:12 -0700 Subject: [PATCH 23/49] Remove unused deepcopy --- redis_om/model/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redis_om/model/model.py b/redis_om/model/model.py index 3031bf9..e00991e 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -4,7 +4,7 @@ import decimal import json import logging import operator -from copy import copy, deepcopy +from copy import copy from enum import Enum from functools import reduce from typing import ( From f44b7fa664beb6f7881e36d1d029a7e9906fdd25 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Fri, 29 Oct 2021 17:31:36 -0700 Subject: [PATCH 24/49] Work on docs, getting started guide --- README.md | 18 +- docs/connections.md | 0 docs/{embedded.md => embedded_models.md} | 0 docs/getting_started.md | 278 +++++++++++++++++++++++ docs/models_and_fields.md | 29 +++ docs/testing.md | 1 + docs/validation.md | 6 +- poetry.lock | 95 +++++++- pyproject.toml | 10 +- tox.ini | 9 + 10 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 docs/connections.md rename docs/{embedded.md => embedded_models.md} (100%) create mode 100644 docs/models_and_fields.md create mode 100644 docs/testing.md create mode 100644 tox.ini diff --git a/README.md b/README.md index 40638a2..f1d6cd8 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - [💻 Installation](#-installation) - [📚 Documentation](#-documentation) - [⛏️ Troubleshooting](#-troubleshooting) - - [✨ So how do you get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) + - [✨ So, How Do You Get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) - [❤️ Contributing](#-contributing) - [📝 License](#-license) @@ -111,7 +111,7 @@ Or, continue reading to see how Redis OM makes data validation a snap. ## ✓ Validating Data With Your Model -Redis OM uses [Pydantic](pydantic-url) to validate data based on the type annotations you assign to fields in a model class. +Redis OM uses [Pydantic][pydantic-url] to validate data based on the type annotations you assign to fields in a model class. This validation ensures that fields like `first_name`, which the `Customer` model marked as a `str`, are always strings. **But every Redis OM model is also a Pydantic model**, so you can use Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validations! @@ -146,13 +146,15 @@ To learn more, see the [documentation on data validation](docs/validation.md). Data modeling, validation, and saving models to Redis all work regardless of how you run Redis. -Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch](redisearch-url) and [RedisJSON](redis-json-url) modules are installed in your Redis deployment, or you're using [Redis Enterprise](redis-enterprise-url). +Next, we'll show you the **rich query expressions** and **embedded models** Redis OM provides when the [RediSearch][redisearch-url] and [RedisJSON][redis-json-url] modules are installed in your Redis deployment, or you're using [Redis Enterprise][redis-enterprise-url]. **TIP**: *Wait, what's a Redis module?* If you aren't familiar with Redis modules, review the [So, How Do You Get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) section of this README. ### Querying -Let's make a small change to the `Customer` model we defined earlier to let Redis OM know that we want to query using the `last_name` and `age` fields: +Redis OM comes with a rich query language that allows you to query Redis with Python expressions. + +To show how this works, we'll make a small change to the `Customer` model we defined earlier. We'll add `Field(index=True)` to tell Redis OM that we want to index the `last_name` and `age` fields: ```python class Customer(HashModel): @@ -164,7 +166,7 @@ class Customer(HashModel): bio: Optional[str] ``` -Now, if we use this model with a Redis deployment that has the [RediSearch module](redisearch-url) installed, we can run queries like the following: +Now, if we use this model with a Redis deployment that has the [RediSearch module][redisearch-url] installed, we can run queries like the following: ```python # Find all customers with the last name "Brookins" @@ -184,7 +186,7 @@ These queries -- and more! -- are possible because **Redis OM manages indexes fo Querying with this index features a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee. We think you'll enjoy it! -To see more example queries, see the [documentation on querying](docs/querying.md). +To learn more about how to query with Redis OM, see the [documentation on querying](docs/querying.md). ### Embedded Models @@ -259,9 +261,9 @@ hit us up on the [Redis Discord Server](http://discord.gg/redis). ## ✨ So How Do You Get RediSearch and RedisJSON? -Some advanced features of Redis OM rely on core features from two source available Redis modules: [RediSearch](redisearch-url) and [RedisJSON](redis-json-url). +Some advanced features of Redis OM rely on core features from two source available Redis modules: [RediSearch][redisearch-url] and [RedisJSON][redis-json-url]. -You can run these modules in your self-hosted Redis deployment, or you can use [Redis Enterprise](redis-enterprise-url), which includes both modules. +You can run these modules in your self-hosted Redis deployment, or you can use [Redis Enterprise][redis-enterprise-url], which includes both modules. To learn more, read [our documentation](docs/redis_modules.md). diff --git a/docs/connections.md b/docs/connections.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/embedded.md b/docs/embedded_models.md similarity index 100% rename from docs/embedded.md rename to docs/embedded_models.md diff --git a/docs/getting_started.md b/docs/getting_started.md index e69de29..48f0e63 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -0,0 +1,278 @@ +# Getting Started With Redis OM + +## Introduction + +This tutorial will walk you through installing Redis OM, creating your first model, and using it to save and validate data. + +## Prerequisites + +Redis OM requires Python version 3.9 or above and a Redis instance to connect to. + +## Python + +Make sure you are running **Python version 3.9 or higher**: + +``` +python --version +Python 3.9.0 +``` + +If you don't have Python installed, you can download it from [Python.org](https://www.python.org/downloads/), use [Pyenv](https://github.com/pyenv/pyenv), or install Python with your operating system's package manager. + +## Redis + +Redis OM saves data in Redis, so you will need Redis installed and running to complete this tutorial. + +### Downloading Redis + +The latest version of Redis is available from [Redis.io](https://redis.io/). You can also install Redis with your operating system's package manager. + +**NOTE:** This tutorial will guide you through starting Redis locally, but the instructions will also work if Redis is running on a remote server. + +### Installing Redis On Windows + +Redis doesn't run directly on Windows, but you can use Windows Subsystem for Linux (WSL) to run Redis. See [our video on YouTube](https://youtu.be/_nFwPTHOMIY) for a walk-through. + +Windows users can also use Docker. See the next section on running Redis with Docker for more information. + +### Running Redis With Docker + +Instead of installing Redis manually or with a package manager, you can run Redis with Docker. The official Redis Docker image is hosted on [Docker Hub](https://hub.docker.com/_/redis). + +**TIP:** If you plan on using Docker, we recommend the [redismod](https://hub.docker.com/r/redislabs/redismod) image because it includes the RediSearch and RedisJSON modules. + +## Recommended: RediSearch and RedisJSON + +Redis OM relies on the [RediSearch][redisearch-url] and [RedisJSON][redis-json-url] Redis modules to support [rich queries](querying.md) and [embedded models](embedded_models.md). + +You don't need these Redis modules to use Redis OM's data modeling, validation, and persistence features, but we recommend them to get the most out of Redis OM. + +The easiest way to run these Redis modules during local development is to use the [redismod](https://hub.docker.com/r/redislabs/redismod) Docker image. + +You can quickly start Redis with the redismod Docker image by running the following command: + + docker run -d -p 6379:6379 redislabs/redismod + +**TIP:** The `-d` option runs Redis in the background. + +For other installation methods, follow the "Quick Start" guides on both modules' home pages for alternative installation methods. + +## Start Redis + +Before you get started with Redis OM, make sure you start Redis. + +The command you use to start Redis will depend on how you installed it. + +### Ubuntu Linux (Including WSL) + +If you installed Redis using `apt`, start it with the `systemctl` command: + + sudo systemctl restart redis.service + +Otherwise, you can start the server manually: + + redis-server start + +### macOS with Homebrew + + brew services start redis + +### Docker + +The command to start Redis with Docker depends on the image you've chosen to use. + +#### Docker with the redismod image (recommended) + + docker run -d -p 6379:6379 redislabs/redismod + +### Docker iwth the redis image + + docker run -d -p 6379:6379 redis + +## Installing Redis OM + +You can install Redis OM with `pip` by running the following command: + + pip install redis-om + +Or, if you're using Poetry, you can install Redis OM with the following command: + + poetry install redis-om + +With Pipenv, the command is: + + pipenv install redis-om + +## Setting the Redis URL Environment Variable + +We're almost ready to create a Redis OM model! But first, we need to make sure that Redis OM knows how to connect to Redis. + +By default, Redis OM tries to connect to Redis on your localhost at port 6379. Most local install methods will result in Redis running at this location, in which case you don't need to do anything special. + +However, if you configured Redis to run on a different port, or if you're using a remote Redis server, you'll need to set the `REDIS_URL` environment variable. + +The `REDIS_URL` environment variable follows the redis-py URL format: + + redis://[[username]:[password]]@localhost:6379/[database number] + +The default connection is eqivalent to the following `REDIS_URL` environment variable: + + redis://@localhost:6379 + +**TIP:** Redis databases are numbered, and the default is 0. You can leave off the database number to use the default database. + +Other supported prefixes include "rediss" for SSL connections and "unix" for Unix domain sockets: + + rediss://[[username]:[password]]@localhost:6379/0 + unix://[[username]:[password]]@/path/to/socket.sock?db=0 + +For more details about how to connect to Redis with Redis OM, see the [connections documentation](connections.md). + +### Redis Cluster Support + +Redis OM supports connecting to Redis Cluster, but this preview release does not support doing so with the `REDIS_URL` environment variable. However, you can connect by manually creating a connection object. + +See the [connections documentation](connections.md) for examples of connecting to Redis Cluster. + +Support for connecting to Redis Cluster via `REDIS_URL` will be added in a future release. + +## Defining a Model + +In this tutorial, we'll create a `Customer` model that validates and saves data. Let's start with a basic definition of the model. We'll add features as we go along. + +```python +import datetime +from typing import Optional + +from redis_om.model import ( + HashModel, +) + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: str +``` + +There are a few details to note: + +1. Our `Customer` model extends the `HashModel` class. This means that it will be saved to Redis as a hash. The other model class that Redis OM provides is `JsonModel`, which we'll discuss later. +2. We've specified the model's fields using Python type annotations. + +Let's dig into these two details a bit more. + +### The HashModel Class + +When you subclass `HashModel`, your subclass is both a Redis OM model, with methods for saving data to Redis, *and* a Pydantic model. + +This means that you can use Pydantic field validations with your Redis OM models, which we'll cover later, when we talk about validation. But this also means you can use Redis OM models anywhere you would use a Pydantic model, like in your FastAPI applications. 🤯 + +### Type Annotations + +The type annotations you add to your model fields are used for a few purposes: + +* Validating data with Pydantic validators +* Serializing data Redis +* Deserializing data from Redis + +We'll see examples of these throughout the course of this tutorial. + +An important detail about the `HashModel` class is that it does not support `list`, `set`, or mapping (like `dict`) types. This is because Redis hashes cannot contain lists, sets, or other hashes. + +If you want to model fields with a list, set, or mapping type, or another model, you'll need to use the `JsonModel` class, which can support these types, as well as embedded models. + +## Creating Models + +Let's see what creating a model object looks like: + +```python +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." +) +``` + +### Optional Fields + +What would happen if we left out one of these fields, like `bio`? + +```python +Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38) +``` + +All fields are required because none of the fields are marked `Optional`, so we get a validation error: + +``` +ValidationError: 1 validation error for Customer +bio + field required (type=value_error.missing) +``` + +If we want the `bio` field to be optional, we need to change the type annotation: + +```python +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] +``` + +Now we can create `Customer` objects with or without the `bio` field. + +### Default Values + +Fields can have default values. + +```python +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] = "Super dope" +``` + +Now, if we create a `Customer` object without a `bio` field, it will use the default value. + +```python +andrew = Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38) + +print(andrew.bio) +'Super Dope' +``` + +## Saving Models + +## Examining Your Data In Redis + +## Validating Data + +## Next Steps + +Now that you know the basics of working with Redis OM, continue on for all the nitty-gritty details about [models and fields](validation.md). + + +[redisearch-url]: https://oss.redis.com/redisearch/ +[redis-json-url]: https://oss.redis.com/redisjson/ diff --git a/docs/models_and_fields.md b/docs/models_and_fields.md new file mode 100644 index 0000000..b25d4d3 --- /dev/null +++ b/docs/models_and_fields.md @@ -0,0 +1,29 @@ +# Models and Fields + +## Introduction + +## Saving Data As Hashes With HashModel + +### What Does Redis Store? + +## Saving Data With JSON With JsonModel + +### What Does Redis Store? + +## Primary Keys + +### Why Primary Keys Matter to Redis OM + +### Using the Default Primary Key + +### Using a Custom Primary Key + +## Meta Classes + +## Subclassing Models + +### Subclassing and Meta Objects + +## Saving Models + + diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..0f1c1c5 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1 @@ +# Testing Your Models diff --git a/docs/validation.md b/docs/validation.md index 5b59a94..9b165dd 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -1,6 +1,6 @@ # Validation -Redis OM uses [Pydantic](pydantic-url) behind the scenes to validate data at runtime, based on the model's type annotations. +Redis OM uses [Pydantic][pydantic-url] behind the scenes to validate data at runtime, based on the model's type annotations. ## Basic Type Validation @@ -68,4 +68,6 @@ Once again, we get the valiation error: pydantic.error_wrappers.ValidationError: 1 validation error for Customer email value is not a valid email address (type=value_error.email) -``` \ No newline at end of file +``` + +[pydantic-url]: https://github.com/samuelcolvin/pydantic diff --git a/poetry.lock b/poetry.lock index 0ef59fd..b6eada7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -72,6 +72,18 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "backports.entry-points-selectable" +version = "1.1.0" +description = "Compatibility shim providing selectable entry points for older implementations" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] + [[package]] name = "bandit" version = "1.7.0" @@ -217,6 +229,14 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "distlib" +version = "0.3.3" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "dnspython" version = "2.1.0" @@ -263,6 +283,18 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] testing = ["pre-commit"] +[[package]] +name = "filelock" +version = "3.3.1" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + [[package]] name = "flake8" version = "4.0.1" @@ -918,6 +950,28 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "tox" +version = "3.24.4" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] + [[package]] name = "tqdm" version = "4.62.3" @@ -1001,6 +1055,25 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "virtualenv" +version = "20.9.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +"backports.entry-points-selectable" = ">=1.0.4" +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + [[package]] name = "wcwidth" version = "0.2.5" @@ -1040,7 +1113,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "de30b2382aaeb2fe0675658bce5a3e5bc21a14e85c66d94a90054bc73a7831cd" +content-hash = "6ee2fea3f5714de8992d03450cbe4a6ce7ac41825252877777a6d61b777d4934" [metadata.files] aioredis = [ @@ -1071,6 +1144,10 @@ backcall = [ {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +"backports.entry-points-selectable" = [ + {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, + {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, +] bandit = [ {file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"}, {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, @@ -1212,6 +1289,10 @@ decorator = [ {file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"}, {file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"}, ] +distlib = [ + {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, + {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, +] dnspython = [ {file = "dnspython-2.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, @@ -1228,6 +1309,10 @@ execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] +filelock = [ + {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"}, + {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, @@ -1591,6 +1676,10 @@ tomli = [ {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, ] +tox = [ + {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, + {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, +] tqdm = [ {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, @@ -1620,6 +1709,10 @@ urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] +virtualenv = [ + {file = "virtualenv-20.9.0-py2.py3-none-any.whl", hash = "sha256:1d145deec2da86b29026be49c775cc5a9aab434f85f7efef98307fb3965165de"}, + {file = "virtualenv-20.9.0.tar.gz", hash = "sha256:bb55ace18de14593947354e5e6cd1be75fb32c3329651da62e92bf5d0aab7213"}, +] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, diff --git a/pyproject.toml b/pyproject.toml index 8859537..1335879 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,18 @@ name = "redis-om" version = "0.0.1" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] +maintainers = ["Andrew Brookins "] license = "BSD-3-Clause" readme = "README.md" +repository = "https://github.com/redis-developer/redis-om-python" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Database :: Front-Ends" +] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" redis = "^3.5.3" aioredis = "^2.0.0" pydantic = "^1.8.2" @@ -33,6 +40,7 @@ pytest-cov = "^3.0.0" pytest-xdist = "^2.4.0" twine = "^3.4.2" email-validator = "^1.1.3" +tox = "^3.24.4" [tool.poetry.scripts] migrate = "redis_om.model.cli.migrate:migrate" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6417a44 --- /dev/null +++ b/tox.ini @@ -0,0 +1,9 @@ +[tox] +skipsdist = true +envlist = py37, py38, py39, py310 + +[testenv] +whitelist_externals = poetry +commands = + poetry install -v + poetry run pytest \ No newline at end of file From 807a29b01130975a4f21b3c086a0af84f9d6da0b Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 1 Nov 2021 09:45:37 -0700 Subject: [PATCH 25/49] Set up tox --- poetry.lock | 19 +++++++++++++++++-- pyproject.toml | 3 ++- redis_om/model/model.py | 2 +- tox.ini | 4 ++-- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/poetry.lock b/poetry.lock index b6eada7..6ef4f3f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -972,6 +972,17 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] +[[package]] +name = "tox-pyenv" +version = "1.1.0" +description = "tox plugin that makes tox use `pyenv which` to find python executables" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +tox = ">=2.0" + [[package]] name = "tqdm" version = "4.62.3" @@ -1112,8 +1123,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" -python-versions = "^3.8" -content-hash = "6ee2fea3f5714de8992d03450cbe4a6ce7ac41825252877777a6d61b777d4934" +python-versions = "^3.9" +content-hash = "fb8766fad12a5598ff95e95e96feae984d7cc64e3ce31eb8d3ad9dfd8b39d621" [metadata.files] aioredis = [ @@ -1680,6 +1691,10 @@ tox = [ {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, {file = "tox-3.24.4.tar.gz", hash = "sha256:c30b57fa2477f1fb7c36aa1d83292d5c2336cd0018119e1b1c17340e2c2708ca"}, ] +tox-pyenv = [ + {file = "tox-pyenv-1.1.0.tar.gz", hash = "sha256:916c2213577aec0b3b5452c5bfb32fd077f3a3196f50a81ad57d7ef3fc2599e4"}, + {file = "tox_pyenv-1.1.0-py2.py3-none-any.whl", hash = "sha256:e470c18af115fe52eeff95e7e3cdd0793613eca19709966fc2724b79d55246cb"}, +] tqdm = [ {file = "tqdm-4.62.3-py2.py3-none-any.whl", hash = "sha256:8dd278a422499cd6b727e6ae4061c40b48fce8b76d1ccbf5d34fca9b7f925b0c"}, {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, diff --git a/pyproject.toml b/pyproject.toml index 1335879..22ade19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.8" redis = "^3.5.3" aioredis = "^2.0.0" pydantic = "^1.8.2" @@ -41,6 +41,7 @@ pytest-xdist = "^2.4.0" twine = "^3.4.2" email-validator = "^1.1.3" tox = "^3.24.4" +tox-pyenv = "^1.1.0" [tool.poetry.scripts] migrate = "redis_om.model.cli.migrate:migrate" diff --git a/redis_om/model/model.py b/redis_om/model/model.py index e00991e..332340d 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -333,7 +333,7 @@ class FindQuery: self._pagination: list[str] = [] self._model_cache: list[RedisModel] = [] - def dict(self) -> dict[str, Any]: + def dict(self) -> Dict[str, Any]: return dict( model=self.model, offset=self.offset, diff --git a/tox.ini b/tox.ini index 6417a44..dc841ba 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] skipsdist = true -envlist = py37, py38, py39, py310 +envlist = py38, py39, py310 [testenv] whitelist_externals = poetry commands = poetry install -v - poetry run pytest \ No newline at end of file + poetry run pytest From 8a5db647dafa443d035a4b6c86d8ef4fb82283dc Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 10:34:54 -0700 Subject: [PATCH 26/49] WIP on getting started guide --- docs/getting_started.md | 75 ++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 48f0e63..279413e 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -37,7 +37,7 @@ Windows users can also use Docker. See the next section on running Redis with Do ### Running Redis With Docker -Instead of installing Redis manually or with a package manager, you can run Redis with Docker. The official Redis Docker image is hosted on [Docker Hub](https://hub.docker.com/_/redis). +Instead of installing Redis manually or with a package manager, you can run Redis with Docker. The official Redis Docker image is hosted on [Docker Hub](https://hub.docker.com/_/redis). **TIP:** If you plan on using Docker, we recommend the [redismod](https://hub.docker.com/r/redislabs/redismod) image because it includes the RediSearch and RedisJSON modules. @@ -51,10 +51,9 @@ The easiest way to run these Redis modules during local development is to use th You can quickly start Redis with the redismod Docker image by running the following command: - docker run -d -p 6379:6379 redislabs/redismod - +docker run -d -p 6379:6379 redislabs/redismod **TIP:** The `-d` option runs Redis in the background. - + For other installation methods, follow the "Quick Start" guides on both modules' home pages for alternative installation methods. ## Start Redis @@ -67,41 +66,38 @@ The command you use to start Redis will depend on how you installed it. If you installed Redis using `apt`, start it with the `systemctl` command: - sudo systemctl restart redis.service - +sudo systemctl restart redis.service Otherwise, you can start the server manually: - redis-server start +redis-server start ### macOS with Homebrew - - brew services start redis - + +brew services start redis + ### Docker The command to start Redis with Docker depends on the image you've chosen to use. #### Docker with the redismod image (recommended) - docker run -d -p 6379:6379 redislabs/redismod +docker run -d -p 6379:6379 redislabs/redismod ### Docker iwth the redis image - docker run -d -p 6379:6379 redis +docker run -d -p 6379:6379 redis ## Installing Redis OM You can install Redis OM with `pip` by running the following command: - pip install redis-om - +pip install redis-om Or, if you're using Poetry, you can install Redis OM with the following command: - poetry install redis-om - +poetry install redis-om With Pipenv, the command is: - pipenv install redis-om +pipenv install redis-om ## Setting the Redis URL Environment Variable @@ -113,19 +109,16 @@ However, if you configured Redis to run on a different port, or if you're using The `REDIS_URL` environment variable follows the redis-py URL format: - redis://[[username]:[password]]@localhost:6379/[database number] - +redis://[[username]:[password]]@localhost:6379/[database number] The default connection is eqivalent to the following `REDIS_URL` environment variable: - redis://@localhost:6379 - +redis://@localhost:6379 **TIP:** Redis databases are numbered, and the default is 0. You can leave off the database number to use the default database. - + Other supported prefixes include "rediss" for SSL connections and "unix" for Unix domain sockets: - rediss://[[username]:[password]]@localhost:6379/0 - unix://[[username]:[password]]@/path/to/socket.sock?db=0 - +rediss://[[username]:[password]]@localhost:6379/0 +unix://[[username]:[password]]@/path/to/socket.sock?db=0 For more details about how to connect to Redis with Redis OM, see the [connections documentation](connections.md). ### Redis Cluster Support @@ -134,7 +127,7 @@ Redis OM supports connecting to Redis Cluster, but this preview release does not See the [connections documentation](connections.md) for examples of connecting to Redis Cluster. -Support for connecting to Redis Cluster via `REDIS_URL` will be added in a future release. +Support for connecting to Redis Cluster via `REDIS_URL` will be added in a future release. ## Defining a Model @@ -167,7 +160,7 @@ Let's dig into these two details a bit more. ### The HashModel Class -When you subclass `HashModel`, your subclass is both a Redis OM model, with methods for saving data to Redis, *and* a Pydantic model. +When you subclass `HashModel`, your subclass is both a Redis OM model, with methods for saving data to Redis, *and* a Pydantic model. This means that you can use Pydantic field validations with your Redis OM models, which we'll cover later, when we talk about validation. But this also means you can use Redis OM models anywhere you would use a Pydantic model, like in your FastAPI applications. 🤯 @@ -231,7 +224,7 @@ class Customer(HashModel): join_date: datetime.date age: int bio: Optional[str] -``` +``` Now we can create `Customer` objects with or without the `bio` field. @@ -247,7 +240,7 @@ class Customer(HashModel): join_date: datetime.date age: int bio: Optional[str] = "Super dope" -``` +``` Now, if we create a `Customer` object without a `bio` field, it will use the default value. @@ -258,13 +251,34 @@ andrew = Customer( email="andrew.brookins@example.com", join_date=datetime.date.today(), age=38) - + print(andrew.bio) 'Super Dope' ``` +The model will then save this default value to Redis the next time you call `save()`. + +### Automatic Primary Keys + +Models generate a globally unique primary key automatically without needing to talk to Redis. + +```python +print(andrew.pk) +'01FJM6PH661HCNNRC884H6K30C' +``` + +The ID is available *before* you save the model. + +The default ID generation function creates [ULIDs](https://github.com/ulid/spec), though you can change the function that generates the primary key for models if you'd like to use a different kind of primary key. + ## Saving Models +We can save the model to Redis by calling `save()`: + +```python +andrew.save() +``` + ## Examining Your Data In Redis ## Validating Data @@ -274,5 +288,6 @@ print(andrew.bio) Now that you know the basics of working with Redis OM, continue on for all the nitty-gritty details about [models and fields](validation.md). + [redisearch-url]: https://oss.redis.com/redisearch/ [redis-json-url]: https://oss.redis.com/redisjson/ From 8df968542aaf457192a0f28877bb70e7916548ea Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:17:49 -0700 Subject: [PATCH 27/49] Update Getting Started guide, note WIP docs --- README.md | 151 +++++++----- docs/connections.md | 3 + docs/embedded_models.md | 13 +- docs/faq.md | 3 + docs/getting_started.md | 473 ++++++++++++++++++++++++++++++++++--- docs/index.md | 3 + docs/integrating.md | 3 + docs/models_and_fields.md | 2 + docs/querying.md | 22 +- docs/redis_modules.md | 2 +- docs/testing.md | 4 + poetry.lock | 380 +++++++++++++++-------------- pyproject.toml | 2 +- redis_om/model/__init__.py | 1 + 14 files changed, 785 insertions(+), 277 deletions(-) diff --git a/README.md b/README.md index f1d6cd8..aadfc10 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,20 @@
Table of contents - +span + - - [💡 Why Redis OM?](#-why-redis-om) - - [📇 Modeling Your Data](#-modeling-your-data) - - [✓ Validating Data With Your Model](#-validating-data-with-your-model) - - [🔎 Rich Queries and Embedded Models](#-rich-queries-and-embedded-models) - - [💻 Installation](#-installation) - - [📚 Documentation](#-documentation) - - [⛏️ Troubleshooting](#-troubleshooting) - - [✨ So, How Do You Get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) - - [❤️ Contributing](#-contributing) - - [📝 License](#-license) +- [💡 Why Redis OM?](#-why-redis-om) +- [📇 Modeling Your Data](#-modeling-your-data) +- [✓ Validating Data With Your Model](#-validating-data-with-your-model) +- [🔎 Rich Queries and Embedded Models](#-rich-queries-and-embedded-models) +- [💻 Installation](#-installation) +- [📚 Documentation](#-documentation) +- [⛏️ Troubleshooting](#-troubleshooting) +- [✨ So, How Do You Get RediSearch and RedisJSON?](#-so-how-do-you-get-redisearch-and-redisjson) +- [❤️ Contributing](#-contributing) +- [📝 License](#-license) @@ -58,9 +59,7 @@ from typing import Optional from pydantic import EmailStr -from redis_om.model import ( - HashModel, -) +from redis_om.model import HashModel class Customer(HashModel): @@ -74,9 +73,25 @@ class Customer(HashModel): Now that we have a `Customer` model, let's use it to save customer data to Redis. -First, we create a new `Customer` object: - ```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] + + +# First, we create a new `Customer` object: andrew = Customer( first_name="Andrew", last_name="Brookins", @@ -85,24 +100,17 @@ andrew = Customer( age=38, bio="Python developer, works at Redis, Inc." ) -``` -The model generates a globally unique primary key automatically without needing to talk to Redis. -```python +# The model generates a globally unique primary key automatically +# without needing to talk to Redis. print(andrew.pk) -'01FJM6PH661HCNNRC884H6K30C' -``` +#> '01FJM6PH661HCNNRC884H6K30C' -We can save the model to Redis by calling `save()`: - -```python +# We can save the model to Redis by calling `save()`: andrew.save() -``` -To retrieve this customer with its primary key, we use `Customer.get()`: - -```python -other_andrew = Customer.get('01FJM6PH661HCNNRC884H6K30C') +# To retrieve this customer with its primary key, we use `Customer.get()`: +assert Customer.get(andrew.pk) == andrew ``` **Ready to learn more?** Check out the [getting started](docs/getting_started.md) guide. @@ -118,30 +126,45 @@ This validation ensures that fields like `first_name`, which the `Customer` mode For example, because we used the `EmailStr` type for the `email` field, we'll get a validation error if we try to create a `Customer` with an invalid email address: ```python -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." -) -``` +import datetime +from typing import Optional -This code generates the following validation error: +from pydantic import EmailStr, ValidationError -``` - Traceback: - pydantic.error_wrappers.ValidationError: 1 validation error for Customer - email - value is not a valid email address (type=value_error.email) +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] + + +try: + 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." + ) +except ValidationError as e: + print(e) + """ + pydantic.error_wrappers.ValidationError: 1 validation error for Customer + email + value is not a valid email address (type=value_error.email) + """ ``` **Any existing Pydantic validator should work** as a drop-in type annotation with a Redis OM model. You can also write arbitrarily complex custom validations! To learn more, see the [documentation on data validation](docs/validation.md). - ## 🔎 Rich Queries and Embedded Models Data modeling, validation, and saving models to Redis all work regardless of how you run Redis. @@ -157,6 +180,18 @@ Redis OM comes with a rich query language that allows you to query Redis with Py To show how this works, we'll make a small change to the `Customer` model we defined earlier. We'll add `Field(index=True)` to tell Redis OM that we want to index the `last_name` and `age` fields: ```python +import datetime +from typing import Optional + +from pydantic import EmailStr + +from redis_om.model import ( + Field, + HashModel, + Migrator +) + + class Customer(HashModel): first_name: str last_name: str = Field(index=True) @@ -164,11 +199,16 @@ class Customer(HashModel): join_date: datetime.date age: int = Field(index=True) bio: Optional[str] -``` + + +# Now, if we use this model with a Redis deployment that has the +# RediSearch module installed, we can run queries like the following. -Now, if we use this model with a Redis deployment that has the [RediSearch module][redisearch-url] installed, we can run queries like the following: +# Before running queries, we need to run migrations to set up the +# indexes that Redis OM will use. You can also use the `migrate` +# CLI tool for this! +Migrator().run() -```python # Find all customers with the last name "Brookins" Customer.find(Customer.last_name == "Brookins").all() @@ -202,6 +242,7 @@ from redis_om.model import ( EmbeddedJsonModel, JsonModel, Field, + Migrator ) class Address(EmbeddedJsonModel): @@ -224,11 +265,15 @@ class Customer(JsonModel): # Creates an embedded model. address: Address -``` -With these two models and a Redis deployment with the RedisJSON module installed, we can run queries like the following: +# With these two models and a Redis deployment with the RedisJSON +# module installed, we can run queries like the following. + +# Before running queries, we need to run migrations to set up the +# indexes that Redis OM will use. You can also use the `migrate` +# CLI tool for this! +Migrator().run() -```python # Find all customers who live in San Antonio, TX Customer.find(Customer.address.city == "San Antonio", Customer.address.state == "TX") @@ -254,7 +299,7 @@ The Redis OM documentation is available [here](docs/index.md). ## ⛏️ Troubleshooting -If you run into trouble or have any questions, we're here to help! +If you run into trouble or have any questions, we're here to help! First, check the [FAQ](docs/faq.md). If you don't find the answer there, hit us up on the [Redis Discord Server](http://discord.gg/redis). @@ -287,7 +332,6 @@ Redis OM uses the [BSD 3-Clause license][license-url]. [ci-url]: https://github.com/redis-developer/redis-om-python/actions/workflows/ci.yml [license-image]: http://img.shields.io/badge/license-BSD_3--Clause-green.svg?style=flat-square [license-url]: LICENSE - [redis-om-website]: https://developer.redis.com @@ -299,4 +343,3 @@ Redis OM uses the [BSD 3-Clause license][license-url]. [pydantic-url]: https://github.com/samuelcolvin/pydantic [ulid-url]: https://github.com/ulid/spec [redis-enterprise-url]: https://redis.com/try-free/ - diff --git a/docs/connections.md b/docs/connections.md index e69de29..10eff89 100644 --- a/docs/connections.md +++ b/docs/connections.md @@ -0,0 +1,3 @@ +# Managing Connections + +WIP! \ No newline at end of file diff --git a/docs/embedded_models.md b/docs/embedded_models.md index 2d1fe3c..b957154 100644 --- a/docs/embedded_models.md +++ b/docs/embedded_models.md @@ -1,5 +1,7 @@ # Embedded Models +**NOTE:** This documentation is a stub, using the same embedded JSON model example as the README. + Redis OM can store and query **nested models** like any document database, with the speed and power you get from Redis. Let's see how this works. In the next example, we'll define a new `Address` model and embed it within the `Customer` model. @@ -12,6 +14,7 @@ from redis_om.model import ( EmbeddedJsonModel, JsonModel, Field, + Migrator ) class Address(EmbeddedJsonModel): @@ -34,11 +37,15 @@ class Customer(JsonModel): # Creates an embedded model. address: Address -``` -With these two models and a Redis deployment with the RedisJSON module installed, we can run queries like the following: +# With these two models and a Redis deployment with the RedisJSON +# module installed, we can run queries like the following. + +# Before running queries, we need to run migrations to set up the +# indexes that Redis OM will use. You can also use the `migrate` +# CLI tool for this! +Migrator().run() -```python # Find all customers who live in San Antonio, TX Customer.find(Customer.address.city == "San Antonio", Customer.address.state == "TX") diff --git a/docs/faq.md b/docs/faq.md index e69de29..78203b2 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -0,0 +1,3 @@ +# Frequently Asked Questions (FAQ) + +WIP! \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md index 279413e..2e90485 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -135,11 +135,8 @@ In this tutorial, we'll create a `Customer` model that validates and saves data. ```python import datetime -from typing import Optional -from redis_om.model import ( - HashModel, -) +from redis_om.model import HashModel class Customer(HashModel): @@ -183,6 +180,20 @@ If you want to model fields with a list, set, or mapping type, or another model, Let's see what creating a model object looks like: ```python +import datetime + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: str + + andrew = Customer( first_name="Andrew", last_name="Brookins", @@ -198,41 +209,88 @@ andrew = Customer( What would happen if we left out one of these fields, like `bio`? ```python -Customer( - first_name="Andrew", - last_name="Brookins", - email="andrew.brookins@example.com", - join_date=datetime.date.today(), - age=38) -``` +import datetime -All fields are required because none of the fields are marked `Optional`, so we get a validation error: +from redis_om.model import HashModel +from pydantic import ValidationError -``` -ValidationError: 1 validation error for Customer -bio - field required (type=value_error.missing) -``` -If we want the `bio` field to be optional, we need to change the type annotation: - -```python class Customer(HashModel): first_name: str last_name: str email: str join_date: datetime.date age: int - bio: Optional[str] + bio: str + +# All fields are required because none of the fields +# are marked `Optional`, so we get a validation error: +try: + Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38 # <- We didn't pass in a bio! + ) +except ValidationError as e: + print(e) + """ + ValidationError: 1 validation error for Customer + bio + field required (type=value_error.missing) + """ +``` + +If we want the `bio` field to be optional, we need to change the type annotation to use `Optional`. + +```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] # <- Now, bio is an Optional[str] ``` Now we can create `Customer` objects with or without the `bio` field. ### Default Values -Fields can have default values. +Fields can have default values. You set them by assigning a value to a field. ```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] = "Super dope" # <- We added a default here +``` + +Now, if we create a `Customer` object without a `bio` field, it will use the default value. + +```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + class Customer(HashModel): first_name: str last_name: str @@ -240,20 +298,17 @@ class Customer(HashModel): join_date: datetime.date age: int bio: Optional[str] = "Super dope" -``` -Now, if we create a `Customer` object without a `bio` field, it will use the default value. - -```python + andrew = Customer( first_name="Andrew", last_name="Brookins", email="andrew.brookins@example.com", join_date=datetime.date.today(), - age=38) + age=38) # <- Notice, we didn't give a bio! -print(andrew.bio) -'Super Dope' +print(andrew.bio) # <- So we got the default value. +#> 'Super Dope' ``` The model will then save this default value to Redis the next time you call `save()`. @@ -263,29 +318,383 @@ The model will then save this default value to Redis the next time you call `sav Models generate a globally unique primary key automatically without needing to talk to Redis. ```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] = "Super dope" + + +andrew = Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38) + print(andrew.pk) -'01FJM6PH661HCNNRC884H6K30C' +#> '01FJM6PH661HCNNRC884H6K30C' ``` The ID is available *before* you save the model. The default ID generation function creates [ULIDs](https://github.com/ulid/spec), though you can change the function that generates the primary key for models if you'd like to use a different kind of primary key. +## Validating Data + +Redis OM uses [Pydantic][pydantic-url] to validate data based on the type annotations you assign to fields in a model class. + +This validation ensures that fields like `first_name`, which the `Customer` model marked as a `str`, are always strings. **But every Redis OM model is also a Pydantic model**, so you can use Pydantic validators like `EmailStr`, `Pattern`, and many more for complex validations! + +For example, we defined the `join_date` for our `Customer` model earlier as a `datetime.date`. So, if we try to create a model with a `join_date` that isn't a date, we'll get a validation error. + +Let's try it now: + +```python +import datetime +from typing import Optional + +from redis_om.model import HashModel +from pydantic import ValidationError + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] = "Super dope" + + +try: + Customer( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date="not a date!", # <- The problem line! + age=38 + ) +except ValidationError as e: + print(e) + """ + pydantic.error_wrappers.ValidationError: 1 validation error for Customer + join_date + invalid date format (type=value_error.date) + """ +``` + +### Models Coerce Values By Default + +You might wonder what qualifies as a "date" in our last validation example. By default, Redis OM will try to coerce input values to the correct type. That means we can pass a date string for `join_date` instead of a `date` object: + +```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + + +andrew = Customer( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date="2020-01-02", # <- We're passing a YYYY-MM-DD date string now + age=38 +) + +print(andrew.join_date) +#> 2021-11-02 +type(andrew.join_date) +#> datetime.date # The model parsed the string automatically! +``` + +This ability to combine parsing (in this case, a YYYY-MM-DD date string) with validation can save you a lot of work. + +However, you can turn off coercion -- check the next section on using strict validation. + +### Strict Validation + +You can turn on strict validation to reject values for a field unless they match the exact type of the model's type annotations. + +You do this by changing a field's type annotation to use one of the ["strict" types provided by Pydantic](https://pydantic-docs.helpmanual.io/usage/types/#strict-types). + +Redis OM supports all of Pydantic's strict types: `StrictStr`, `StrictBytes`, `StrictInt`, `StrictFloat`, and `StrictBool`. + +If we wanted to make sure that the `age` field only accepts integers and doesn't try to parse a string containing an integer, like "1", we'd use the `StrictInt` class. + +```python +import datetime +from typing import Optional + +from pydantic import StrictInt, ValidationError +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: StrictInt # <- Instead of int, we use StrictInt + bio: Optional[str] + +# Now if we use a string instead of an integer for `age`, +# we get a validation error: +try: + Customer( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date="2020-01-02", # <- A date as a string shouldn't work now! + age="38" + ) +except ValidationError as e: + print(e) + """ + pydantic.error_wrappers.ValidationError: 1 validation error for Customer + join_date + Value must be a datetime.date object (type=value_error) + """ +``` + +Pydantic doesn't include a `StrictDate` class, but we can create our own. In this example, we create a `StrictDate` type that we'll use to validate that `join_date` is a `datetime.date` object. + +```python +import datetime +from typing import Optional + +from pydantic import ValidationError +from redis_om.model import HashModel + + +class StrictDate(datetime.date): + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: datetime.date, **kwargs) -> datetime.date: + if not isinstance(value, datetime.date): + raise ValueError("Value must be a datetime.date object") + return value + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: StrictDate + age: int + bio: Optional[str] + + +# Now if we use a string instead of a date object for `join_date`, +# we get a validation error: +try: + Customer( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date="2020-01-02", # <- A string shouldn't work now! + age="38" + ) +except ValidationError as e: + print(e) + """ + pydantic.error_wrappers.ValidationError: 1 validation error for Customer + join_date + Value must be a datetime.date object (type=value_error) + """ +``` + ## Saving Models We can save the model to Redis by calling `save()`: ```python +import datetime + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + + +andrew = Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38) + andrew.save() ``` ## Examining Your Data In Redis -## Validating Data +You can view the data stored in Redis for any Redis OM model. + +First, get the key of a model instance you want to inspect. The `key()` method will give you the exact Redis key used to store the model. + +**NOTE:** The naming of this method may be confusing. This is not the primary key, but is instead the Redis key for this model. For this reason, the method name may change. + +In this example, we're looking at the key created for the `Customer` model we've been building: + +```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] = "Super dope" + +andrew = Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38) + +andrew.save() +andrew.key() +#> 'mymodel.Customer:01FKGX1DFEV9Z2XKF59WQ6DC9T' +``` + +With the model's Redis key, you can start `redis-cli` and inspect the data stored under that key. Here, we run `JSON.GET` command with `redis-cli` using the running "redis" container that this project's Docker Compose file defines: + +``` +$ docker-compose exec -T redis redis-cli HGETALL mymodel.Customer:01FKGX1DFEV9Z2XKF59WQ6DC9r + + 1) "pk" + 2) "01FKGX1DFEV9Z2XKF59WQ6DC9T" + 3) "first_name" + 4) "Andrew" + 5) "last_name" + 6) "Brookins" + 7) "email" + 8) "andrew.brookins@example.com" + 9) "join_date" +10) "2021-11-02" +11) "age" +12) "38" +13) "bio" +14) "Super dope" +``` + +## Getting a Model + +If you have the primary key of a model, you can call the `get()` method on the model class to get the model's data. + +```python +import datetime +from typing import Optional + +from redis_om.model import HashModel + + +class Customer(HashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + bio: Optional[str] = "Super dope" + +andrew = Customer( + first_name="Andrew", + last_name="Brookins", + email="andrew.brookins@example.com", + join_date=datetime.date.today(), + age=38) + +andrew.save() + +assert Customer.get(andrew.pk) == andrew +``` + +## Querying for Models With Expressions + +Redis OM comes with a rich query language that allows you to query Redis with Python expressions. + +To show how this works, we'll make a small change to the `Customer` model we defined earlier. We'll add `Field(index=True)` to tell Redis OM that we want to index the `last_name` and `age` fields: + +```python +import datetime +from typing import Optional + +from pydantic import EmailStr + +from redis_om.model import ( + Field, + HashModel, + Migrator +) + + +class Customer(HashModel): + first_name: str + last_name: str = Field(index=True) + email: EmailStr + join_date: datetime.date + age: int = Field(index=True) + bio: Optional[str] + + +# Now, if we use this model with a Redis deployment that has the +# RediSearch module installed, we can run queries like the following. + +# Before running queries, we need to run migrations to set up the +# indexes that Redis OM will use. You can also use the `migrate` +# CLI tool for this! +Migrator().run() + +# Find all customers with the last name "Brookins" +Customer.find(Customer.last_name == "Brookins").all() + +# Find all customers that do NOT have the last name "Brookins" +Customer.find(Customer.last_name != "Brookins").all() + +# Find all customers whose last name is "Brookins" OR whose age is +# 100 AND whose last name is "Smith" +Customer.find((Customer.last_name == "Brookins") | ( + Customer.age == 100 +) & (Customer.last_name == "Smith")).all() +``` + +Many more types of queries are possible. learn more about querying with Redis OM, see the [documentation on querying](docs/querying.md). ## Next Steps -Now that you know the basics of working with Redis OM, continue on for all the nitty-gritty details about [models and fields](validation.md). +Now that you know the basics of working with Redis OM, continue on for all the nitty-gritty details about [models and fields](models_and_fields.md). diff --git a/docs/index.md b/docs/index.md index e69de29..20b4e10 100644 --- a/docs/index.md +++ b/docs/index.md @@ -0,0 +1,3 @@ +# Redis OM Documentation + +WIP! \ No newline at end of file diff --git a/docs/integrating.md b/docs/integrating.md index e69de29..404c079 100644 --- a/docs/integrating.md +++ b/docs/integrating.md @@ -0,0 +1,3 @@ +# Integration Redis OM With Popular Frameworks + +WIP! \ No newline at end of file diff --git a/docs/models_and_fields.md b/docs/models_and_fields.md index b25d4d3..0c082ba 100644 --- a/docs/models_and_fields.md +++ b/docs/models_and_fields.md @@ -1,5 +1,7 @@ # Models and Fields +**NOTE:** This documentation is a stub. Documentation for this project is a work in progress! + ## Introduction ## Saving Data As Hashes With HashModel diff --git a/docs/querying.md b/docs/querying.md index 745faeb..ea589c8 100644 --- a/docs/querying.md +++ b/docs/querying.md @@ -1,15 +1,12 @@ # 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. +**NOTE:** This documentation is a stub that uses examples from other documentation in this project (the README, the Getting Started guide, etc.). Detailed documentation on querying in a work in progress. -With these two classes defined, you can now: +Querying in Redis OM uses a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee. -* 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 +In the following example, we define `Address` and `Customer` models for use with a Redis database that has the [RedisJSON](redis-json-url) module installed. +With these two classes defined, we can query on any indexed fields in the models -- including indexed fields within embedded models. ```python import datetime @@ -19,6 +16,7 @@ from redis_om.model import ( EmbeddedJsonModel, JsonModel, Field, + Migrator ) class Address(EmbeddedJsonModel): @@ -41,11 +39,15 @@ class Customer(JsonModel): # Creates an embedded model. address: Address -``` -Here are a few example queries that use the models we defined earlier: -```python +# Before running queries, we need to run migrations to set up the +# indexes that Redis OM will use. You can also use the `migrate` +# CLI tool for this! +Migrator().run() + +# Here are a few example queries that use these two models... + # Find all customers with the last name "Brookins" Customer.find(Customer.last_name == "Brookins").all() diff --git a/docs/redis_modules.md b/docs/redis_modules.md index cd110dc..db60c4e 100644 --- a/docs/redis_modules.md +++ b/docs/redis_modules.md @@ -25,6 +25,6 @@ You can use RediSearch and RedisJSON with your self-hosted Redis deployment. Jus - [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. +**NOTE**: Both of these modules' Quick Start Guides also have instructions on how to run the 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/) \ No newline at end of file diff --git a/docs/testing.md b/docs/testing.md index 0f1c1c5..7cb7055 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1 +1,5 @@ # Testing Your Models + +**NOTE:** This documentation is a Work in Progress. + +Writing tests that use a Redis OM model requires some setup. For now, review the tests in the redis-om-python project for examples. diff --git a/poetry.lock b/poetry.lock index 6ef4f3f..322f5ed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,7 +23,7 @@ python-versions = "*" [[package]] name = "astroid" -version = "2.8.3" +version = "2.8.4" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -36,11 +36,14 @@ wrapt = ">=1.11,<1.14" [[package]] name = "async-timeout" -version = "3.0.1" +version = "4.0.0" description = "Timeout context manager for asyncio programs" category = "main" optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.6.5" [[package]] name = "atomicwrites" @@ -101,7 +104,7 @@ stevedore = ">=1.20.0" [[package]] name = "black" -version = "21.9b0" +version = "21.10b0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -121,9 +124,9 @@ typing-extensions = [ [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"] +d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -python2 = ["typed-ast (>=1.4.2)"] +python2 = ["typed-ast (>=1.4.3)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] @@ -190,7 +193,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.0.2" +version = "6.1.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -254,7 +257,7 @@ trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] [[package]] name = "docutils" -version = "0.17.1" +version = "0.18" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false @@ -285,7 +288,7 @@ testing = ["pre-commit"] [[package]] name = "filelock" -version = "3.3.1" +version = "3.3.2" description = "A platform independent file lock." category = "dev" optional = false @@ -310,14 +313,14 @@ pyflakes = ">=2.4.0,<2.5.0" [[package]] name = "gitdb" -version = "4.0.7" +version = "4.0.9" description = "Git Object Database" category = "dev" optional = false -python-versions = ">=3.4" +python-versions = ">=3.6" [package.dependencies] -smmap = ">=3.0.1,<5" +smmap = ">=3.0.1,<6" [[package]] name = "gitpython" @@ -378,7 +381,7 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} [[package]] name = "ipython" -version = "7.28.0" +version = "7.29.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false @@ -521,14 +524,14 @@ python-versions = "*" [[package]] name = "packaging" -version = "21.0" +version = "21.2" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3" [[package]] name = "parso" @@ -622,7 +625,7 @@ python-versions = "*" [[package]] name = "prompt-toolkit" -version = "3.0.20" +version = "3.0.21" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false @@ -849,7 +852,7 @@ hiredis = ["hiredis (>=0.1.3)"] [[package]] name = "regex" -version = "2021.10.8" +version = "2021.11.2" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -917,11 +920,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "smmap" -version = "4.0.0" +version = "5.0.0" description = "A pure Python implementation of a sliding window memory map manager" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "stevedore" @@ -944,7 +947,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.1" +version = "1.2.2" description = "A lil' TOML parser" category = "dev" optional = false @@ -1001,7 +1004,7 @@ telegram = ["requests"] [[package]] name = "traitlets" -version = "5.1.0" +version = "5.1.1" description = "Traitlets Python configuration system" category = "dev" optional = false @@ -1068,7 +1071,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.9.0" +version = "20.10.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1082,7 +1085,7 @@ platformdirs = ">=2,<3" six = ">=1.9.0,<2" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] [[package]] @@ -1103,7 +1106,7 @@ python-versions = "*" [[package]] name = "wrapt" -version = "1.13.2" +version = "1.13.3" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false @@ -1123,8 +1126,8 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "fb8766fad12a5598ff95e95e96feae984d7cc64e3ce31eb8d3ad9dfd8b39d621" +python-versions = "^3.8" +content-hash = "1447e711408fe0aecd3bc17697456f773472a131ce2834b7fa09464f21144c37" [metadata.files] aioredis = [ @@ -1136,12 +1139,12 @@ appnope = [ {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, ] astroid = [ - {file = "astroid-2.8.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"}, - {file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"}, + {file = "astroid-2.8.4-py3-none-any.whl", hash = "sha256:0755c998e7117078dcb7d0bda621391dd2a85da48052d948c7411ab187325346"}, + {file = "astroid-2.8.4.tar.gz", hash = "sha256:1e83a69fd51b013ebf5912d26b9338d6643a55fec2f20c787792680610eed4a2"}, ] async-timeout = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, + {file = "async-timeout-4.0.0.tar.gz", hash = "sha256:7d87a4e8adba8ededb52e579ce6bc8276985888913620c935094c2276fd83382"}, + {file = "async_timeout-4.0.0-py3-none-any.whl", hash = "sha256:f3303dddf6cafa748a92747ab6c2ecf60e0aeca769aee4c151adfce243a05d9b"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1164,8 +1167,8 @@ bandit = [ {file = "bandit-1.7.0.tar.gz", hash = "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"}, ] black = [ - {file = "black-21.9b0-py3-none-any.whl", hash = "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115"}, - {file = "black-21.9b0.tar.gz", hash = "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91"}, + {file = "black-21.10b0-py3-none-any.whl", hash = "sha256:6eb7448da9143ee65b856a5f3676b7dda98ad9abe0f87fce8c59291f15e82a5b"}, + {file = "black-21.10b0.tar.gz", hash = "sha256:a9952229092e325fe5f3dae56d81f639b23f7131eb840781947e4b2886030f33"}, ] bleach = [ {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, @@ -1240,39 +1243,55 @@ colorama = [ {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] coverage = [ - {file = "coverage-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1549e1d08ce38259de2bc3e9a0d5f3642ff4a8f500ffc1b2df73fd621a6cdfc0"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcae10fccb27ca2a5f456bf64d84110a5a74144be3136a5e598f9d9fb48c0caa"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:53a294dc53cfb39c74758edaa6305193fb4258a30b1f6af24b360a6c8bd0ffa7"}, - {file = "coverage-6.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8251b37be1f2cd9c0e5ccd9ae0380909c24d2a5ed2162a41fcdbafaf59a85ebd"}, - {file = "coverage-6.0.2-cp310-cp310-win32.whl", hash = "sha256:db42baa892cba723326284490283a68d4de516bfb5aaba369b4e3b2787a778b7"}, - {file = "coverage-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:bbffde2a68398682623d9dd8c0ca3f46fda074709b26fcf08ae7a4c431a6ab2d"}, - {file = "coverage-6.0.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:60e51a3dd55540bec686d7fff61b05048ca31e804c1f32cbb44533e6372d9cc3"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6a9409223a27d5ef3cca57dd7cd4dfcb64aadf2fad5c3b787830ac9223e01a"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4b34ae4f51bbfa5f96b758b55a163d502be3dcb24f505d0227858c2b3f94f5b9"}, - {file = "coverage-6.0.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbda1b550e70fa6ac40533d3f23acd4f4e9cb4e6e77251ce77fdf41b3309fb2"}, - {file = "coverage-6.0.2-cp36-cp36m-win32.whl", hash = "sha256:4e28d2a195c533b58fc94a12826f4431726d8eb029ac21d874345f943530c122"}, - {file = "coverage-6.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a82d79586a0a4f5fd1cf153e647464ced402938fbccb3ffc358c7babd4da1dd9"}, - {file = "coverage-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3be1206dc09fb6298de3fce70593e27436862331a85daee36270b6d0e1c251c4"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9cd3828bbe1a40070c11fe16a51df733fd2f0cb0d745fb83b7b5c1f05967df7"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d036dc1ed8e1388e995833c62325df3f996675779541f682677efc6af71e96cc"}, - {file = "coverage-6.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04560539c19ec26995ecfb3d9307ff154fbb9a172cb57e3b3cfc4ced673103d1"}, - {file = "coverage-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:e4fb7ced4d9dec77d6cf533acfbf8e1415fe799430366affb18d69ee8a3c6330"}, - {file = "coverage-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:77b1da5767ed2f44611bc9bc019bc93c03fa495728ec389759b6e9e5039ac6b1"}, - {file = "coverage-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61b598cbdbaae22d9e34e3f675997194342f866bb1d781da5d0be54783dce1ff"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36e9040a43d2017f2787b28d365a4bb33fcd792c7ff46a047a04094dc0e2a30d"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9f1627e162e3864a596486774876415a7410021f4b67fd2d9efdf93ade681afc"}, - {file = "coverage-6.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e7a0b42db2a47ecb488cde14e0f6c7679a2c5a9f44814393b162ff6397fcdfbb"}, - {file = "coverage-6.0.2-cp38-cp38-win32.whl", hash = "sha256:a1b73c7c4d2a42b9d37dd43199c5711d91424ff3c6c22681bc132db4a4afec6f"}, - {file = "coverage-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:1db67c497688fd4ba85b373b37cc52c50d437fd7267520ecd77bddbd89ea22c9"}, - {file = "coverage-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f2f184bf38e74f152eed7f87e345b51f3ab0b703842f447c22efe35e59942c24"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1cf1deb3d5544bd942356364a2fdc8959bad2b6cf6eb17f47d301ea34ae822"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ad9b8c1206ae41d46ec7380b78ba735ebb77758a650643e841dd3894966c31d0"}, - {file = "coverage-6.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:381d773d896cc7f8ba4ff3b92dee4ed740fb88dfe33b6e42efc5e8ab6dfa1cfe"}, - {file = "coverage-6.0.2-cp39-cp39-win32.whl", hash = "sha256:424c44f65e8be58b54e2b0bd1515e434b940679624b1b72726147cfc6a9fc7ce"}, - {file = "coverage-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:abbff240f77347d17306d3201e14431519bf64495648ca5a49571f988f88dee9"}, - {file = "coverage-6.0.2-pp36-none-any.whl", hash = "sha256:7092eab374346121805fb637572483270324407bf150c30a3b161fc0c4ca5164"}, - {file = "coverage-6.0.2-pp37-none-any.whl", hash = "sha256:30922626ce6f7a5a30bdba984ad21021529d3d05a68b4f71ea3b16bda35b8895"}, - {file = "coverage-6.0.2.tar.gz", hash = "sha256:6807947a09510dc31fa86f43595bf3a14017cd60bf633cc746d52141bfa6b149"}, + {file = "coverage-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42a1fb5dee3355df90b635906bb99126faa7936d87dfc97eacc5293397618cb7"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a00284dbfb53b42e35c7dd99fc0e26ef89b4a34efff68078ed29d03ccb28402a"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:51a441011a30d693e71dea198b2a6f53ba029afc39f8e2aeb5b77245c1b282ef"}, + {file = "coverage-6.1.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e76f017b6d4140a038c5ff12be1581183d7874e41f1c0af58ecf07748d36a336"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7833c872718dc913f18e51ee97ea0dece61d9930893a58b20b3daf09bb1af6b6"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8186b5a4730c896cbe1e4b645bdc524e62d874351ae50e1db7c3e9f5dc81dc26"}, + {file = "coverage-6.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbca34dca5a2d60f81326d908d77313816fad23d11b6069031a3d6b8c97a54f9"}, + {file = "coverage-6.1.1-cp310-cp310-win32.whl", hash = "sha256:72bf437d54186d104388cbae73c9f2b0f8a3e11b6e8d7deb593bd14625c96026"}, + {file = "coverage-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:994ce5a7b3d20981b81d83618aa4882f955bfa573efdbef033d5632b58597ba9"}, + {file = "coverage-6.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ab6a0fe4c96f8058d41948ddf134420d3ef8c42d5508b5a341a440cce7a37a1d"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10ab138b153e4cc408b43792cb7f518f9ee02f4ff55cd1ab67ad6fd7e9905c7e"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7e083d32965d2eb6638a77e65b622be32a094fdc0250f28ce6039b0732fbcaa8"}, + {file = "coverage-6.1.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:359a32515e94e398a5c0fa057e5887a42e647a9502d8e41165cf5cb8d3d1ca67"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:bf656cd74ff7b4ed7006cdb2a6728150aaad69c7242b42a2a532f77b63ea233f"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dc5023be1c2a8b0a0ab5e31389e62c28b2453eb31dd069f4b8d1a0f9814d951a"}, + {file = "coverage-6.1.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:557594a50bfe3fb0b1b57460f6789affe8850ad19c1acf2d14a3e12b2757d489"}, + {file = "coverage-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:9eb0a1923354e0fdd1c8a6f53f5db2e6180d670e2b587914bf2e79fa8acfd003"}, + {file = "coverage-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:04a92a6cf9afd99f9979c61348ec79725a9f9342fb45e63c889e33c04610d97b"}, + {file = "coverage-6.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:479228e1b798d3c246ac89b09897ee706c51b3e5f8f8d778067f38db73ccc717"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78287731e3601ea5ce9d6468c82d88a12ef8fe625d6b7bdec9b45d96c1ad6533"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c95257aa2ccf75d3d91d772060538d5fea7f625e48157f8ca44594f94d41cb33"}, + {file = "coverage-6.1.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ad5895938a894c368d49d8470fe9f519909e5ebc6b8f8ea5190bd0df6aa4271"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:326d944aad0189603733d646e8d4a7d952f7145684da973c463ec2eefe1387c2"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e7d5606b9240ed4def9cbdf35be4308047d11e858b9c88a6c26974758d6225ce"}, + {file = "coverage-6.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:572f917267f363101eec375c109c9c1118037c7cc98041440b5eabda3185ac7b"}, + {file = "coverage-6.1.1-cp37-cp37m-win32.whl", hash = "sha256:35cd2230e1ed76df7d0081a997f0fe705be1f7d8696264eb508076e0d0b5a685"}, + {file = "coverage-6.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:65ad3ff837c89a229d626b8004f0ee32110f9bfdb6a88b76a80df36ccc60d926"}, + {file = "coverage-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:977ce557d79577a3dd510844904d5d968bfef9489f512be65e2882e1c6eed7d8"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62512c0ec5d307f56d86504c58eace11c1bc2afcdf44e3ff20de8ca427ca1d0e"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2e5b9c17a56b8bf0c0a9477fcd30d357deb486e4e1b389ed154f608f18556c8a"}, + {file = "coverage-6.1.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:666c6b32b69e56221ad1551d377f718ed00e6167c7a1b9257f780b105a101271"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fb2fa2f6506c03c48ca42e3fe5a692d7470d290c047ee6de7c0f3e5fa7639ac9"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f0f80e323a17af63eac6a9db0c9188c10f1fd815c3ab299727150cc0eb92c7a4"}, + {file = "coverage-6.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:738e823a746841248b56f0f3bd6abf3b73af191d1fd65e4c723b9c456216f0ad"}, + {file = "coverage-6.1.1-cp38-cp38-win32.whl", hash = "sha256:8605add58e6a960729aa40c0fd9a20a55909dd9b586d3e8104cc7f45869e4c6b"}, + {file = "coverage-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:6e994003e719458420e14ffb43c08f4c14990e20d9e077cb5cad7a3e419bbb54"}, + {file = "coverage-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e3c4f5211394cd0bf6874ac5d29684a495f9c374919833dcfff0bd6d37f96201"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e14bceb1f3ae8a14374be2b2d7bc12a59226872285f91d66d301e5f41705d4d6"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0147f7833c41927d84f5af9219d9b32f875c0689e5e74ac8ca3cb61e73a698f9"}, + {file = "coverage-6.1.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b1d0a1bce919de0dd8da5cff4e616b2d9e6ebf3bd1410ff645318c3dd615010a"}, + {file = "coverage-6.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae6de0e41f44794e68d23644636544ed8003ce24845f213b24de097cbf44997f"}, + {file = "coverage-6.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2797ed7a7e883b9ab76e8e778bb4c859fc2037d6fd0644d8675e64d58d1653"}, + {file = "coverage-6.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c40966b683d92869b72ea3c11fd6b99a091fd30e12652727eca117273fc97366"}, + {file = "coverage-6.1.1-cp39-cp39-win32.whl", hash = "sha256:a11a2c019324fc111485e79d55907e7289e53d0031275a6c8daed30690bc50c0"}, + {file = "coverage-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4d8b453764b9b26b0dd2afb83086a7c3f9379134e340288d2a52f8a91592394b"}, + {file = "coverage-6.1.1-pp36-none-any.whl", hash = "sha256:3b270c6b48d3ff5a35deb3648028ba2643ad8434b07836782b1139cf9c66313f"}, + {file = "coverage-6.1.1-pp37-none-any.whl", hash = "sha256:ffa8fee2b1b9e60b531c4c27cf528d6b5d5da46b1730db1f4d6eee56ff282e07"}, + {file = "coverage-6.1.1-pp38-none-any.whl", hash = "sha256:4cd919057636f63ab299ccb86ea0e78b87812400c76abab245ca385f17d19fb5"}, + {file = "coverage-6.1.1.tar.gz", hash = "sha256:b8e4f15b672c9156c1154249a9c5746e86ac9ae9edc3799ee3afebc323d9d9e0"}, ] cryptography = [ {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, @@ -1309,8 +1328,8 @@ dnspython = [ {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"}, + {file = "docutils-0.18-py2.py3-none-any.whl", hash = "sha256:a31688b2ea858517fa54293e5d5df06fbb875fb1f7e4c64529271b77781ca8fc"}, + {file = "docutils-0.18.tar.gz", hash = "sha256:c1d5dab2b11d16397406a282e53953fe495a46d69ae329f55aa98a5c4e3c5fbb"}, ] email-validator = [ {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, @@ -1321,16 +1340,16 @@ execnet = [ {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] filelock = [ - {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"}, - {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"}, + {file = "filelock-3.3.2-py3-none-any.whl", hash = "sha256:bb2a1c717df74c48a2d00ed625e5a66f8572a3a30baacb7657add1d7bac4097b"}, + {file = "filelock-3.3.2.tar.gz", hash = "sha256:7afc856f74fa7006a289fd10fa840e1eebd8bbff6bffb69c26c54a0512ea8cf8"}, ] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, ] gitdb = [ - {file = "gitdb-4.0.7-py3-none-any.whl", hash = "sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0"}, - {file = "gitdb-4.0.7.tar.gz", hash = "sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"}, + {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, + {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, ] gitpython = [ {file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"}, @@ -1352,8 +1371,8 @@ ipdb = [ {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, ] ipython = [ - {file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"}, - {file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"}, + {file = "ipython-7.29.0-py3-none-any.whl", hash = "sha256:a658beaf856ce46bc453366d5dc6b2ddc6c481efd3540cb28aa3943819caac9f"}, + {file = "ipython-7.29.0.tar.gz", hash = "sha256:4f69d7423a5a1972f6347ff233e38bbf4df6a150ef20fbb00c635442ac3060aa"}, ] isort = [ {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, @@ -1433,8 +1452,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"}, + {file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"}, ] parso = [ {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, @@ -1472,8 +1491,8 @@ pptree = [ {file = "pptree-3.1.tar.gz", hash = "sha256:4dd0ba2f58000cbd29d68a5b64bac29bcb5a663642f79404877c0059668a69f6"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.20-py3-none-any.whl", hash = "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c"}, - {file = "prompt_toolkit-3.0.20.tar.gz", hash = "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"}, + {file = "prompt_toolkit-3.0.21-py3-none-any.whl", hash = "sha256:62b3d3ea5a3ccee94dc1aac018279cf64866a76837156ebe159b981c42dd20a8"}, + {file = "prompt_toolkit-3.0.21.tar.gz", hash = "sha256:27f13ff4e4850fe8f860b77414c7880f67c6158076a7b099062cc8570f1562e5"}, ] ptyprocess = [ {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, @@ -1603,53 +1622,55 @@ redis = [ {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, ] regex = [ - {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:094a905e87a4171508c2a0e10217795f83c636ccc05ddf86e7272c26e14056ae"}, - {file = "regex-2021.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:981c786293a3115bc14c103086ae54e5ee50ca57f4c02ce7cf1b60318d1e8072"}, - {file = "regex-2021.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b0f2f874c6a157c91708ac352470cb3bef8e8814f5325e3c5c7a0533064c6a24"}, - {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51feefd58ac38eb91a21921b047da8644155e5678e9066af7bcb30ee0dca7361"}, - {file = "regex-2021.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8de658d7db5987b11097445f2b1f134400e2232cb40e614e5f7b6f5428710e"}, - {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1ce02f420a7ec3b2480fe6746d756530f69769292eca363218c2291d0b116a01"}, - {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39079ebf54156be6e6902f5c70c078f453350616cfe7bfd2dd15bdb3eac20ccc"}, - {file = "regex-2021.10.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ff24897f6b2001c38a805d53b6ae72267025878d35ea225aa24675fbff2dba7f"}, - {file = "regex-2021.10.8-cp310-cp310-win32.whl", hash = "sha256:c6569ba7b948c3d61d27f04e2b08ebee24fec9ff8e9ea154d8d1e975b175bfa7"}, - {file = "regex-2021.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:45cb0f7ff782ef51bc79e227a87e4e8f24bc68192f8de4f18aae60b1d60bc152"}, - {file = "regex-2021.10.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fab3ab8aedfb443abb36729410403f0fe7f60ad860c19a979d47fb3eb98ef820"}, - {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e55f8d66f1b41d44bc44c891bcf2c7fad252f8f323ee86fba99d71fd1ad5e3"}, - {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d52c5e089edbdb6083391faffbe70329b804652a53c2fdca3533e99ab0580d9"}, - {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1abbd95cbe9e2467cac65c77b6abd9223df717c7ae91a628502de67c73bf6838"}, - {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9b5c215f3870aa9b011c00daeb7be7e1ae4ecd628e9beb6d7e6107e07d81287"}, - {file = "regex-2021.10.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f540f153c4f5617bc4ba6433534f8916d96366a08797cbbe4132c37b70403e92"}, - {file = "regex-2021.10.8-cp36-cp36m-win32.whl", hash = "sha256:1f51926db492440e66c89cd2be042f2396cf91e5b05383acd7372b8cb7da373f"}, - {file = "regex-2021.10.8-cp36-cp36m-win_amd64.whl", hash = "sha256:5f55c4804797ef7381518e683249310f7f9646da271b71cb6b3552416c7894ee"}, - {file = "regex-2021.10.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb2baff66b7d2267e07ef71e17d01283b55b3cc51a81b54cc385e721ae172ba4"}, - {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e527ab1c4c7cf2643d93406c04e1d289a9d12966529381ce8163c4d2abe4faf"}, - {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c98b013273e9da5790ff6002ab326e3f81072b4616fd95f06c8fa733d2745f"}, - {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:55ef044899706c10bc0aa052f2fc2e58551e2510694d6aae13f37c50f3f6ff61"}, - {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0ab3530a279a3b7f50f852f1bab41bc304f098350b03e30a3876b7dd89840e"}, - {file = "regex-2021.10.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a37305eb3199d8f0d8125ec2fb143ba94ff6d6d92554c4b8d4a8435795a6eccd"}, - {file = "regex-2021.10.8-cp37-cp37m-win32.whl", hash = "sha256:2efd47704bbb016136fe34dfb74c805b1ef5c7313aef3ce6dcb5ff844299f432"}, - {file = "regex-2021.10.8-cp37-cp37m-win_amd64.whl", hash = "sha256:924079d5590979c0e961681507eb1773a142553564ccae18d36f1de7324e71ca"}, - {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19b8f6d23b2dc93e8e1e7e288d3010e58fafed323474cf7f27ab9451635136d9"}, - {file = "regex-2021.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b09d3904bf312d11308d9a2867427479d277365b1617e48ad09696fa7dfcdf59"}, - {file = "regex-2021.10.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:951be934dc25d8779d92b530e922de44dda3c82a509cdb5d619f3a0b1491fafa"}, - {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f125fce0a0ae4fd5c3388d369d7a7d78f185f904c90dd235f7ecf8fe13fa741"}, - {file = "regex-2021.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f199419a81c1016e0560c39773c12f0bd924c37715bffc64b97140d2c314354"}, - {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:09e1031e2059abd91177c302da392a7b6859ceda038be9e015b522a182c89e4f"}, - {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c070d5895ac6aeb665bd3cd79f673775caf8d33a0b569e98ac434617ecea57d"}, - {file = "regex-2021.10.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:176796cb7f82a7098b0c436d6daac82f57b9101bb17b8e8119c36eecf06a60a3"}, - {file = "regex-2021.10.8-cp38-cp38-win32.whl", hash = "sha256:5e5796d2f36d3c48875514c5cd9e4325a1ca172fc6c78b469faa8ddd3d770593"}, - {file = "regex-2021.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:e4204708fa116dd03436a337e8e84261bc8051d058221ec63535c9403a1582a1"}, - {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6dcf53d35850ce938b4f044a43b33015ebde292840cef3af2c8eb4c860730fff"}, - {file = "regex-2021.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b8b6ee6555b6fbae578f1468b3f685cdfe7940a65675611365a7ea1f8d724991"}, - {file = "regex-2021.10.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e2ec1c106d3f754444abf63b31e5c4f9b5d272272a491fa4320475aba9e8157c"}, - {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:973499dac63625a5ef9dfa4c791aa33a502ddb7615d992bdc89cf2cc2285daa3"}, - {file = "regex-2021.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88dc3c1acd3f0ecfde5f95c32fcb9beda709dbdf5012acdcf66acbc4794468eb"}, - {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4786dae85c1f0624ac77cb3813ed99267c9adb72e59fdc7297e1cf4d6036d493"}, - {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe6ce4f3d3c48f9f402da1ceb571548133d3322003ce01b20d960a82251695d2"}, - {file = "regex-2021.10.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9e3e2cea8f1993f476a6833ef157f5d9e8c75a59a8d8b0395a9a6887a097243b"}, - {file = "regex-2021.10.8-cp39-cp39-win32.whl", hash = "sha256:82cfb97a36b1a53de32b642482c6c46b6ce80803854445e19bc49993655ebf3b"}, - {file = "regex-2021.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:b04e512eb628ea82ed86eb31c0f7fc6842b46bf2601b66b1356a7008327f7700"}, - {file = "regex-2021.10.8.tar.gz", hash = "sha256:26895d7c9bbda5c52b3635ce5991caa90fbb1ddfac9c9ff1c7ce505e2282fb2a"}, + {file = "regex-2021.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:897c539f0f3b2c3a715be651322bef2167de1cdc276b3f370ae81a3bda62df71"}, + {file = "regex-2021.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:886f459db10c0f9d17c87d6594e77be915f18d343ee138e68d259eb385f044a8"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:075b0fdbaea81afcac5a39a0d1bb91de887dd0d93bf692a5dd69c430e7fc58cb"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6238d30dcff141de076344cf7f52468de61729c2f70d776fce12f55fe8df790"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fab29411d75c2eb48070020a40f80255936d7c31357b086e5931c107d48306e"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0148988af0182a0a4e5020e7c168014f2c55a16d11179610f7883dd48ac0ebe"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be30cd315db0168063a1755fa20a31119da91afa51da2907553493516e165640"}, + {file = "regex-2021.11.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e9cec3a62d146e8e122d159ab93ac32c988e2ec0dcb1e18e9e53ff2da4fbd30c"}, + {file = "regex-2021.11.2-cp310-cp310-win32.whl", hash = "sha256:41c66bd6750237a8ed23028a6c9173dc0c92dc24c473e771d3bfb9ee817700c3"}, + {file = "regex-2021.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:0075fe4e2c2720a685fef0f863edd67740ff78c342cf20b2a79bc19388edf5db"}, + {file = "regex-2021.11.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0ed3465acf8c7c10aa2e0f3d9671da410ead63b38a77283ef464cbb64275df58"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab1fea8832976ad0bebb11f652b692c328043057d35e9ebc78ab0a7a30cf9a70"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1e44d860345ab5d4f533b6c37565a22f403277f44c4d2d5e06c325da959883"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9486ebda015913909bc28763c6b92fcc3b5e5a67dee4674bceed112109f5dfb8"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20605bfad484e1341b2cbfea0708e4b211d233716604846baa54b94821f487cb"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f20f9f430c33597887ba9bd76635476928e76cad2981643ca8be277b8e97aa96"}, + {file = "regex-2021.11.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1d85ca137756d62c8138c971453cafe64741adad1f6a7e63a22a5a8abdbd19fa"}, + {file = "regex-2021.11.2-cp36-cp36m-win32.whl", hash = "sha256:af23b9ca9a874ef0ec20e44467b8edd556c37b0f46f93abfa93752ea7c0e8d1e"}, + {file = "regex-2021.11.2-cp36-cp36m-win_amd64.whl", hash = "sha256:070336382ca92c16c45b4066c4ba9fa83fb0bd13d5553a82e07d344df8d58a84"}, + {file = "regex-2021.11.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef4e53e2fdc997d91f5b682f81f7dc9661db9a437acce28745d765d251902d85"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35ed5714467fc606551db26f80ee5d6aa1f01185586a7bccd96f179c4b974a11"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee36d5113b6506b97f45f2e8447cb9af146e60e3f527d93013d19f6d0405f3b"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fba661a4966adbd2c3c08d3caad6822ecb6878f5456588e2475ae23a6e47929"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77f9d16f7970791f17ecce7e7f101548314ed1ee2583d4268601f30af3170856"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6a28e87ba69f3a4f30d775b179aac55be1ce59f55799328a0d9b6df8f16b39d"}, + {file = "regex-2021.11.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9267e4fba27e6dd1008c4f2983cc548c98b4be4444e3e342db11296c0f45512f"}, + {file = "regex-2021.11.2-cp37-cp37m-win32.whl", hash = "sha256:d4bfe3bc3976ccaeb4ae32f51e631964e2f0e85b2b752721b7a02de5ce3b7f27"}, + {file = "regex-2021.11.2-cp37-cp37m-win_amd64.whl", hash = "sha256:2bb7cae741de1aa03e3dd3a7d98c304871eb155921ca1f0d7cc11f5aade913fd"}, + {file = "regex-2021.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:23f93e74409c210de4de270d4bf88fb8ab736a7400f74210df63a93728cf70d6"}, + {file = "regex-2021.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8ee91e1c295beb5c132ebd78616814de26fedba6aa8687ea460c7f5eb289b72"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e3ff69ab203b54ce5c480c3ccbe959394ea5beef6bd5ad1785457df7acea92e"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3c00cb5c71da655e1e5161481455479b613d500dd1bd252aa01df4f037c641f"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf35e16f4b639daaf05a2602c1b1d47370e01babf9821306aa138924e3fe92"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb11c982a849dc22782210b01d0c1b98eb3696ce655d58a54180774e4880ac66"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e3755e0f070bc31567dfe447a02011bfa8444239b3e9e5cca6773a22133839"}, + {file = "regex-2021.11.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0621c90f28d17260b41838b22c81a79ff436141b322960eb49c7b3f91d1cbab6"}, + {file = "regex-2021.11.2-cp38-cp38-win32.whl", hash = "sha256:8fbe1768feafd3d0156556677b8ff234c7bf94a8110e906b2d73506f577a3269"}, + {file = "regex-2021.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:f9ee98d658a146cb6507be720a0ce1b44f2abef8fb43c2859791d91aace17cd5"}, + {file = "regex-2021.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3794cea825f101fe0df9af8a00f9fad8e119c91e39a28636b95ee2b45b6c2e5"}, + {file = "regex-2021.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3576e173e7b4f88f683b4de7db0c2af1b209bb48b2bf1c827a6f3564fad59a97"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b4f4810117a9072a5aa70f7fea5f86fa9efbe9a798312e0a05044bd707cc33"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5930d334c2f607711d54761956aedf8137f83f1b764b9640be21d25a976f3a4"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:956187ff49db7014ceb31e88fcacf4cf63371e6e44d209cf8816cd4a2d61e11a"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e095f7f96a4b9f24b93c2c915f31a5201a6316618d919b0593afb070a5270e"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a56735c35a3704603d9d7b243ee06139f0837bcac2171d9ba1d638ce1df0742a"}, + {file = "regex-2021.11.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:adf35d88d9cffc202e6046e4c32e1e11a1d0238b2fcf095c94f109e510ececea"}, + {file = "regex-2021.11.2-cp39-cp39-win32.whl", hash = "sha256:30fe317332de0e50195665bc61a27d46e903d682f94042c36b3f88cb84bd7958"}, + {file = "regex-2021.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:85289c25f658e3260b00178757c87f033f3d4b3e40aa4abdd4dc875ff11a94fb"}, + {file = "regex-2021.11.2.tar.gz", hash = "sha256:5e85dcfc5d0f374955015ae12c08365b565c6f1eaf36dd182476a4d8e5a1cdb7"}, ] requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, @@ -1672,8 +1693,8 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] smmap = [ - {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, - {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, + {file = "smmap-5.0.0-py3-none-any.whl", hash = "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94"}, + {file = "smmap-5.0.0.tar.gz", hash = "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"}, ] stevedore = [ {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, @@ -1684,8 +1705,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.1-py3-none-any.whl", hash = "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f"}, - {file = "tomli-1.2.1.tar.gz", hash = "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442"}, + {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, + {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, ] tox = [ {file = "tox-3.24.4-py2.py3-none-any.whl", hash = "sha256:5e274227a53dc9ef856767c21867377ba395992549f02ce55eb549f9fb9a8d10"}, @@ -1700,8 +1721,8 @@ tqdm = [ {file = "tqdm-4.62.3.tar.gz", hash = "sha256:d359de7217506c9851b7869f3708d8ee53ed70a1b8edbba4dbcb47442592920d"}, ] traitlets = [ - {file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"}, - {file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"}, + {file = "traitlets-5.1.1-py3-none-any.whl", hash = "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033"}, + {file = "traitlets-5.1.1.tar.gz", hash = "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7"}, ] twine = [ {file = "twine-3.4.2-py3-none-any.whl", hash = "sha256:087328e9bb405e7ce18527a2dca4042a84c7918658f951110b38bc135acab218"}, @@ -1725,8 +1746,8 @@ urllib3 = [ {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] virtualenv = [ - {file = "virtualenv-20.9.0-py2.py3-none-any.whl", hash = "sha256:1d145deec2da86b29026be49c775cc5a9aab434f85f7efef98307fb3965165de"}, - {file = "virtualenv-20.9.0.tar.gz", hash = "sha256:bb55ace18de14593947354e5e6cd1be75fb32c3329651da62e92bf5d0aab7213"}, + {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, + {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1737,50 +1758,57 @@ webencodings = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] wrapt = [ - {file = "wrapt-1.13.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3de7b4d3066cc610054e7aa2c005645e308df2f92be730aae3a47d42e910566a"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:8164069f775c698d15582bf6320a4f308c50d048c1c10cf7d7a341feaccf5df7"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9adee1891253670575028279de8365c3a02d3489a74a66d774c321472939a0b1"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a70d876c9aba12d3bd7f8f1b05b419322c6789beb717044eea2c8690d35cb91b"}, - {file = "wrapt-1.13.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3f87042623530bcffea038f824b63084180513c21e2e977291a9a7e65a66f13b"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e634136f700a21e1fcead0c137f433dde928979538c14907640607d43537d468"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3e33c138d1e3620b1e0cc6fd21e46c266393ed5dae0d595b7ed5a6b73ed57aa0"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:283e402e5357e104ac1e3fba5791220648e9af6fb14ad7d9cc059091af2b31d2"}, - {file = "wrapt-1.13.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:ccb34ce599cab7f36a4c90318697ead18312c67a9a76327b3f4f902af8f68ea1"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:fbad5ba74c46517e6488149514b2e2348d40df88cd6b52a83855b7a8bf04723f"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:724ed2bc9c91a2b9026e5adce310fa60c6e7c8760b03391445730b9789b9d108"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:83f2793ec6f3ef513ad8d5b9586f5ee6081cad132e6eae2ecb7eac1cc3decae0"}, - {file = "wrapt-1.13.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:0473d1558b93e314e84313cc611f6c86be779369f9d3734302bf185a4d2625b1"}, - {file = "wrapt-1.13.2-cp35-cp35m-win32.whl", hash = "sha256:15eee0e6fd07f48af2f66d0e6f2ff1916ffe9732d464d5e2390695296872cad9"}, - {file = "wrapt-1.13.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bc85d17d90201afd88e3d25421da805e4e135012b5d1f149e4de2981394b2a52"}, - {file = "wrapt-1.13.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6ee5f8734820c21b9b8bf705e99faba87f21566d20626568eeb0d62cbeaf23c"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:53c6706a1bcfb6436f1625511b95b812798a6d2ccc51359cd791e33722b5ea32"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fbe6aebc9559fed7ea27de51c2bf5c25ba2a4156cf0017556f72883f2496ee9a"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:0582180566e7a13030f896c2f1ac6a56134ab5f3c3f4c5538086f758b1caf3f2"}, - {file = "wrapt-1.13.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:bff0a59387a0a2951cb869251257b6553663329a1b5525b5226cab8c88dcbe7e"}, - {file = "wrapt-1.13.2-cp36-cp36m-win32.whl", hash = "sha256:df3eae297a5f1594d1feb790338120f717dac1fa7d6feed7b411f87e0f2401c7"}, - {file = "wrapt-1.13.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1eb657ed84f4d3e6ad648483c8a80a0cf0a78922ef94caa87d327e2e1ad49b48"}, - {file = "wrapt-1.13.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0cdedf681db878416c05e1831ec69691b0e6577ac7dca9d4f815632e3549580"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:87ee3c73bdfb4367b26c57259995935501829f00c7b3eed373e2ad19ec21e4e4"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3e0d16eedc242d01a6f8cf0623e9cdc3b869329da3f97a15961d8864111d8cf0"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:8318088860968c07e741537030b1abdd8908ee2c71fbe4facdaade624a09e006"}, - {file = "wrapt-1.13.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d90520616fce71c05dedeac3a0fe9991605f0acacd276e5f821842e454485a70"}, - {file = "wrapt-1.13.2-cp37-cp37m-win32.whl", hash = "sha256:22142afab65daffc95863d78effcbd31c19a8003eca73de59f321ee77f73cadb"}, - {file = "wrapt-1.13.2-cp37-cp37m-win_amd64.whl", hash = "sha256:d0d717e10f952df7ea41200c507cc7e24458f4c45b56c36ad418d2e79dacd1d4"}, - {file = "wrapt-1.13.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:593cb049ce1c391e0288523b30426c4430b26e74c7e6f6e2844bd99ac7ecc831"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8860c8011a6961a651b1b9f46fdbc589ab63b0a50d645f7d92659618a3655867"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:ada5e29e59e2feb710589ca1c79fd989b1dd94d27079dc1d199ec954a6ecc724"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:fdede980273aeca591ad354608778365a3a310e0ecdd7a3587b38bc5be9b1808"}, - {file = "wrapt-1.13.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:af9480de8e63c5f959a092047aaf3d7077422ded84695b3398f5d49254af3e90"}, - {file = "wrapt-1.13.2-cp38-cp38-win32.whl", hash = "sha256:c65e623ea7556e39c4f0818200a046cbba7575a6b570ff36122c276fdd30ab0a"}, - {file = "wrapt-1.13.2-cp38-cp38-win_amd64.whl", hash = "sha256:b20703356cae1799080d0ad15085dc3213c1ac3f45e95afb9f12769b98231528"}, - {file = "wrapt-1.13.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c5c4cf188b5643a97e87e2110bbd4f5bc491d54a5b90633837b34d5df6a03fe"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:82223f72eba6f63eafca87a0f614495ae5aa0126fe54947e2b8c023969e9f2d7"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:81a4cf257263b299263472d669692785f9c647e7dca01c18286b8f116dbf6b38"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:728e2d9b7a99dd955d3426f237b940fc74017c4a39b125fec913f575619ddfe9"}, - {file = "wrapt-1.13.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:7574de567dcd4858a2ffdf403088d6df8738b0e1eabea220553abf7c9048f59e"}, - {file = "wrapt-1.13.2-cp39-cp39-win32.whl", hash = "sha256:c7ac2c7a8e34bd06710605b21dd1f3576764443d68e069d2afba9b116014d072"}, - {file = "wrapt-1.13.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e6d1a8eeef415d7fb29fe017de0e48f45e45efd2d1bfda28fc50b7b330859ef"}, - {file = "wrapt-1.13.2.tar.gz", hash = "sha256:dca56cc5963a5fd7c2aa8607017753f534ee514e09103a6c55d2db70b50e7447"}, + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] zipp = [ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, diff --git a/pyproject.toml b/pyproject.toml index 22ade19..314028c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.1" +version = "0.0.2" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] diff --git a/redis_om/model/__init__.py b/redis_om/model/__init__.py index 8fe2844..7df1623 100644 --- a/redis_om/model/__init__.py +++ b/redis_om/model/__init__.py @@ -1 +1,2 @@ +from .migrations.migrator import MigrationError, Migrator from .model import EmbeddedJsonModel, Field, HashModel, JsonModel, RedisModel From 6178b3cd9d1ebd6e3c02420a05dbf1d2e7c78679 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:22:32 -0700 Subject: [PATCH 28/49] Add logo --- README.md | 13 ++++++++++--- images/logo.svg | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 images/logo.svg diff --git a/README.md b/README.md index aadfc10..d6c13f9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,14 @@ -

Redis OM

+
+
+
+ Redis OM +
+
+
+

- Objecting mapping, and more, for Redis. + Object mapping, and more, for Redis and .NET

@@ -227,7 +234,7 @@ These queries -- and more! -- are possible because **Redis OM manages indexes fo Querying with this index features a rich expression syntax inspired by the Django ORM, SQLAlchemy, and Peewee. We think you'll enjoy it! To learn more about how to query with Redis OM, see the [documentation on querying](docs/querying.md). - +**** ### Embedded Models Redis OM can store and query **nested models** like any document database, with the speed and power you get from Redis. Let's see how this works. diff --git a/images/logo.svg b/images/logo.svg new file mode 100644 index 0000000..1c70d4f --- /dev/null +++ b/images/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file From f735b658006e697d5b20a7905fff280ecb4c5cc3 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:23:02 -0700 Subject: [PATCH 29/49] Fix copied text --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6c13f9..38fedd7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

- Object mapping, and more, for Redis and .NET + Object mapping, and more, for Redis and Python

From 7d77348f13d97526de28f25162ff9a77213089a0 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:24:11 -0700 Subject: [PATCH 30/49] Bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 314028c..8cdcad5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.2" +version = "0.0.3" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] From c128038d3386f10760fe47936ab6c4dddfafc4b8 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:36:20 -0700 Subject: [PATCH 31/49] Include docs in build package --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8cdcad5..c29dc57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.3" +version = "0.0.4" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] @@ -12,6 +12,7 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Database :: Front-Ends" ] +include=["docs/*"] [tool.poetry.dependencies] python = "^3.8" From 411133012431c3026109a8f2eb275079fd084b13 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:40:08 -0700 Subject: [PATCH 32/49] Add images to built package --- Makefile | 4 ++++ pyproject.toml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8ddd2be..35d2905 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,10 @@ clean: dist: clean $(POETRY) build +.PHONY: upload +upload: dist + $(POETRY) run twine upload dist/* + .PHONY: lint lint: $(INSTALL_STAMP) dist $(POETRY) run isort --profile=black --lines-after-imports=2 ./tests/ $(NAME) diff --git a/pyproject.toml b/pyproject.toml index c29dc57..c8a2b0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.4" +version = "0.0.5" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] @@ -12,7 +12,7 @@ classifiers = [ "Intended Audience :: Developers", "Topic :: Database :: Front-Ends" ] -include=["docs/*"] +include=["docs/*", "images/*"] [tool.poetry.dependencies] python = "^3.8" From 9606a5785269c0afd8465d2358ae74f1d6b36b28 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 14:56:09 -0700 Subject: [PATCH 33/49] Fix mangled CLI examples --- docs/getting_started.md | 45 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index 2e90485..e62eadb 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -41,6 +41,12 @@ Instead of installing Redis manually or with a package manager, you can run Redi **TIP:** If you plan on using Docker, we recommend the [redismod](https://hub.docker.com/r/redislabs/redismod) image because it includes the RediSearch and RedisJSON modules. +You start Redis with Docker with the `docker run` command, like this: + + docker run -d -p 6379:6379 redislabs/redismod + +**NOTE**: We'll talk more about this command (specifically, the arguments chosen) when we discuss running Redis later in this guide. + ## Recommended: RediSearch and RedisJSON Redis OM relies on the [RediSearch][redisearch-url] and [RedisJSON][redis-json-url] Redis modules to support [rich queries](querying.md) and [embedded models](embedded_models.md). @@ -51,7 +57,8 @@ The easiest way to run these Redis modules during local development is to use th You can quickly start Redis with the redismod Docker image by running the following command: -docker run -d -p 6379:6379 redislabs/redismod + docker run -d -p 6379:6379 redislabs/redismod + **TIP:** The `-d` option runs Redis in the background. For other installation methods, follow the "Quick Start" guides on both modules' home pages for alternative installation methods. @@ -66,38 +73,41 @@ The command you use to start Redis will depend on how you installed it. If you installed Redis using `apt`, start it with the `systemctl` command: -sudo systemctl restart redis.service + sudo systemctl restart redis.service + Otherwise, you can start the server manually: -redis-server start + redis-server start ### macOS with Homebrew -brew services start redis + brew services start redis ### Docker The command to start Redis with Docker depends on the image you've chosen to use. -#### Docker with the redismod image (recommended) +#### Docker with the `redismod` image (recommended) -docker run -d -p 6379:6379 redislabs/redismod + docker run -d -p 6379:6379 redislabs/redismod -### Docker iwth the redis image +### Docker with the `redis` image -docker run -d -p 6379:6379 redis + docker run -d -p 6379:6379 redis ## Installing Redis OM You can install Redis OM with `pip` by running the following command: -pip install redis-om + pip install redis-om + Or, if you're using Poetry, you can install Redis OM with the following command: -poetry install redis-om + poetry install redis-om + With Pipenv, the command is: -pipenv install redis-om + pipenv install redis-om ## Setting the Redis URL Environment Variable @@ -109,16 +119,19 @@ However, if you configured Redis to run on a different port, or if you're using The `REDIS_URL` environment variable follows the redis-py URL format: -redis://[[username]:[password]]@localhost:6379/[database number] -The default connection is eqivalent to the following `REDIS_URL` environment variable: + redis://[[username]:[password]]@localhost:6379/[database number] + +The default connection is equivalent to the following `REDIS_URL` environment variable: + + redis://@localhost:6379 -redis://@localhost:6379 **TIP:** Redis databases are numbered, and the default is 0. You can leave off the database number to use the default database. Other supported prefixes include "rediss" for SSL connections and "unix" for Unix domain sockets: -rediss://[[username]:[password]]@localhost:6379/0 -unix://[[username]:[password]]@/path/to/socket.sock?db=0 + rediss://[[username]:[password]]@localhost:6379/0 + unix://[[username]:[password]]@/path/to/socket.sock?db=0 + For more details about how to connect to Redis with Redis OM, see the [connections documentation](connections.md). ### Redis Cluster Support From 9aeb269851e1283699f3479b1c1ec5460441806d Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 15:12:53 -0700 Subject: [PATCH 34/49] Add notes on virtualenvs, etc. --- docs/getting_started.md | 51 +++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/docs/getting_started.md b/docs/getting_started.md index e62eadb..45d05ae 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -35,17 +35,15 @@ Redis doesn't run directly on Windows, but you can use Windows Subsystem for Lin Windows users can also use Docker. See the next section on running Redis with Docker for more information. -### Running Redis With Docker +### Using Redis With Docker -Instead of installing Redis manually or with a package manager, you can run Redis with Docker. The official Redis Docker image is hosted on [Docker Hub](https://hub.docker.com/_/redis). +Instead of installing Redis manually or with a package manager, you can run Redis with Docker. -**TIP:** If you plan on using Docker, we recommend the [redismod](https://hub.docker.com/r/redislabs/redismod) image because it includes the RediSearch and RedisJSON modules. +We recommend the [redismod](https://hub.docker.com/r/redislabs/redismod) image because it includes Redis modules that Redis OM can use to give you extra features. Later sections of this guide will provide more detail about these features. -You start Redis with Docker with the `docker run` command, like this: +You can also use the official Redis Docker image, which is hosted on [Docker Hub](https://hub.docker.com/_/redis). - docker run -d -p 6379:6379 redislabs/redismod - -**NOTE**: We'll talk more about this command (specifically, the arguments chosen) when we discuss running Redis later in this guide. +**NOTE**: We'll talk about how to actually start Redis with Docker when we discuss _running_ Redis later in this guide. ## Recommended: RediSearch and RedisJSON @@ -55,59 +53,58 @@ You don't need these Redis modules to use Redis OM's data modeling, validation, The easiest way to run these Redis modules during local development is to use the [redismod](https://hub.docker.com/r/redislabs/redismod) Docker image. -You can quickly start Redis with the redismod Docker image by running the following command: +For other installation methods, follow the "Quick Start" guides on both modules' home pages. - docker run -d -p 6379:6379 redislabs/redismod - -**TIP:** The `-d` option runs Redis in the background. - -For other installation methods, follow the "Quick Start" guides on both modules' home pages for alternative installation methods. - -## Start Redis +## Starting Redis Before you get started with Redis OM, make sure you start Redis. -The command you use to start Redis will depend on how you installed it. +The command to start Redis will depend on how you installed it. ### Ubuntu Linux (Including WSL) If you installed Redis using `apt`, start it with the `systemctl` command: - sudo systemctl restart redis.service + $ sudo systemctl restart redis.service Otherwise, you can start the server manually: - redis-server start + $ redis-server start ### macOS with Homebrew - brew services start redis + $ brew services start redis ### Docker The command to start Redis with Docker depends on the image you've chosen to use. +**TIP:** The `-d` option in these examples runs Redis in the background, while `-p 6379:6379` makes Redis reachable at port 6379 on your localhost. + #### Docker with the `redismod` image (recommended) - docker run -d -p 6379:6379 redislabs/redismod + $ docker run -d -p 6379:6379 redislabs/redismod ### Docker with the `redis` image - docker run -d -p 6379:6379 redis + $ docker run -d -p 6379:6379 redis ## Installing Redis OM -You can install Redis OM with `pip` by running the following command: +The recommended way to install Redis OM is with [Poetry](https://python-poetry.org/docs/). You can install Redis OM using Poetry with the following command: - pip install redis-om + $ poetry install redis-om -Or, if you're using Poetry, you can install Redis OM with the following command: +If you're using Pipenv, the command is: - poetry install redis-om + $ pipenv install redis-om -With Pipenv, the command is: +Finally, you can install Redis OM with `pip` by running the following command: + + $ pip install redis-om + +**TIP:** If you aren't using Poetry or Pipenv and are instead installing directly with `pip`, we recommend that you install Redis OM in a virtual environment (AKA, a virtualenv). If you aren't familiar with this concept, see [Dan Bader's video and transcript](https://realpython.com/lessons/creating-virtual-environment/). - pipenv install redis-om ## Setting the Redis URL Environment Variable From 3a69a6635a3227c703db0368bea0f8fd5b5a89c4 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 15:13:40 -0700 Subject: [PATCH 35/49] Bump version for the latest docs --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c8a2b0b..3a6e469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.5" +version = "0.0.6" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] From 60dc986947dff73acb094daf3aa694209fa3af71 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 17:06:09 -0700 Subject: [PATCH 36/49] Bump version to test CI --- .github/workflows/ci.yml | 37 +++++++++++++++++++++++++++++-------- pyproject.toml | 2 +- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf722c7..c383c5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -152,15 +152,36 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.9 - - name: Install dependencies - run: - python -m pip install -U pip wheel twine - - name: Make dists - run: - python setup.py sdist bdist_wheel - - name: PyPI upload + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install your root project, if required + #---------------------------------------------- + - name: Install library + run: poetry install --no-interaction + - name: PyPI upload env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: | - twine upload dist/* + make upload diff --git a/pyproject.toml b/pyproject.toml index 3a6e469..01de65c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.6" +version = "0.0.7" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] From ca48b222f339e5a4f1d1b1d17f14861cca565a58 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 2 Nov 2021 17:08:32 -0700 Subject: [PATCH 37/49] Fix yaml for CI #blessed --- .github/workflows/ci.yml | 273 +++++++++++++++++++-------------------- 1 file changed, 136 insertions(+), 137 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c383c5c..58804f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ on: - '[0-9].[0-9]+' - 'update/pre-commit-autoupdate' schedule: - - cron: '0 6 * * *' # Daily 6AM UTC build + - cron: '0 6 * * *' # Daily 6AM UTC build jobs: @@ -22,57 +22,57 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - name: Checkout - uses: actions/checkout@v2.3.5 - - name: Setup Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - #---------------------------------------------- - # ----- install & configure poetry ----- - #---------------------------------------------- - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - #---------------------------------------------- - # load cached venv if cache exists - #---------------------------------------------- - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v2 - with: - path: .venv - key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} - #---------------------------------------------- - # install dependencies if cache does not exist - #---------------------------------------------- - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - #---------------------------------------------- - # install your root project, if required - #---------------------------------------------- - - name: Install library - run: poetry install --no-interaction - #---------------------------------------------- - # run test suite - #---------------------------------------------- - - name: Run linter - run: | - make dist - make lint + - name: Checkout + uses: actions/checkout@v2.3.5 + - name: Setup Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + #---------------------------------------------- + # ----- install & configure poetry ----- + #---------------------------------------------- + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install your root project, if required + #---------------------------------------------- + - name: Install library + run: poetry install --no-interaction + #---------------------------------------------- + # run test suite + #---------------------------------------------- + - name: Run linter + run: | + make dist + make lint test-unix: name: Test Unix needs: lint strategy: matrix: - os: [ubuntu-latest] - pyver: [3.6, 3.7, 3.8, 3.9, pypy3] - redismod: ["preview"] # Removed "edge" because it's broken currently + os: [ ubuntu-latest ] + pyver: [ 3.6, 3.7, 3.8, 3.9, pypy3 ] + redismod: [ "preview" ] # Removed "edge" because it's broken currently fail-fast: false services: redis: @@ -92,96 +92,95 @@ jobs: OS: ${{ matrix.os }} INSTALL_DIR: ${{ github.workspace }}/redis steps: - - name: Checkout - uses: actions/checkout@v2.3.5 - - name: Setup Python ${{ matrix.pyver }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.pyver }} - #---------------------------------------------- - # ----- install & configure poetry ----- - #---------------------------------------------- - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - #---------------------------------------------- - # load cached venv if cache exists - #---------------------------------------------- - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v2 - with: - path: .venv - key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} - #---------------------------------------------- - # install dependencies if cache does not exist - #---------------------------------------------- - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - #---------------------------------------------- - # install your root project, if required - #---------------------------------------------- - - name: Install library - run: poetry install --no-interaction - - name: Run unittests (redismod:${{ matrix.redismod }}, ${{ matrix.os }}) - run: | - make test - poetry run coverage xml - - name: Upload coverage - uses: codecov/codecov-action@v2.1.0 - with: - file: ./coverage.xml - flags: unit - env_vars: OS - fail_ci_if_error: false + - name: Checkout + uses: actions/checkout@v2.3.5 + - name: Setup Python ${{ matrix.pyver }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.pyver }} + #---------------------------------------------- + # ----- install & configure poetry ----- + #---------------------------------------------- + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install your root project, if required + #---------------------------------------------- + - name: Install library + run: poetry install --no-interaction + - name: Run unittests (redismod:${{ matrix.redismod }}, ${{ matrix.os }}) + run: | + make test + poetry run coverage xml + - name: Upload coverage + uses: codecov/codecov-action@v2.1.0 + with: + file: ./coverage.xml + flags: unit + env_vars: OS + fail_ci_if_error: false deploy: - name: Deploy - runs-on: ubuntu-latest - needs: test-unix - # Run only on pushing a tag - if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - steps: - - name: Checkout - uses: actions/checkout@v2.3.5 - - name: Setup Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - virtualenvs-create: true - virtualenvs-in-project: true - installer-parallel: true - #---------------------------------------------- - # load cached venv if cache exists - #---------------------------------------------- - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v2 - with: - path: .venv - key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} - #---------------------------------------------- - # install dependencies if cache does not exist - #---------------------------------------------- - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - #---------------------------------------------- - # install your root project, if required - #---------------------------------------------- - - name: Install library - run: poetry install --no-interaction - - name: PyPI upload - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} - run: | - make upload + name: Deploy + runs-on: ubuntu-latest + needs: test-unix + # Run only on pushing a tag + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + steps: + - name: Checkout + uses: actions/checkout@v2.3.5 + - name: Setup Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + #---------------------------------------------- + # load cached venv if cache exists + #---------------------------------------------- + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v2 + with: + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }} + #---------------------------------------------- + # install dependencies if cache does not exist + #---------------------------------------------- + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: poetry install --no-interaction --no-root + #---------------------------------------------- + # install your root project, if required + #---------------------------------------------- + - name: Install library + run: poetry install --no-interaction + - name: PyPI upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: | + make upload From 2b1994b98b58d28a6567ce48dbaca346825d4a7b Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Wed, 3 Nov 2021 12:37:09 -0700 Subject: [PATCH 38/49] Disable features without required Redis modules Some features, like querying and embedded models, require either the RediSearch or RedisJSON modules running in Redis. Without these modules, using these features would result in inscrutable errors. We now disable some tests if the Redis module required for the test is not found in the Redis instance the tests are using, and raise errors or log messages if the same is true during execution of HashModel and JsonModel. --- Makefile | 8 ++ docker-compose.yml | 8 ++ docs/getting_started.md | 10 +- redis_om/checks.py | 28 +++++ redis_om/model/model.py | 46 ++++++++- tests/test_hash_model.py | 4 + tests/test_json_model.py | 6 +- tests/test_oss_redis_features.py | 169 +++++++++++++++++++++++++++++++ 8 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 redis_om/checks.py create mode 100644 tests/test_oss_redis_features.py diff --git a/Makefile b/Makefile index 35d2905..8f3f51f 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,14 @@ format: $(INSTALL_STAMP) test: $(INSTALL_STAMP) $(POETRY) run pytest -n auto -s -vv ./tests/ --cov-report term-missing --cov $(NAME) +.PHONY: test_oss +test_oss: $(INSTALL_STAMP) + # Specifically tests against a local OSS Redis instance via + # docker-compose.yml. Do not use this for CI testing, where we should + # instead have a matrix of Docker images. + REDIS_OM_URL="redis://localhost:6381" $(POETRY) run pytest -n auto -s -vv ./tests/ --cov-report term-missing --cov $(NAME) + + .PHONY: shell shell: $(INSTALL_STAMP) $(POETRY) shell diff --git a/docker-compose.yml b/docker-compose.yml index 87e406a..f333d22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,11 @@ services: - "6380:6379" volumes: - ./data:/data + + oss_redis: + image: "redis:latest" + restart: always + ports: + - "6381:6379" + volumes: + - ./oss_data:/oss_data \ No newline at end of file diff --git a/docs/getting_started.md b/docs/getting_started.md index 45d05ae..5240403 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -112,13 +112,13 @@ We're almost ready to create a Redis OM model! But first, we need to make sure t By default, Redis OM tries to connect to Redis on your localhost at port 6379. Most local install methods will result in Redis running at this location, in which case you don't need to do anything special. -However, if you configured Redis to run on a different port, or if you're using a remote Redis server, you'll need to set the `REDIS_URL` environment variable. +However, if you configured Redis to run on a different port, or if you're using a remote Redis server, you'll need to set the `REDIS_OM_URL` environment variable. -The `REDIS_URL` environment variable follows the redis-py URL format: +The `REDIS_OM_URL` environment variable follows the redis-py URL format: redis://[[username]:[password]]@localhost:6379/[database number] -The default connection is equivalent to the following `REDIS_URL` environment variable: +The default connection is equivalent to the following `REDIS_OM_URL` environment variable: redis://@localhost:6379 @@ -133,11 +133,11 @@ For more details about how to connect to Redis with Redis OM, see the [connectio ### Redis Cluster Support -Redis OM supports connecting to Redis Cluster, but this preview release does not support doing so with the `REDIS_URL` environment variable. However, you can connect by manually creating a connection object. +Redis OM supports connecting to Redis Cluster, but this preview release does not support doing so with the `REDIS_OM_URL` environment variable. However, you can connect by manually creating a connection object. See the [connections documentation](connections.md) for examples of connecting to Redis Cluster. -Support for connecting to Redis Cluster via `REDIS_URL` will be added in a future release. +Support for connecting to Redis Cluster via `REDIS_OM_URL` will be added in a future release. ## Defining a Model diff --git a/redis_om/checks.py b/redis_om/checks.py new file mode 100644 index 0000000..fde1d87 --- /dev/null +++ b/redis_om/checks.py @@ -0,0 +1,28 @@ +from functools import lru_cache +from typing import List + +from redis_om.connections import get_redis_connection + + +@lru_cache(maxsize=None) +def get_modules(conn) -> List[str]: + modules = conn.execute_command("module", "list") + return [m[1] for m in modules] + + +@lru_cache(maxsize=None) +def has_redis_json(conn=None): + if conn is None: + conn = get_redis_connection() + names = get_modules(conn) + return b"ReJSON" in names or "ReJSON" in names + + +@lru_cache(maxsize=None) +def has_redisearch(conn=None): + if conn is None: + conn = get_redis_connection() + if has_redis_json(conn): + return True + names = get_modules(conn) + return b"search" in names or "search" in names diff --git a/redis_om/model/model.py b/redis_om/model/model.py index 332340d..0effa91 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -37,6 +37,7 @@ from pydantic.utils import Representation from redis.client import Pipeline from ulid import ULID +from ..checks import has_redis_json, has_redisearch from ..connections import get_redis_connection from .encoders import jsonable_encoder from .render_tree import render_tree @@ -121,6 +122,20 @@ def validate_model_fields(model: Type["RedisModel"], field_values: Dict[str, Any ) +def decode_redis_value( + obj: Union[List[bytes], Dict[bytes, bytes], bytes], encoding: str +) -> Union[List[str], Dict[str, str], str]: + """Decode a binary-encoded Redis hash into the specified encoding.""" + if isinstance(obj, list): + return [v.decode(encoding) for v in obj] + if isinstance(obj, dict): + return { + key.decode(encoding): value.decode(encoding) for key, value in obj.items() + } + elif isinstance(obj, bytes): + return obj.decode(encoding) + + class ExpressionProtocol(Protocol): op: Operators left: ExpressionOrModelField @@ -317,6 +332,11 @@ class FindQuery: page_size: int = DEFAULT_PAGE_SIZE, sort_fields: Optional[List[str]] = None, ): + if not has_redisearch(model.db()): + raise RedisModelError("Your Redis instance does not have either the RediSearch module " + "or RedisJSON module installed. Querying requires that your Redis " + "instance has one of these modules installed.") + self.expressions = expressions self.model = model self.offset = offset @@ -330,8 +350,8 @@ class FindQuery: self._expression = None self._query: Optional[str] = None - self._pagination: list[str] = [] - self._model_cache: list[RedisModel] = [] + self._pagination: List[str] = [] + self._model_cache: List[RedisModel] = [] def dict(self) -> Dict[str, Any]: return dict( @@ -919,6 +939,7 @@ class MetaProtocol(Protocol): index_name: str abstract: bool embedded: bool + encoding: str @dataclasses.dataclass @@ -938,6 +959,7 @@ class DefaultMeta: index_name: Optional[str] = None abstract: Optional[bool] = False embedded: Optional[bool] = False + encoding: Optional[str] = "utf-8" class ModelMeta(ModelMetaclass): @@ -1007,6 +1029,8 @@ class ModelMeta(ModelMetaclass): new_class._meta.database = getattr( base_meta, "database", get_redis_connection() ) + if not getattr(new_class._meta, "encoding", None): + new_class._meta.encoding = getattr(base_meta, "encoding") if not getattr(new_class._meta, "primary_key_creator_cls", None): new_class._meta.primary_key_creator_cls = getattr( base_meta, "primary_key_creator_cls", UlidPrimaryKey @@ -1059,7 +1083,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta): def save(self, pipeline: Optional[Pipeline] = None) -> "RedisModel": raise NotImplementedError - @validator("pk", always=True) + @validator("pk", always=True, allow_reuse=True) def validate_pk(cls, v): if not v: v = cls._meta.primary_key_creator_cls().create_pk() @@ -1205,7 +1229,18 @@ class HashModel(RedisModel, abc.ABC): document = cls.db().hgetall(cls.make_primary_key(pk)) if not document: raise NotFoundError - return cls.parse_obj(document) + try: + result = cls.parse_obj(document) + except TypeError as e: + log.warning( + f'Could not parse Redis response. Error was: "{e}". Probably, the ' + "connection is not set to decode responses from bytes. " + "Attempting to decode response using the encoding set on " + f"model class ({cls.__class__}. Encoding: {cls.Meta.encoding}." + ) + document = decode_redis_value(document, cls.Meta.encoding) + result = cls.parse_obj(document) + return result @classmethod @no_type_check @@ -1316,6 +1351,9 @@ class HashModel(RedisModel, abc.ABC): class JsonModel(RedisModel, abc.ABC): def __init_subclass__(cls, **kwargs): + if not has_redis_json(cls.db()): + log.error("Your Redis instance does not have the RedisJson module " + "loaded. JsonModel depends on RedisJson.") # Generate the RediSearch schema once to validate fields. cls.redisearch_schema() diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index e2a3882..2b4201d 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -8,11 +8,15 @@ from unittest import mock import pytest from pydantic import ValidationError +from redis_om.checks import has_redisearch from redis_om.model import Field, HashModel from redis_om.model.migrations.migrator import Migrator from redis_om.model.model import NotFoundError, QueryNotSupportedError, RedisModelError +if not has_redisearch(): + pytestmark = pytest.mark.skip + today = datetime.date.today() diff --git a/tests/test_json_model.py b/tests/test_json_model.py index 779da78..166f3c3 100644 --- a/tests/test_json_model.py +++ b/tests/test_json_model.py @@ -8,11 +8,15 @@ from unittest import mock import pytest from pydantic import ValidationError +from redis_om.checks import has_redis_json from redis_om.model import EmbeddedJsonModel, Field, JsonModel from redis_om.model.migrations.migrator import Migrator from redis_om.model.model import NotFoundError, QueryNotSupportedError, RedisModelError +if not has_redis_json(): + pytestmark = pytest.mark.skip + today = datetime.date.today() @@ -477,7 +481,7 @@ def test_numeric_queries(members, m): actual = m.Member.find(m.Member.age == 34).all() assert actual == [member2] - actual = m.Member.find(m.Member.age > 34).all() + actual = m.Member.find(m.Member.age > 34).sort_by("age").all() assert actual == [member1, member3] actual = m.Member.find(m.Member.age < 35).all() diff --git a/tests/test_oss_redis_features.py b/tests/test_oss_redis_features.py new file mode 100644 index 0000000..72eeaf7 --- /dev/null +++ b/tests/test_oss_redis_features.py @@ -0,0 +1,169 @@ +import abc +import datetime +import decimal +from collections import namedtuple +from typing import Optional +from unittest import mock + +import pytest +from pydantic import ValidationError + +from redis_om.model import Field, HashModel +from redis_om.model.migrations.migrator import Migrator +from redis_om.model.model import NotFoundError, QueryNotSupportedError, RedisModelError + + +today = datetime.date.today() + + +@pytest.fixture +def m(key_prefix): + class BaseHashModel(HashModel, abc.ABC): + class Meta: + global_key_prefix = key_prefix + + class Order(BaseHashModel): + total: decimal.Decimal + currency: str + created_on: datetime.datetime + + class Member(BaseHashModel): + first_name: str + last_name: str + email: str + join_date: datetime.date + age: int + + class Meta: + model_key_prefix = "member" + primary_key_pattern = "" + + Migrator().run() + + return namedtuple("Models", ["BaseHashModel", "Order", "Member"])( + BaseHashModel, Order, Member + ) + + +@pytest.fixture +def members(m): + member1 = m.Member( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + age=38, + join_date=today, + ) + + member2 = m.Member( + first_name="Kim", + last_name="Brookins", + email="k@example.com", + age=34, + join_date=today, + ) + + member3 = m.Member( + first_name="Andrew", + last_name="Smith", + email="as@example.com", + age=100, + join_date=today, + ) + member1.save() + member2.save() + member3.save() + + yield member1, member2, member3 + + +def test_validates_required_fields(m): + # Raises ValidationError: last_name is required + with pytest.raises(ValidationError): + m.Member(first_name="Andrew", zipcode="97086", join_date=today) + + +def test_validates_field(m): + # Raises ValidationError: join_date is not a date + with pytest.raises(ValidationError): + m.Member(first_name="Andrew", last_name="Brookins", join_date="yesterday") + + +# Passes validation +def test_validation_passes(m): + member = m.Member( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date=today, + age=38, + ) + assert member.first_name == "Andrew" + + +def test_saves_model_and_creates_pk(m): + member = m.Member( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date=today, + age=38, + ) + # Save a model instance to Redis + member.save() + + member2 = m.Member.get(member.pk) + assert member2 == member + + +def test_raises_error_with_embedded_models(m): + class Address(m.BaseHashModel): + address_line_1: str + address_line_2: Optional[str] + city: str + country: str + postal_code: str + + with pytest.raises(RedisModelError): + + class InvalidMember(m.BaseHashModel): + address: Address + + +@pytest.mark.skip("Not implemented yet") +def test_saves_many(m): + members = [ + m.Member( + first_name="Andrew", + last_name="Brookins", + email="a@example.com", + join_date=today, + ), + m.Member( + first_name="Kim", + last_name="Brookins", + email="k@example.com", + join_date=today, + ), + ] + m.Member.add(members) + + +@pytest.mark.skip("Not ready yet") +def test_updates_a_model(members, m): + member1, member2, member3 = members + + # Or, with an implicit save: + member1.update(last_name="Smith") + assert m.Member.find(m.Member.pk == member1.pk).first() == member1 + + # Or, affecting multiple model instances with an implicit save: + m.Member.find(m.Member.last_name == "Brookins").update(last_name="Smith") + results = m.Member.find(m.Member.last_name == "Smith") + assert results == members + + +def test_not_found(m): + with pytest.raises(NotFoundError): + # This ID does not exist. + m.Member.get(1000) From 3ee2252539d4154a2737bc62745b05200fa0a7dd Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Wed, 3 Nov 2021 12:53:00 -0700 Subject: [PATCH 39/49] Reformat with linter --- redis_om/model/model.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/redis_om/model/model.py b/redis_om/model/model.py index 0effa91..95de9ac 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -333,9 +333,11 @@ class FindQuery: sort_fields: Optional[List[str]] = None, ): if not has_redisearch(model.db()): - raise RedisModelError("Your Redis instance does not have either the RediSearch module " - "or RedisJSON module installed. Querying requires that your Redis " - "instance has one of these modules installed.") + raise RedisModelError( + "Your Redis instance does not have either the RediSearch module " + "or RedisJSON module installed. Querying requires that your Redis " + "instance has one of these modules installed." + ) self.expressions = expressions self.model = model @@ -959,7 +961,7 @@ class DefaultMeta: index_name: Optional[str] = None abstract: Optional[bool] = False embedded: Optional[bool] = False - encoding: Optional[str] = "utf-8" + encoding: str = "utf-8" class ModelMeta(ModelMetaclass): @@ -1352,8 +1354,10 @@ class HashModel(RedisModel, abc.ABC): class JsonModel(RedisModel, abc.ABC): def __init_subclass__(cls, **kwargs): if not has_redis_json(cls.db()): - log.error("Your Redis instance does not have the RedisJson module " - "loaded. JsonModel depends on RedisJson.") + log.error( + "Your Redis instance does not have the RedisJson module " + "loaded. JsonModel depends on RedisJson." + ) # Generate the RediSearch schema once to validate fields. cls.redisearch_schema() From 984c11679a7872f2ae2d28f3887ea23abb15a9df Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 8 Nov 2021 15:41:10 -0800 Subject: [PATCH 40/49] Drop Protocol class to support Python 3.7 --- pyproject.toml | 2 +- redis_om/model/model.py | 25 +------------------------ tox.ini | 2 +- 3 files changed, 3 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01de65c..deeaa19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.7" +version = "0.0.8" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] diff --git a/redis_om/model/model.py b/redis_om/model/model.py index 95de9ac..017f1bb 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -136,29 +136,6 @@ def decode_redis_value( return obj.decode(encoding) -class ExpressionProtocol(Protocol): - op: Operators - left: ExpressionOrModelField - right: ExpressionOrModelField - - def __invert__(self) -> "Expression": - pass - - def __and__(self, other: ExpressionOrModelField): - pass - - def __or__(self, other: ExpressionOrModelField): - pass - - @property - def name(self) -> str: - raise NotImplementedError - - @property - def tree(self) -> str: - raise NotImplementedError - - @dataclasses.dataclass class NegatedExpression: """A negated Expression object. @@ -814,7 +791,7 @@ class FindQuery: return query.execute()[0] -class PrimaryKeyCreator(Protocol): +class PrimaryKeyCreator(abc.ABC): def create_pk(self, *args, **kwargs) -> str: """Create a new primary key""" diff --git a/tox.ini b/tox.ini index dc841ba..6ec4fbf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = true -envlist = py38, py39, py310 +envlist = py37, py38, py39, py310 [testenv] whitelist_externals = poetry From db7b8d19ad4b890b741d415380117920d5c05df2 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 8 Nov 2021 15:49:32 -0800 Subject: [PATCH 41/49] Lower the Python requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index deeaa19..9b48b36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ include=["docs/*", "images/*"] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.7" redis = "^3.5.3" aioredis = "^2.0.0" pydantic = "^1.8.2" From bc441143debcd119700d54cd3c1bba47f7925682 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 8 Nov 2021 16:28:58 -0800 Subject: [PATCH 42/49] Add all_pks() method to HashModel --- pyproject.toml | 2 +- redis_om/model/model.py | 12 ++++++++++++ tests/test_hash_model.py | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9b48b36..27e3775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.8" +version = "0.0.9" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] diff --git a/redis_om/model/model.py b/redis_om/model/model.py index 017f1bb..eaa42c6 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -1014,6 +1014,7 @@ class ModelMeta(ModelMetaclass): new_class._meta.primary_key_creator_cls = getattr( base_meta, "primary_key_creator_cls", UlidPrimaryKey ) + # TODO: Configurable key separate, defaults to ":" if not getattr(new_class._meta, "index_name", None): new_class._meta.index_name = ( f"{new_class._meta.global_key_prefix}:" @@ -1203,6 +1204,17 @@ class HashModel(RedisModel, abc.ABC): db.hset(self.key(), mapping=document) return self + @classmethod + def all_pks(cls): + key_prefix = cls.make_key(cls._meta.primary_key_pattern.format(pk="")) + # TODO: We assume the key ends with the default separator, ":" -- when + # we make the separator configurable, we need to update this as well. + # ... And probably lots of other places ... + return ( + key.split(":")[-1] + for key in cls.db().scan_iter(f"{key_prefix}*", _type="HASH") + ) + @classmethod def get(cls, pk: Any) -> "HashModel": document = cls.db().hgetall(cls.make_primary_key(pk)) diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index 2b4201d..4e8f2c3 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -411,6 +411,12 @@ def test_sorting(members, m): m.Member.find().sort_by("join_date").all() +def test_all_keys(members, m): + pks = sorted(list(m.Member.all_pks())) + assert len(pks) == 3 + assert pks == sorted([m.pk for m in members]) + + def test_not_found(m): with pytest.raises(NotFoundError): # This ID does not exist. From c7c6dffe7a7e1856a676e81aef01cb4db8ed9d87 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 8 Nov 2021 16:39:27 -0800 Subject: [PATCH 43/49] Tweak CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58804f9..054bbf6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,7 @@ jobs: matrix: os: [ ubuntu-latest ] pyver: [ 3.6, 3.7, 3.8, 3.9, pypy3 ] - redismod: [ "preview" ] # Removed "edge" because it's broken currently + redismod: [ "preview" ] fail-fast: false services: redis: From 6c8cf0b01db38bd4007d52f2c898d90062d641f4 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 8 Nov 2021 16:48:43 -0800 Subject: [PATCH 44/49] Fix all_pks to work if decode_responses=False --- pyproject.toml | 2 +- redis_om/model/model.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27e3775..1545f36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.9" +version = "0.0.11" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] diff --git a/redis_om/model/model.py b/redis_om/model/model.py index eaa42c6..64e139b 100644 --- a/redis_om/model/model.py +++ b/redis_om/model/model.py @@ -1210,8 +1210,11 @@ class HashModel(RedisModel, abc.ABC): # TODO: We assume the key ends with the default separator, ":" -- when # we make the separator configurable, we need to update this as well. # ... And probably lots of other places ... + # + # TODO: Also, we need to decide how we want to handle the lack of + # decode_responses=True... return ( - key.split(":")[-1] + key.split(":")[-1] if isinstance(key, str) else key.decode(cls.Meta.encoding).split(":")[-1] for key in cls.db().scan_iter(f"{key_prefix}*", _type="HASH") ) From 3f45793bae4a9bcf663889994601551f3cf796b5 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Mon, 8 Nov 2021 16:50:09 -0800 Subject: [PATCH 45/49] Set 0.0.10 version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1545f36..02e277d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "redis-om" -version = "0.0.11" +version = "0.0.10" description = "A high-level library containing useful Redis abstractions and tools, like an ORM and leaderboard." authors = ["Andrew Brookins "] maintainers = ["Andrew Brookins "] From e58ab1b8f6e4545b71df72c0b868920e3c643bbd Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 9 Nov 2021 08:05:20 -0800 Subject: [PATCH 46/49] FastAPI integration example --- docs/fastapi_integration.md | 133 ++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 docs/fastapi_integration.md diff --git a/docs/fastapi_integration.md b/docs/fastapi_integration.md new file mode 100644 index 0000000..3994155 --- /dev/null +++ b/docs/fastapi_integration.md @@ -0,0 +1,133 @@ +# FastAPI Integration + +## Introduction + +This section includes a complete example showing how to integrate Redis OM with FastAPI. + +Good news: Redis OM was **specifically designed to integrate with FastAPI**! + +## Concepts + +### Every Redis OM Model is also a Pydantic model + +Every Redis OM model is also a Pydantic model, so you can define a model and then pass the model class into any location that FastAPI expects a Pydantic model. + +This means a couple of things: + +1. A Redis OM model can be used for request body validation +2. Redis OM models show up in the auto-generated API documentation + +### Cache vs. Data + +Redis works well as either a durable data store or a cache, but the optiomal Redis configuration is often different between these two use cases. + +You almost always want to use a Redis instance tuned for caching when you're caching and a separate Redis instance tuned for data durability for storing application state. + +This example shows how to manage these two uses of Redis within the same application. The app uses a FastAPI caching framework and dedicated caching instance of Redis for caching, and a separate Redis instance tuned for durability for Redis OM models. + + +## Example app code + +This is a complete example that you can run as-is: + +```python +import datetime +from typing import Optional + +import aioredis + +from fastapi import FastAPI, HTTPException +from starlette.requests import Request +from starlette.responses import Response + +from fastapi_cache import FastAPICache +from fastapi_cache.backends.redis import RedisBackend +from fastapi_cache.decorator import cache + +from pydantic import EmailStr + +from redis_om.model import HashModel, NotFoundError +from redis_om.connections import get_redis_connection + +# This Redis instance is tuned for durability. +REDIS_DATA_URL = "redis://localhost:6380" + +# This Redis instance is tuned for cache performance. +REDIS_CACHE_URL = "redis://localhost:6381" + + +class Customer(HashModel): + first_name: str + last_name: str + email: EmailStr + join_date: datetime.date + age: int + bio: Optional[str] + + +app = FastAPI() + + +@app.post("/customer") +async def save_customer(customer: Customer): + # We can save the model to Redis by calling `save()`: + return customer.save() + + +@app.get("/customers") +async def list_customers(request: Request, response: Response): + # To retrieve this customer with its primary key, we use `Customer.get()`: + return {"customers": Customer.all_pks()} + + +@app.get("/customer/{pk}") +@cache(expire=10) +async def get_customer(pk: str, request: Request, response: Response): + # To retrieve this customer with its primary key, we use `Customer.get()`: + try: + return Customer.get(pk) + except NotFoundError: + raise HTTPException(status_code=404, detail="Customer not found") + + +@app.on_event("startup") +async def startup(): + r = aioredis.from_url(REDIS_CACHE_URL, encoding="utf8", decode_responses=True) + FastAPICache.init(RedisBackend(r), prefix="fastapi-cache") + + # You can set the Redis OM URL using the REDIS_OM_URL environment + # variable, or by manually creating the connection using your model's + # Meta object. + Customer.Meta.database = get_redis_connection(url=REDIS_DATA_URL, decode_responses=True) +``` + +## Testing the app + +You should install the app's dependencies first. This app uses Poetry, so you'll want to make sure you have that installed first: + + $ pip install poetry + +Then install the dependencies: + + $ poetry install + +Next, start the server: + + $ poetry run uvicorn --reload main:test + +Then, in another shell, create a customer: + + $ curl -X POST -H 'Content-Length: 0' "http://localhost:8000/customer" + $ curl -X POST "http://localhost:8000/customer" -H 'Content-Type: application/json' -d '{"first_name":"Andrew","last_name":"Brookins","email":"a@example.com","age":"38","join_date":"2020 +-01-02"}' + {"pk":"01FM2G8EP38AVMH7PMTAJ123TA","first_name":"Andrew","last_name":"Brookins","email":"a@example.com","join_date":"2020-01-02","age":38,"bio":""} + +Get a copy of the value for "pk" and make another request to get that customer: + + $ curl "http://localhost:8000/customer/01FM2G8EP38AVMH7PMTAJ123TA" + {"pk":"01FM2G8EP38AVMH7PMTAJ123TA","first_name":"Andrew","last_name":"Brookins","email":"a@example.com","join_date":"2020-01-02","age":38,"bio":""} + +You can also get a list of all customer PKs: + + $ curl "http://localhost:8000/customers" + {"customers":["01FM2G8EP38AVMH7PMTAJ123TA"]} \ No newline at end of file From 12bd0690265da9581b4636d65ad00543474aa080 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 9 Nov 2021 08:05:46 -0800 Subject: [PATCH 47/49] Fix typo in FastAPI integration doc --- docs/fastapi_integration.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/fastapi_integration.md b/docs/fastapi_integration.md index 3994155..4808ead 100644 --- a/docs/fastapi_integration.md +++ b/docs/fastapi_integration.md @@ -117,7 +117,6 @@ Next, start the server: Then, in another shell, create a customer: - $ curl -X POST -H 'Content-Length: 0' "http://localhost:8000/customer" $ curl -X POST "http://localhost:8000/customer" -H 'Content-Type: application/json' -d '{"first_name":"Andrew","last_name":"Brookins","email":"a@example.com","age":"38","join_date":"2020 -01-02"}' {"pk":"01FM2G8EP38AVMH7PMTAJ123TA","first_name":"Andrew","last_name":"Brookins","email":"a@example.com","join_date":"2020-01-02","age":38,"bio":""} From a1fef8cfe689ca156f586969b810c1bc896509c3 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 9 Nov 2021 08:06:32 -0800 Subject: [PATCH 48/49] Word choice --- docs/fastapi_integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fastapi_integration.md b/docs/fastapi_integration.md index 4808ead..f703a5c 100644 --- a/docs/fastapi_integration.md +++ b/docs/fastapi_integration.md @@ -10,7 +10,7 @@ Good news: Redis OM was **specifically designed to integrate with FastAPI**! ### Every Redis OM Model is also a Pydantic model -Every Redis OM model is also a Pydantic model, so you can define a model and then pass the model class into any location that FastAPI expects a Pydantic model. +Every Redis OM model is also a Pydantic model, so you can define a model and then use the model class anywhere that FastAPI expects a Pydantic model. This means a couple of things: From 1e369e33c85b8e16330b5c7b9df7a732f6bdfd3c Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Tue, 9 Nov 2021 08:06:51 -0800 Subject: [PATCH 49/49] Word choice --- docs/fastapi_integration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fastapi_integration.md b/docs/fastapi_integration.md index f703a5c..62942e6 100644 --- a/docs/fastapi_integration.md +++ b/docs/fastapi_integration.md @@ -19,7 +19,7 @@ This means a couple of things: ### Cache vs. Data -Redis works well as either a durable data store or a cache, but the optiomal Redis configuration is often different between these two use cases. +Redis works well as either a durable data store or a cache, but the optimal Redis configuration is often different between these two use cases. You almost always want to use a Redis instance tuned for caching when you're caching and a separate Redis instance tuned for data durability for storing application state.