Add Makefile, black, reformat with black
This commit is contained in:
parent
cfc50b82bb
commit
d2fa4c586f
16 changed files with 978 additions and 366 deletions
0
.install.stamp
Normal file
0
.install.stamp
Normal file
55
Makefile
Normal file
55
Makefile
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
NAME := redis_developer
|
||||||
|
INSTALL_STAMP := .install.stamp
|
||||||
|
POETRY := $(shell command -v poetry 2> /dev/null)
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "Please use 'make <target>' where <target> is one of"
|
||||||
|
@echo ""
|
||||||
|
@echo " install install packages and prepare environment"
|
||||||
|
@echo " clean remove all temporary files"
|
||||||
|
@echo " lint run the code linters"
|
||||||
|
@echo " format reformat code"
|
||||||
|
@echo " test run all the tests"
|
||||||
|
@echo " shell open a Poetry shell"
|
||||||
|
@echo ""
|
||||||
|
@echo "Check the Makefile to know exactly what each target is doing."
|
||||||
|
|
||||||
|
install: $(INSTALL_STAMP)
|
||||||
|
$(INSTALL_STAMP): pyproject.toml poetry.lock
|
||||||
|
@if [ -z $(POETRY) ]; then echo "Poetry could not be found. See https://python-poetry.org/docs/"; exit 2; fi
|
||||||
|
$(POETRY) install
|
||||||
|
touch $(INSTALL_STAMP)
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
find . -type d -name "__pycache__" | xargs rm -rf {};
|
||||||
|
rm -rf $(INSTALL_STAMP) .coverage .mypy_cache
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: $(INSTALL_STAMP)
|
||||||
|
$(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
|
||||||
|
|
||||||
|
.PHONY: format
|
||||||
|
format: $(INSTALL_STAMP)
|
||||||
|
$(POETRY) run isort --profile=black --lines-after-imports=2 ./tests/ $(NAME)
|
||||||
|
$(POETRY) run black ./tests/ $(NAME)
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: $(INSTALL_STAMP)
|
||||||
|
#$(POETRY) run pytest ./tests/ --cov-report term-missing --cov-fail-under 100 --cov $(NAME)
|
||||||
|
$(POETRY) run pytest ./tests/
|
||||||
|
|
||||||
|
.PHONY: shell
|
||||||
|
shell: $(INSTALL_STAMP)
|
||||||
|
$(POETRY) shell
|
||||||
|
|
||||||
|
.PHONY: redis
|
||||||
|
redis:
|
||||||
|
docker-compose up -d
|
11
docker-compose.yml
Normal file
11
docker-compose.yml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
redis:
|
||||||
|
image: "redislabs/redismod:edge"
|
||||||
|
entrypoint: ["redis-server", "--appendonly", "yes", "--loadmodule", "/usr/lib/redis/modules/rejson.so"]
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
388
poetry.lock
generated
388
poetry.lock
generated
|
@ -23,16 +23,16 @@ python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "astroid"
|
name = "astroid"
|
||||||
version = "2.8.0"
|
version = "2.8.3"
|
||||||
description = "An abstract syntax tree for Python with inference support."
|
description = "An abstract syntax tree for Python with inference support."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "~=3.6"
|
python-versions = "~=3.6"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
lazy-object-proxy = ">=1.4.0"
|
lazy-object-proxy = ">=1.4.0"
|
||||||
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
|
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
|
||||||
wrapt = ">=1.11,<1.13"
|
wrapt = ">=1.11,<1.14"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-timeout"
|
name = "async-timeout"
|
||||||
|
@ -72,9 +72,51 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bandit"
|
||||||
|
version = "1.7.0"
|
||||||
|
description = "Security oriented static analyser for python code."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
|
||||||
|
GitPython = ">=1.0.1"
|
||||||
|
PyYAML = ">=5.3.1"
|
||||||
|
six = ">=1.10.0"
|
||||||
|
stevedore = ">=1.20.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "21.9b0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.2"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=7.1.2"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
pathspec = ">=0.9.0,<1"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
regex = ">=2020.1.8"
|
||||||
|
tomli = ">=0.2.6,<2.0.0"
|
||||||
|
typing-extensions = [
|
||||||
|
{version = ">=3.10.0.0", markers = "python_version < \"3.10\""},
|
||||||
|
{version = "!=3.10.0.1", markers = "python_version >= \"3.10\""},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.6.0)", "aiohttp-cors (>=0.4.0)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
python2 = ["typed-ast (>=1.4.2)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.0.1"
|
version = "8.0.3"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -99,6 +141,42 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "4.0.1"
|
||||||
|
description = "the modular source code checker: pep8 pyflakes and co"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mccabe = ">=0.6.0,<0.7.0"
|
||||||
|
pycodestyle = ">=2.8.0,<2.9.0"
|
||||||
|
pyflakes = ">=2.4.0,<2.5.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gitdb"
|
||||||
|
version = "4.0.7"
|
||||||
|
description = "Git Object Database"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.4"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
smmap = ">=3.0.1,<5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gitpython"
|
||||||
|
version = "3.1.24"
|
||||||
|
description = "GitPython is a python library used to interact with Git repositories"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
gitdb = ">=4.0.1,<5"
|
||||||
|
typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.10\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -122,7 +200,7 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython"
|
name = "ipython"
|
||||||
version = "7.27.0"
|
version = "7.28.0"
|
||||||
description = "IPython: Productive Interactive Computing"
|
description = "IPython: Productive Interactive Computing"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -156,7 +234,7 @@ test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipyk
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.9.3"
|
version = "5.9.3"
|
||||||
description = "A Python utility / library to sort Python imports."
|
description = "A Python utility / library to sort Python imports."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.1,<4.0"
|
python-versions = ">=3.6.1,<4.0"
|
||||||
|
|
||||||
|
@ -185,7 +263,7 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
|
||||||
name = "lazy-object-proxy"
|
name = "lazy-object-proxy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
description = "A fast and thorough lazy object proxy."
|
description = "A fast and thorough lazy object proxy."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
|
||||||
|
@ -204,7 +282,7 @@ traitlets = "*"
|
||||||
name = "mccabe"
|
name = "mccabe"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
description = "McCabe checker, plugin for flake8"
|
description = "McCabe checker, plugin for flake8"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
@ -212,7 +290,7 @@ python-versions = "*"
|
||||||
name = "mypy"
|
name = "mypy"
|
||||||
version = "0.910"
|
version = "0.910"
|
||||||
description = "Optional static typing for Python"
|
description = "Optional static typing for Python"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
@ -229,7 +307,7 @@ python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
@ -256,6 +334,22 @@ python-versions = ">=3.6"
|
||||||
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
||||||
testing = ["docopt", "pytest (<6.0.0)"]
|
testing = ["docopt", "pytest (<6.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.9.0"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pbr"
|
||||||
|
version = "5.6.0"
|
||||||
|
description = "Python Build Reasonableness"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pexpect"
|
name = "pexpect"
|
||||||
version = "4.8.0"
|
version = "4.8.0"
|
||||||
|
@ -279,7 +373,7 @@ python-versions = "*"
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
@ -334,6 +428,14 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycodestyle"
|
||||||
|
version = "2.8.0"
|
||||||
|
description = "Python style guide checker"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
|
@ -349,6 +451,14 @@ typing-extensions = ">=3.7.4.3"
|
||||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||||
email = ["email-validator (>=1.0.3)"]
|
email = ["email-validator (>=1.0.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "2.4.0"
|
||||||
|
description = "passive checker of Python programs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
|
@ -361,7 +471,7 @@ python-versions = ">=3.5"
|
||||||
name = "pylint"
|
name = "pylint"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
description = "python code static checker"
|
description = "python code static checker"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "~=3.6"
|
python-versions = "~=3.6"
|
||||||
|
|
||||||
|
@ -411,6 +521,14 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0"
|
||||||
|
description = "YAML parser and emitter for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "3.5.3"
|
version = "3.5.3"
|
||||||
|
@ -422,6 +540,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
hiredis = ["hiredis (>=0.1.3)"]
|
hiredis = ["hiredis (>=0.1.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "2021.10.8"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -430,14 +556,41 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smmap"
|
||||||
|
version = "4.0.0"
|
||||||
|
description = "A pure Python implementation of a sliding window memory map manager"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stevedore"
|
||||||
|
version = "3.5.0"
|
||||||
|
description = "Manage dynamic plugins for Python applications"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pbr = ">=2.0.0,<2.1.0 || >2.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "1.2.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "traitlets"
|
name = "traitlets"
|
||||||
version = "5.1.0"
|
version = "5.1.0"
|
||||||
|
@ -451,7 +604,7 @@ test = ["pytest"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-redis"
|
name = "types-redis"
|
||||||
version = "3.5.9"
|
version = "3.5.15"
|
||||||
description = "Typing stubs for redis"
|
description = "Typing stubs for redis"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -459,7 +612,7 @@ python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-six"
|
name = "types-six"
|
||||||
version = "1.16.1"
|
version = "1.16.2"
|
||||||
description = "Typing stubs for six"
|
description = "Typing stubs for six"
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -483,16 +636,16 @@ python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wrapt"
|
name = "wrapt"
|
||||||
version = "1.12.1"
|
version = "1.13.2"
|
||||||
description = "Module for decorators, wrappers and monkey patching."
|
description = "Module for decorators, wrappers and monkey patching."
|
||||||
category = "main"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "e643c8bcc3f54c414e388a8c62256c3c0fe9e2fb0374c3f3b4140e2b0684b654"
|
content-hash = "f1ccd73314f307ce41497d093ddce99cfb96ebf1814e854a94e37d5156647967"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aioredis = [
|
aioredis = [
|
||||||
|
@ -504,8 +657,8 @@ appnope = [
|
||||||
{file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
|
{file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
|
||||||
]
|
]
|
||||||
astroid = [
|
astroid = [
|
||||||
{file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"},
|
{file = "astroid-2.8.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"},
|
||||||
{file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"},
|
{file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"},
|
||||||
]
|
]
|
||||||
async-timeout = [
|
async-timeout = [
|
||||||
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
|
||||||
|
@ -523,9 +676,17 @@ backcall = [
|
||||||
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
|
{file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"},
|
||||||
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
|
{file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"},
|
||||||
]
|
]
|
||||||
|
bandit = [
|
||||||
|
{file = "bandit-1.7.0-py3-none-any.whl", hash = "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07"},
|
||||||
|
{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"},
|
||||||
|
]
|
||||||
click = [
|
click = [
|
||||||
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
|
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
|
||||||
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
|
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
|
||||||
]
|
]
|
||||||
colorama = [
|
colorama = [
|
||||||
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
|
||||||
|
@ -535,6 +696,18 @@ decorator = [
|
||||||
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
|
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
|
||||||
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
||||||
]
|
]
|
||||||
|
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"},
|
||||||
|
]
|
||||||
|
gitpython = [
|
||||||
|
{file = "GitPython-3.1.24-py3-none-any.whl", hash = "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647"},
|
||||||
|
{file = "GitPython-3.1.24.tar.gz", hash = "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"},
|
||||||
|
]
|
||||||
iniconfig = [
|
iniconfig = [
|
||||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
|
@ -543,8 +716,8 @@ ipdb = [
|
||||||
{file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
|
{file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"},
|
||||||
]
|
]
|
||||||
ipython = [
|
ipython = [
|
||||||
{file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"},
|
{file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"},
|
||||||
{file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"},
|
{file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"},
|
||||||
]
|
]
|
||||||
isort = [
|
isort = [
|
||||||
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
|
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
|
||||||
|
@ -623,6 +796,14 @@ parso = [
|
||||||
{file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
|
{file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
|
||||||
{file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"},
|
{file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"},
|
||||||
]
|
]
|
||||||
|
pathspec = [
|
||||||
|
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||||
|
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||||
|
]
|
||||||
|
pbr = [
|
||||||
|
{file = "pbr-5.6.0-py2.py3-none-any.whl", hash = "sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"},
|
||||||
|
{file = "pbr-5.6.0.tar.gz", hash = "sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd"},
|
||||||
|
]
|
||||||
pexpect = [
|
pexpect = [
|
||||||
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
|
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
|
||||||
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
|
{file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
|
||||||
|
@ -654,6 +835,10 @@ py = [
|
||||||
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
|
{file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"},
|
||||||
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
|
{file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"},
|
||||||
]
|
]
|
||||||
|
pycodestyle = [
|
||||||
|
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
|
||||||
|
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
|
||||||
|
]
|
||||||
pydantic = [
|
pydantic = [
|
||||||
{file = "pydantic-1.8.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739"},
|
{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"},
|
{file = "pydantic-1.8.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4"},
|
||||||
|
@ -678,6 +863,10 @@ pydantic = [
|
||||||
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
|
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
|
||||||
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
|
{file = "pydantic-1.8.2.tar.gz", hash = "sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b"},
|
||||||
]
|
]
|
||||||
|
pyflakes = [
|
||||||
|
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
|
||||||
|
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
|
||||||
|
]
|
||||||
pygments = [
|
pygments = [
|
||||||
{file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
|
{file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
|
||||||
{file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
|
{file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
|
||||||
|
@ -698,29 +887,125 @@ python-ulid = [
|
||||||
{file = "python-ulid-1.0.3.tar.gz", hash = "sha256:5dd8b969312a40e2212cec9c1ad63f25d4b6eafd92ee3195883e0287b6e9d19e"},
|
{file = "python-ulid-1.0.3.tar.gz", hash = "sha256:5dd8b969312a40e2212cec9c1ad63f25d4b6eafd92ee3195883e0287b6e9d19e"},
|
||||||
{file = "python_ulid-1.0.3-py3-none-any.whl", hash = "sha256:8704dc20f547f531fe3a41d4369842d737a0f275403b909d0872e7ea0fe8d6f2"},
|
{file = "python_ulid-1.0.3-py3-none-any.whl", hash = "sha256:8704dc20f547f531fe3a41d4369842d737a0f275403b909d0872e7ea0fe8d6f2"},
|
||||||
]
|
]
|
||||||
|
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"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"},
|
||||||
|
{file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"},
|
||||||
|
{file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"},
|
||||||
|
{file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"},
|
||||||
|
{file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"},
|
||||||
|
{file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"},
|
||||||
|
{file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"},
|
||||||
|
]
|
||||||
redis = [
|
redis = [
|
||||||
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
||||||
{file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"},
|
{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"},
|
||||||
|
]
|
||||||
six = [
|
six = [
|
||||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{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"},
|
||||||
|
]
|
||||||
|
stevedore = [
|
||||||
|
{file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"},
|
||||||
|
{file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"},
|
||||||
|
]
|
||||||
toml = [
|
toml = [
|
||||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
{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"},
|
||||||
|
]
|
||||||
traitlets = [
|
traitlets = [
|
||||||
{file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"},
|
{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.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"},
|
||||||
]
|
]
|
||||||
types-redis = [
|
types-redis = [
|
||||||
{file = "types-redis-3.5.9.tar.gz", hash = "sha256:f142c48f4080757ca2a9441ec40213bda3b1535eebebfc4f3519e5aa46498076"},
|
{file = "types-redis-3.5.15.tar.gz", hash = "sha256:e52be0077ca1189d8cce813a20c2a70e9e577f34ab898371c6cbed696a88bdee"},
|
||||||
{file = "types_redis-3.5.9-py3-none-any.whl", hash = "sha256:5f5648ffc025708858097173cf695164c20f2b5e3f57177de14e352cae8cc335"},
|
{file = "types_redis-3.5.15-py3-none-any.whl", hash = "sha256:e617c08bff88449b52f6dbdaa9bb81a806f27c89fd30bbf98fe9683ed5d1046a"},
|
||||||
]
|
]
|
||||||
types-six = [
|
types-six = [
|
||||||
{file = "types-six-1.16.1.tar.gz", hash = "sha256:a9e6769cb0808f920958ac95f75c5191f49e21e041eac127fa62e286e1005616"},
|
{file = "types-six-1.16.2.tar.gz", hash = "sha256:b96bd911f87d15258c38e10ee3f0921c32887a5d22e41c39d15707b4d0e4d0f1"},
|
||||||
{file = "types_six-1.16.1-py2.py3-none-any.whl", hash = "sha256:b14f5abe26c0997bd41a1a32d6816af25932f7bfbc54246dfdc8f6f6404fd1d4"},
|
{file = "types_six-1.16.2-py2.py3-none-any.whl", hash = "sha256:606dd8c7edff3100fae8277c270e65285e5cdb6a7819c0b1ea6a8973690e68da"},
|
||||||
]
|
]
|
||||||
typing-extensions = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
{file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"},
|
||||||
|
@ -732,5 +1017,48 @@ wcwidth = [
|
||||||
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
|
||||||
]
|
]
|
||||||
wrapt = [
|
wrapt = [
|
||||||
{file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
|
{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"},
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,15 +13,20 @@ pydantic = "^1.8.2"
|
||||||
click = "^8.0.1"
|
click = "^8.0.1"
|
||||||
six = "^1.16.0"
|
six = "^1.16.0"
|
||||||
pptree = "^3.1"
|
pptree = "^3.1"
|
||||||
mypy = "^0.910"
|
|
||||||
types-redis = "^3.5.9"
|
types-redis = "^3.5.9"
|
||||||
types-six = "^1.16.1"
|
types-six = "^1.16.1"
|
||||||
python-ulid = "^1.0.3"
|
python-ulid = "^1.0.3"
|
||||||
pylint = "^2.11.1"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
mypy = "^0.910"
|
||||||
pytest = "^6.2.4"
|
pytest = "^6.2.4"
|
||||||
ipdb = "^0.13.9"
|
ipdb = "^0.13.9"
|
||||||
|
pylint = "^2.11.1"
|
||||||
|
black = "^21.9b0"
|
||||||
|
isort = "^5.9.3"
|
||||||
|
flake8 = "^4.0.1"
|
||||||
|
bandit = "^1.7.0"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
migrate = "redis_developer.orm.cli.migrate:migrate"
|
migrate = "redis_developer.orm.cli.migrate:migrate"
|
||||||
|
|
|
@ -1,7 +1 @@
|
||||||
from .model import (
|
from .model import EmbeddedJsonModel, Field, HashModel, JsonModel, RedisModel
|
||||||
RedisModel,
|
|
||||||
HashModel,
|
|
||||||
JsonModel,
|
|
||||||
EmbeddedJsonModel,
|
|
||||||
Field
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import click
|
import click
|
||||||
|
|
||||||
from redis_developer.model.migrations.migrator import Migrator
|
from redis_developer.model.migrations.migrator import Migrator
|
||||||
|
|
||||||
|
|
||||||
|
@ -12,5 +13,5 @@ def migrate(module):
|
||||||
for migration in migrator.migrations:
|
for migration in migrator.migrations:
|
||||||
print(migration)
|
print(migration)
|
||||||
|
|
||||||
if input(f"Run migrations? (y/n) ") == "y":
|
if input("Run migrations? (y/n) ") == "y":
|
||||||
migrator.run()
|
migrator.run()
|
||||||
|
|
|
@ -34,6 +34,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from pydantic.json import ENCODERS_BY_TYPE
|
from pydantic.json import ENCODERS_BY_TYPE
|
||||||
|
|
||||||
|
|
||||||
SetIntStr = Set[Union[int, str]]
|
SetIntStr = Set[Union[int, str]]
|
||||||
DictIntStrAny = Dict[Union[int, str], Any]
|
DictIntStrAny = Dict[Union[int, str], Any]
|
||||||
|
|
||||||
|
|
|
@ -9,20 +9,33 @@ from redis import ResponseError
|
||||||
from redis_developer.connections import get_redis_connection
|
from redis_developer.connections import get_redis_connection
|
||||||
from redis_developer.model.model import model_registry
|
from redis_developer.model.model import model_registry
|
||||||
|
|
||||||
|
|
||||||
redis = get_redis_connection()
|
redis = get_redis_connection()
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
import importlib
|
import importlib # noqa: E402
|
||||||
import pkgutil
|
import pkgutil # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class MigrationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def import_submodules(root_module_name: str):
|
def import_submodules(root_module_name: str):
|
||||||
"""Import all submodules of a module, recursively."""
|
"""Import all submodules of a module, recursively."""
|
||||||
# TODO: Call this without specifying a module name, to import everything?
|
# TODO: Call this without specifying a module name, to import everything?
|
||||||
root_module = importlib.import_module(root_module_name)
|
root_module = importlib.import_module(root_module_name)
|
||||||
|
|
||||||
|
if not hasattr(root_module, "__path__"):
|
||||||
|
raise MigrationError(
|
||||||
|
"The root module must be a Python package. "
|
||||||
|
f"You specified: {root_module_name}"
|
||||||
|
)
|
||||||
|
|
||||||
for loader, module_name, is_pkg in pkgutil.walk_packages(
|
for loader, module_name, is_pkg in pkgutil.walk_packages(
|
||||||
root_module.__path__, root_module.__name__ + '.'):
|
root_module.__path__, root_module.__name__ + "." # type: ignore
|
||||||
|
):
|
||||||
importlib.import_module(module_name)
|
importlib.import_module(module_name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,14 +90,20 @@ class Migrator:
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
log.info("Skipping migrations for %s", name)
|
log.info("Skipping migrations for %s", name)
|
||||||
continue
|
continue
|
||||||
current_hash = hashlib.sha1(schema.encode("utf-8")).hexdigest()
|
current_hash = hashlib.sha1(schema.encode("utf-8")).hexdigest() # nosec
|
||||||
|
|
||||||
try:
|
try:
|
||||||
redis.execute_command("ft.info", cls.Meta.index_name)
|
redis.execute_command("ft.info", cls.Meta.index_name)
|
||||||
except ResponseError:
|
except ResponseError:
|
||||||
self.migrations.append(
|
self.migrations.append(
|
||||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
IndexMigration(
|
||||||
MigrationAction.CREATE))
|
name,
|
||||||
|
cls.Meta.index_name,
|
||||||
|
schema,
|
||||||
|
current_hash,
|
||||||
|
MigrationAction.CREATE,
|
||||||
|
)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stored_hash = redis.get(hash_key)
|
stored_hash = redis.get(hash_key)
|
||||||
|
@ -93,11 +112,25 @@ class Migrator:
|
||||||
if schema_out_of_date:
|
if schema_out_of_date:
|
||||||
# TODO: Switch out schema with an alias to avoid downtime -- separate migration?
|
# TODO: Switch out schema with an alias to avoid downtime -- separate migration?
|
||||||
self.migrations.append(
|
self.migrations.append(
|
||||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
IndexMigration(
|
||||||
MigrationAction.DROP, stored_hash))
|
name,
|
||||||
|
cls.Meta.index_name,
|
||||||
|
schema,
|
||||||
|
current_hash,
|
||||||
|
MigrationAction.DROP,
|
||||||
|
stored_hash,
|
||||||
|
)
|
||||||
|
)
|
||||||
self.migrations.append(
|
self.migrations.append(
|
||||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
IndexMigration(
|
||||||
MigrationAction.CREATE, stored_hash))
|
name,
|
||||||
|
cls.Meta.index_name,
|
||||||
|
schema,
|
||||||
|
current_hash,
|
||||||
|
MigrationAction.CREATE,
|
||||||
|
stored_hash,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
# TODO: Migration history
|
# TODO: Migration history
|
||||||
|
|
|
@ -4,7 +4,7 @@ import decimal
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from copy import deepcopy, copy
|
from copy import copy, deepcopy
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import (
|
from typing import (
|
||||||
|
@ -12,18 +12,19 @@ from typing import (
|
||||||
Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
Dict,
|
Dict,
|
||||||
|
List,
|
||||||
Mapping,
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
Tuple,
|
Tuple,
|
||||||
|
Type,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
Union,
|
Union,
|
||||||
Sequence,
|
get_args,
|
||||||
no_type_check,
|
|
||||||
Protocol,
|
|
||||||
List,
|
|
||||||
get_origin,
|
get_origin,
|
||||||
get_args, Type
|
no_type_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
import redis
|
import redis
|
||||||
|
@ -40,6 +41,7 @@ from .encoders import jsonable_encoder
|
||||||
from .render_tree import render_tree
|
from .render_tree import render_tree
|
||||||
from .token_escaper import TokenEscaper
|
from .token_escaper import TokenEscaper
|
||||||
|
|
||||||
|
|
||||||
model_registry = {}
|
model_registry = {}
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T")
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -92,7 +94,7 @@ class Operators(Enum):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
|
|
||||||
ExpressionOrModelField = Union['Expression', 'NegatedExpression', ModelField]
|
ExpressionOrModelField = Union["Expression", "NegatedExpression", ModelField]
|
||||||
|
|
||||||
|
|
||||||
def embedded(cls):
|
def embedded(cls):
|
||||||
|
@ -100,20 +102,22 @@ def embedded(cls):
|
||||||
Mark a model as embedded to avoid creating multiple indexes if the model is
|
Mark a model as embedded to avoid creating multiple indexes if the model is
|
||||||
only ever used embedded within other models.
|
only ever used embedded within other models.
|
||||||
"""
|
"""
|
||||||
setattr(cls.Meta, 'embedded', True)
|
setattr(cls.Meta, "embedded", True)
|
||||||
|
|
||||||
|
|
||||||
def is_supported_container_type(typ: type) -> bool:
|
def is_supported_container_type(typ: Optional[type]) -> bool:
|
||||||
if typ == list or typ == tuple:
|
if typ == list or typ == tuple:
|
||||||
return True
|
return True
|
||||||
unwrapped = get_origin(typ)
|
unwrapped = get_origin(typ)
|
||||||
return unwrapped == list or unwrapped == tuple
|
return unwrapped == list or unwrapped == tuple
|
||||||
|
|
||||||
|
|
||||||
def validate_model_fields(model: Type['RedisModel'], field_values: Dict[str, Any]):
|
def validate_model_fields(model: Type["RedisModel"], field_values: Dict[str, Any]):
|
||||||
for field_name in field_values.keys():
|
for field_name in field_values.keys():
|
||||||
if field_name not in model.__fields__:
|
if field_name not in model.__fields__:
|
||||||
raise QuerySyntaxError(f"The field {field_name} does not exist on the model {self.model}")
|
raise QuerySyntaxError(
|
||||||
|
f"The field {field_name} does not exist on the model {model.__name__}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExpressionProtocol(Protocol):
|
class ExpressionProtocol(Protocol):
|
||||||
|
@ -121,7 +125,7 @@ class ExpressionProtocol(Protocol):
|
||||||
left: ExpressionOrModelField
|
left: ExpressionOrModelField
|
||||||
right: ExpressionOrModelField
|
right: ExpressionOrModelField
|
||||||
|
|
||||||
def __invert__(self) -> 'Expression':
|
def __invert__(self) -> "Expression":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __and__(self, other: ExpressionOrModelField):
|
def __and__(self, other: ExpressionOrModelField):
|
||||||
|
@ -148,16 +152,21 @@ class NegatedExpression:
|
||||||
responsible for querying) to negate the logic in the wrapped Expression. A
|
responsible for querying) to negate the logic in the wrapped Expression. A
|
||||||
better design is probably possible, maybe at least an ExpressionProtocol?
|
better design is probably possible, maybe at least an ExpressionProtocol?
|
||||||
"""
|
"""
|
||||||
expression: 'Expression'
|
|
||||||
|
expression: "Expression"
|
||||||
|
|
||||||
def __invert__(self):
|
def __invert__(self):
|
||||||
return self.expression
|
return self.expression
|
||||||
|
|
||||||
def __and__(self, other):
|
def __and__(self, other):
|
||||||
return Expression(left=self, op=Operators.AND, right=other, parents=self.expression.parents)
|
return Expression(
|
||||||
|
left=self, op=Operators.AND, right=other, parents=self.expression.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __or__(self, other):
|
def __or__(self, other):
|
||||||
return Expression(left=self, op=Operators.OR, right=other, parents=self.expression.parents)
|
return Expression(
|
||||||
|
left=self, op=Operators.OR, right=other, parents=self.expression.parents
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def left(self):
|
def left(self):
|
||||||
|
@ -188,13 +197,15 @@ class Expression:
|
||||||
op: Operators
|
op: Operators
|
||||||
left: Optional[ExpressionOrModelField]
|
left: Optional[ExpressionOrModelField]
|
||||||
right: Optional[ExpressionOrModelField]
|
right: Optional[ExpressionOrModelField]
|
||||||
parents: List[Tuple[str, 'RedisModel']]
|
parents: List[Tuple[str, "RedisModel"]]
|
||||||
|
|
||||||
def __invert__(self):
|
def __invert__(self):
|
||||||
return NegatedExpression(self)
|
return NegatedExpression(self)
|
||||||
|
|
||||||
def __and__(self, other: ExpressionOrModelField):
|
def __and__(self, other: ExpressionOrModelField):
|
||||||
return Expression(left=self, op=Operators.AND, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self, op=Operators.AND, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __or__(self, other: ExpressionOrModelField):
|
def __or__(self, other: ExpressionOrModelField):
|
||||||
return Expression(left=self, op=Operators.OR, right=other, parents=self.parents)
|
return Expression(left=self, op=Operators.OR, right=other, parents=self.parents)
|
||||||
|
@ -212,41 +223,59 @@ ExpressionOrNegated = Union[Expression, NegatedExpression]
|
||||||
|
|
||||||
|
|
||||||
class ExpressionProxy:
|
class ExpressionProxy:
|
||||||
def __init__(self, field: ModelField, parents: List[Tuple[str, 'RedisModel']]):
|
def __init__(self, field: ModelField, parents: List[Tuple[str, "RedisModel"]]):
|
||||||
self.field = field
|
self.field = field
|
||||||
self.parents = parents
|
self.parents = parents
|
||||||
|
|
||||||
def __eq__(self, other: Any) -> Expression: # type: ignore[override]
|
def __eq__(self, other: Any) -> Expression: # type: ignore[override]
|
||||||
return Expression(left=self.field, op=Operators.EQ, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.EQ, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __ne__(self, other: Any) -> Expression: # type: ignore[override]
|
def __ne__(self, other: Any) -> Expression: # type: ignore[override]
|
||||||
return Expression(left=self.field, op=Operators.NE, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.NE, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __lt__(self, other: Any) -> Expression:
|
def __lt__(self, other: Any) -> Expression:
|
||||||
return Expression(left=self.field, op=Operators.LT, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.LT, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __le__(self, other: Any) -> Expression:
|
def __le__(self, other: Any) -> Expression:
|
||||||
return Expression(left=self.field, op=Operators.LE, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.LE, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __gt__(self, other: Any) -> Expression:
|
def __gt__(self, other: Any) -> Expression:
|
||||||
return Expression(left=self.field, op=Operators.GT, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.GT, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __ge__(self, other: Any) -> Expression:
|
def __ge__(self, other: Any) -> Expression:
|
||||||
return Expression(left=self.field, op=Operators.GE, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.GE, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __mod__(self, other: Any) -> Expression:
|
def __mod__(self, other: Any) -> Expression:
|
||||||
return Expression(left=self.field, op=Operators.LIKE, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.LIKE, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __lshift__(self, other: Any) -> Expression:
|
def __lshift__(self, other: Any) -> Expression:
|
||||||
return Expression(left=self.field, op=Operators.IN, right=other, parents=self.parents)
|
return Expression(
|
||||||
|
left=self.field, op=Operators.IN, right=other, parents=self.parents
|
||||||
|
)
|
||||||
|
|
||||||
def __getattr__(self, item):
|
def __getattr__(self, item):
|
||||||
if is_supported_container_type(self.field.outer_type_):
|
if is_supported_container_type(self.field.outer_type_):
|
||||||
embedded_cls = get_args(self.field.outer_type_)
|
embedded_cls = get_args(self.field.outer_type_)
|
||||||
if not embedded_cls:
|
if not embedded_cls:
|
||||||
raise QuerySyntaxError("In order to query on a list field, you must define "
|
raise QuerySyntaxError(
|
||||||
|
"In order to query on a list field, you must define "
|
||||||
"the contents of the list with a type annotation, like: "
|
"the contents of the list with a type annotation, like: "
|
||||||
"orders: List[Order]. Docs: TODO")
|
"orders: List[Order]. Docs: TODO"
|
||||||
|
)
|
||||||
embedded_cls = embedded_cls[0]
|
embedded_cls = embedded_cls[0]
|
||||||
attr = getattr(embedded_cls, item)
|
attr = getattr(embedded_cls, item)
|
||||||
else:
|
else:
|
||||||
|
@ -266,10 +295,10 @@ class QueryNotSupportedError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class RediSearchFieldTypes(Enum):
|
class RediSearchFieldTypes(Enum):
|
||||||
TEXT = 'TEXT'
|
TEXT = "TEXT"
|
||||||
TAG = 'TAG'
|
TAG = "TAG"
|
||||||
NUMERIC = 'NUMERIC'
|
NUMERIC = "NUMERIC"
|
||||||
GEO = 'GEO'
|
GEO = "GEO"
|
||||||
|
|
||||||
|
|
||||||
# TODO: How to handle Geo fields?
|
# TODO: How to handle Geo fields?
|
||||||
|
@ -278,13 +307,15 @@ DEFAULT_PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
class FindQuery:
|
class FindQuery:
|
||||||
def __init__(self,
|
def __init__(
|
||||||
|
self,
|
||||||
expressions: Sequence[ExpressionOrNegated],
|
expressions: Sequence[ExpressionOrNegated],
|
||||||
model: Type['RedisModel'],
|
model: Type["RedisModel"],
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int = DEFAULT_PAGE_SIZE,
|
limit: int = DEFAULT_PAGE_SIZE,
|
||||||
page_size: int = DEFAULT_PAGE_SIZE,
|
page_size: int = DEFAULT_PAGE_SIZE,
|
||||||
sort_fields: Optional[List[str]] = None):
|
sort_fields: Optional[List[str]] = None,
|
||||||
|
):
|
||||||
self.expressions = expressions
|
self.expressions = expressions
|
||||||
self.model = model
|
self.model = model
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
|
@ -308,7 +339,7 @@ class FindQuery:
|
||||||
page_size=self.page_size,
|
page_size=self.page_size,
|
||||||
limit=self.limit,
|
limit=self.limit,
|
||||||
expressions=copy(self.expressions),
|
expressions=copy(self.expressions),
|
||||||
sort_fields=copy(self.sort_fields)
|
sort_fields=copy(self.sort_fields),
|
||||||
)
|
)
|
||||||
|
|
||||||
def copy(self, **kwargs):
|
def copy(self, **kwargs):
|
||||||
|
@ -330,7 +361,9 @@ class FindQuery:
|
||||||
if self.expressions:
|
if self.expressions:
|
||||||
self._expression = reduce(operator.and_, self.expressions)
|
self._expression = reduce(operator.and_, self.expressions)
|
||||||
else:
|
else:
|
||||||
self._expression = Expression(left=None, right=None, op=Operators.ALL, parents=[])
|
self._expression = Expression(
|
||||||
|
left=None, right=None, op=Operators.ALL, parents=[]
|
||||||
|
)
|
||||||
return self._expression
|
return self._expression
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -350,24 +383,30 @@ class FindQuery:
|
||||||
for sort_field in sort_fields:
|
for sort_field in sort_fields:
|
||||||
field_name = sort_field.lstrip("-")
|
field_name = sort_field.lstrip("-")
|
||||||
if field_name not in self.model.__fields__:
|
if field_name not in self.model.__fields__:
|
||||||
raise QueryNotSupportedError(f"You tried sort by {field_name}, but that field "
|
raise QueryNotSupportedError(
|
||||||
f"does not exist on the model {self.model}")
|
f"You tried sort by {field_name}, but that field "
|
||||||
|
f"does not exist on the model {self.model}"
|
||||||
|
)
|
||||||
field_proxy = getattr(self.model, field_name)
|
field_proxy = getattr(self.model, field_name)
|
||||||
if not getattr(field_proxy.field.field_info, 'sortable', False):
|
if not getattr(field_proxy.field.field_info, "sortable", False):
|
||||||
raise QueryNotSupportedError(f"You tried sort by {field_name}, but {self.model} does "
|
raise QueryNotSupportedError(
|
||||||
"not define that field as sortable. See docs: XXX")
|
f"You tried sort by {field_name}, but {self.model} does "
|
||||||
|
"not define that field as sortable. See docs: XXX"
|
||||||
|
)
|
||||||
return sort_fields
|
return sort_fields
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_field_type(field: ModelField, op: Operators) -> RediSearchFieldTypes:
|
def resolve_field_type(field: ModelField, op: Operators) -> RediSearchFieldTypes:
|
||||||
if getattr(field.field_info, 'primary_key', None) is True:
|
if getattr(field.field_info, "primary_key", None) is True:
|
||||||
return RediSearchFieldTypes.TAG
|
return RediSearchFieldTypes.TAG
|
||||||
elif op is Operators.LIKE:
|
elif op is Operators.LIKE:
|
||||||
fts = getattr(field.field_info, 'full_text_search', None)
|
fts = getattr(field.field_info, "full_text_search", None)
|
||||||
if fts is not True: # Could be PydanticUndefined
|
if fts is not True: # Could be PydanticUndefined
|
||||||
raise QuerySyntaxError(f"You tried to do a full-text search on the field '{field.name}', "
|
raise QuerySyntaxError(
|
||||||
|
f"You tried to do a full-text search on the field '{field.name}', "
|
||||||
f"but the field is not indexed for full-text search. Use the "
|
f"but the field is not indexed for full-text search. Use the "
|
||||||
f"full_text_search=True option. Docs: TODO")
|
f"full_text_search=True option. Docs: TODO"
|
||||||
|
)
|
||||||
return RediSearchFieldTypes.TEXT
|
return RediSearchFieldTypes.TEXT
|
||||||
|
|
||||||
field_type = field.outer_type_
|
field_type = field.outer_type_
|
||||||
|
@ -391,8 +430,10 @@ class FindQuery:
|
||||||
# within the model inside the list marked as `index=True`.
|
# within the model inside the list marked as `index=True`.
|
||||||
return RediSearchFieldTypes.TAG
|
return RediSearchFieldTypes.TAG
|
||||||
elif container_type is not None:
|
elif container_type is not None:
|
||||||
raise QuerySyntaxError("Only lists and tuples are supported for multi-value fields. "
|
raise QuerySyntaxError(
|
||||||
"See docs: TODO")
|
"Only lists and tuples are supported for multi-value fields. "
|
||||||
|
"See docs: TODO"
|
||||||
|
)
|
||||||
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
|
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
|
||||||
# Index numeric Python types as NUMERIC fields, so we can support
|
# Index numeric Python types as NUMERIC fields, so we can support
|
||||||
# range queries.
|
# range queries.
|
||||||
|
@ -419,14 +460,23 @@ class FindQuery:
|
||||||
try:
|
try:
|
||||||
return "|".join([escaper.escape(str(v)) for v in value])
|
return "|".join([escaper.escape(str(v)) for v in value])
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.debug("Escaping single non-iterable value used for an IN or "
|
log.debug(
|
||||||
"NOT_IN query: %s", value)
|
"Escaping single non-iterable value used for an IN or "
|
||||||
|
"NOT_IN query: %s",
|
||||||
|
value,
|
||||||
|
)
|
||||||
return escaper.escape(str(value))
|
return escaper.escape(str(value))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resolve_value(cls, field_name: str, field_type: RediSearchFieldTypes,
|
def resolve_value(
|
||||||
field_info: PydanticFieldInfo, op: Operators, value: Any,
|
cls,
|
||||||
parents: List[Tuple[str, 'RedisModel']]) -> str:
|
field_name: str,
|
||||||
|
field_type: RediSearchFieldTypes,
|
||||||
|
field_info: PydanticFieldInfo,
|
||||||
|
op: Operators,
|
||||||
|
value: Any,
|
||||||
|
parents: List[Tuple[str, "RedisModel"]],
|
||||||
|
) -> str:
|
||||||
if parents:
|
if parents:
|
||||||
prefix = "_".join([p[0] for p in parents])
|
prefix = "_".join([p[0] for p in parents])
|
||||||
field_name = f"{prefix}_{field_name}"
|
field_name = f"{prefix}_{field_name}"
|
||||||
|
@ -440,9 +490,11 @@ class FindQuery:
|
||||||
elif op is Operators.LIKE:
|
elif op is Operators.LIKE:
|
||||||
result += value
|
result += value
|
||||||
else:
|
else:
|
||||||
raise QueryNotSupportedError("Only equals (=), not-equals (!=), and like() "
|
raise QueryNotSupportedError(
|
||||||
|
"Only equals (=), not-equals (!=), and like() "
|
||||||
"comparisons are supported for TEXT fields. See "
|
"comparisons are supported for TEXT fields. See "
|
||||||
"docs: TODO.")
|
"docs: TODO."
|
||||||
|
)
|
||||||
elif field_type is RediSearchFieldTypes.NUMERIC:
|
elif field_type is RediSearchFieldTypes.NUMERIC:
|
||||||
if op is Operators.EQ:
|
if op is Operators.EQ:
|
||||||
result += f"@{field_name}:[{value} {value}]"
|
result += f"@{field_name}:[{value} {value}]"
|
||||||
|
@ -460,16 +512,22 @@ class FindQuery:
|
||||||
# field and our hidden use of TAG for exact-match queries?
|
# field and our hidden use of TAG for exact-match queries?
|
||||||
elif field_type is RediSearchFieldTypes.TAG:
|
elif field_type is RediSearchFieldTypes.TAG:
|
||||||
if op is Operators.EQ:
|
if op is Operators.EQ:
|
||||||
separator_char = getattr(field_info, 'separator',
|
separator_char = getattr(
|
||||||
SINGLE_VALUE_TAG_FIELD_SEPARATOR)
|
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
|
||||||
|
)
|
||||||
if value == separator_char:
|
if value == separator_char:
|
||||||
# The value is ONLY the TAG field separator character --
|
# The value is ONLY the TAG field separator character --
|
||||||
# this is not going to work.
|
# this is not going to work.
|
||||||
log.warning("Your query against the field %s is for a single character, %s, "
|
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-developer-python. We must ignore "
|
||||||
"this portion of the query. Please review your query to find "
|
"this portion of the query. Please review your query to find "
|
||||||
"an alternative query that uses a string containing more than "
|
"an alternative query that uses a string containing more than "
|
||||||
"just the character %s.", field_name, separator_char, separator_char)
|
"just the character %s.",
|
||||||
|
field_name,
|
||||||
|
separator_char,
|
||||||
|
separator_char,
|
||||||
|
)
|
||||||
return ""
|
return ""
|
||||||
if separator_char in value:
|
if separator_char in value:
|
||||||
# The value contains the TAG field separator. We can work
|
# The value contains the TAG field separator. We can work
|
||||||
|
@ -506,8 +564,8 @@ class FindQuery:
|
||||||
return
|
return
|
||||||
fields = []
|
fields = []
|
||||||
for f in self.sort_fields:
|
for f in self.sort_fields:
|
||||||
direction = "desc" if f.startswith('-') else 'asc'
|
direction = "desc" if f.startswith("-") else "asc"
|
||||||
fields.extend([f.lstrip('-'), direction])
|
fields.extend([f.lstrip("-"), direction])
|
||||||
if self.sort_fields:
|
if self.sort_fields:
|
||||||
return ["SORTBY", *fields]
|
return ["SORTBY", *fields]
|
||||||
|
|
||||||
|
@ -550,23 +608,30 @@ class FindQuery:
|
||||||
if encompassing_expression_is_negated:
|
if encompassing_expression_is_negated:
|
||||||
# TODO: Is there a use case for this, perhaps for dynamic
|
# TODO: Is there a use case for this, perhaps for dynamic
|
||||||
# scoring purposes with full-text search?
|
# scoring purposes with full-text search?
|
||||||
raise QueryNotSupportedError("You cannot negate a query for all results.")
|
raise QueryNotSupportedError(
|
||||||
|
"You cannot negate a query for all results."
|
||||||
|
)
|
||||||
return "*"
|
return "*"
|
||||||
|
|
||||||
if isinstance(expression.left, Expression) or \
|
if isinstance(expression.left, Expression) or isinstance(
|
||||||
isinstance(expression.left, NegatedExpression):
|
expression.left, NegatedExpression
|
||||||
|
):
|
||||||
result += f"({cls.resolve_redisearch_query(expression.left)})"
|
result += f"({cls.resolve_redisearch_query(expression.left)})"
|
||||||
elif isinstance(expression.left, ModelField):
|
elif isinstance(expression.left, ModelField):
|
||||||
field_type = cls.resolve_field_type(expression.left, expression.op)
|
field_type = cls.resolve_field_type(expression.left, expression.op)
|
||||||
field_name = expression.left.name
|
field_name = expression.left.name
|
||||||
field_info = expression.left.field_info
|
field_info = expression.left.field_info
|
||||||
if not field_info or not getattr(field_info, "index", None):
|
if not field_info or not getattr(field_info, "index", None):
|
||||||
raise QueryNotSupportedError(f"You tried to query by a field ({field_name}) "
|
raise QueryNotSupportedError(
|
||||||
f"that isn't indexed. See docs: TODO")
|
f"You tried to query by a field ({field_name}) "
|
||||||
|
f"that isn't indexed. See docs: TODO"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise QueryNotSupportedError(f"A query expression should start with either a field "
|
raise QueryNotSupportedError(
|
||||||
f"or an expression enclosed in parenthesis. See docs: "
|
"A query expression should start with either a field "
|
||||||
f"TODO")
|
"or an expression enclosed in parenthesis. See docs: "
|
||||||
|
"TODO"
|
||||||
|
)
|
||||||
|
|
||||||
right = expression.right
|
right = expression.right
|
||||||
|
|
||||||
|
@ -576,8 +641,10 @@ class FindQuery:
|
||||||
elif expression.op == Operators.OR:
|
elif expression.op == Operators.OR:
|
||||||
result += "| "
|
result += "| "
|
||||||
else:
|
else:
|
||||||
raise QueryNotSupportedError("You can only combine two query expressions with"
|
raise QueryNotSupportedError(
|
||||||
"AND (&) or OR (|). See docs: TODO")
|
"You can only combine two query expressions with"
|
||||||
|
"AND (&) or OR (|). See docs: TODO"
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(right, NegatedExpression):
|
if isinstance(right, NegatedExpression):
|
||||||
result += "-"
|
result += "-"
|
||||||
|
@ -594,10 +661,18 @@ class FindQuery:
|
||||||
elif not field_info:
|
elif not field_info:
|
||||||
raise QuerySyntaxError("Could not resolve field info. See docs: TODO")
|
raise QuerySyntaxError("Could not resolve field info. See docs: TODO")
|
||||||
elif isinstance(right, ModelField):
|
elif isinstance(right, ModelField):
|
||||||
raise QueryNotSupportedError("Comparing fields is not supported. See docs: TODO")
|
raise QueryNotSupportedError(
|
||||||
|
"Comparing fields is not supported. See docs: TODO"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result += cls.resolve_value(field_name, field_type, field_info,
|
result += cls.resolve_value(
|
||||||
expression.op, right, expression.parents)
|
field_name,
|
||||||
|
field_type,
|
||||||
|
field_info,
|
||||||
|
expression.op,
|
||||||
|
right,
|
||||||
|
expression.parents,
|
||||||
|
)
|
||||||
|
|
||||||
if encompassing_expression_is_negated:
|
if encompassing_expression_is_negated:
|
||||||
result = f"-({result})"
|
result = f"-({result})"
|
||||||
|
@ -658,7 +733,7 @@ class FindQuery:
|
||||||
return self
|
return self
|
||||||
return self.copy(sort_fields=list(fields))
|
return self.copy(sort_fields=list(fields))
|
||||||
|
|
||||||
def update(self, use_transaction=True, **field_values) -> Optional[List[str]]:
|
def update(self, use_transaction=True, **field_values):
|
||||||
"""
|
"""
|
||||||
Update models that match this query to the given field-value pairs.
|
Update models that match this query to the given field-value pairs.
|
||||||
|
|
||||||
|
@ -672,11 +747,14 @@ class FindQuery:
|
||||||
for model in self.all():
|
for model in self.all():
|
||||||
for field, value in field_values.items():
|
for field, value in field_values.items():
|
||||||
setattr(model, field, value)
|
setattr(model, field, value)
|
||||||
|
# TODO: In the non-transaction case, can we do more to detect
|
||||||
|
# failure responses from Redis?
|
||||||
model.save(pipeline=pipeline)
|
model.save(pipeline=pipeline)
|
||||||
|
|
||||||
if pipeline:
|
if pipeline:
|
||||||
# TODO: Better response type, error detection
|
# TODO: Response type?
|
||||||
return pipeline.execute()
|
# TODO: Better error detection for transactions.
|
||||||
|
pipeline.execute()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Delete all matching records in this query."""
|
"""Delete all matching records in this query."""
|
||||||
|
@ -722,6 +800,7 @@ class UlidPrimaryKey:
|
||||||
"""A client-side generated primary key that follows the ULID spec.
|
"""A client-side generated primary key that follows the ULID spec.
|
||||||
https://github.com/ulid/javascript#specification
|
https://github.com/ulid/javascript#specification
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_pk(*args, **kwargs) -> str:
|
def create_pk(*args, **kwargs) -> str:
|
||||||
return str(ULID())
|
return str(ULID())
|
||||||
|
@ -848,6 +927,7 @@ class DefaultMeta:
|
||||||
TODO: Revisit whether this is really necessary, and whether making
|
TODO: Revisit whether this is really necessary, and whether making
|
||||||
these all optional here is the right choice.
|
these all optional here is the right choice.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global_key_prefix: Optional[str] = None
|
global_key_prefix: Optional[str] = None
|
||||||
model_key_prefix: Optional[str] = None
|
model_key_prefix: Optional[str] = None
|
||||||
primary_key_pattern: Optional[str] = None
|
primary_key_pattern: Optional[str] = None
|
||||||
|
@ -863,28 +943,32 @@ class ModelMeta(ModelMetaclass):
|
||||||
_meta: MetaProtocol
|
_meta: MetaProtocol
|
||||||
|
|
||||||
def __new__(cls, name, bases, attrs, **kwargs): # noqa C901
|
def __new__(cls, name, bases, attrs, **kwargs): # noqa C901
|
||||||
meta = attrs.pop('Meta', None)
|
meta = attrs.pop("Meta", None)
|
||||||
new_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
new_class = super().__new__(cls, name, bases, attrs, **kwargs)
|
||||||
|
|
||||||
# The fact that there is a Meta field and _meta field is important: a
|
# The fact that there is a Meta field and _meta field is important: a
|
||||||
# user may have given us a Meta object with their configuration, while
|
# user may have given us a Meta object with their configuration, while
|
||||||
# we might have inherited _meta from a parent class, and should
|
# we might have inherited _meta from a parent class, and should
|
||||||
# therefore use some of the inherited fields.
|
# therefore use some of the inherited fields.
|
||||||
meta = meta or getattr(new_class, 'Meta', None)
|
meta = meta or getattr(new_class, "Meta", None)
|
||||||
base_meta = getattr(new_class, '_meta', None)
|
base_meta = getattr(new_class, "_meta", None)
|
||||||
|
|
||||||
if meta and meta != DefaultMeta and meta != base_meta:
|
if meta and meta != DefaultMeta and meta != base_meta:
|
||||||
new_class.Meta = meta
|
new_class.Meta = meta
|
||||||
new_class._meta = meta
|
new_class._meta = meta
|
||||||
elif base_meta:
|
elif base_meta:
|
||||||
new_class._meta = type(f'{new_class.__name__}Meta', (base_meta,), dict(base_meta.__dict__))
|
new_class._meta = type(
|
||||||
|
f"{new_class.__name__}Meta", (base_meta,), dict(base_meta.__dict__)
|
||||||
|
)
|
||||||
new_class.Meta = new_class._meta
|
new_class.Meta = new_class._meta
|
||||||
# Unset inherited values we don't want to reuse (typically based on
|
# Unset inherited values we don't want to reuse (typically based on
|
||||||
# the model name).
|
# the model name).
|
||||||
new_class._meta.model_key_prefix = None
|
new_class._meta.model_key_prefix = None
|
||||||
new_class._meta.index_name = None
|
new_class._meta.index_name = None
|
||||||
else:
|
else:
|
||||||
new_class._meta = type(f'{new_class.__name__}Meta', (DefaultMeta,), dict(DefaultMeta.__dict__))
|
new_class._meta = type(
|
||||||
|
f"{new_class.__name__}Meta", (DefaultMeta,), dict(DefaultMeta.__dict__)
|
||||||
|
)
|
||||||
new_class.Meta = new_class._meta
|
new_class.Meta = new_class._meta
|
||||||
|
|
||||||
# Create proxies for each model field so that we can use the field
|
# Create proxies for each model field so that we can use the field
|
||||||
|
@ -894,29 +978,40 @@ class ModelMeta(ModelMetaclass):
|
||||||
# Check if this is our FieldInfo version with extended ORM metadata.
|
# Check if this is our FieldInfo version with extended ORM metadata.
|
||||||
if isinstance(field.field_info, FieldInfo):
|
if isinstance(field.field_info, FieldInfo):
|
||||||
if field.field_info.primary_key:
|
if field.field_info.primary_key:
|
||||||
new_class._meta.primary_key = PrimaryKey(name=field_name, field=field)
|
new_class._meta.primary_key = PrimaryKey(
|
||||||
|
name=field_name, field=field
|
||||||
|
)
|
||||||
|
|
||||||
if not getattr(new_class._meta, 'global_key_prefix', None):
|
if not getattr(new_class._meta, "global_key_prefix", None):
|
||||||
new_class._meta.global_key_prefix = getattr(base_meta, "global_key_prefix", "")
|
new_class._meta.global_key_prefix = getattr(
|
||||||
if not getattr(new_class._meta, 'model_key_prefix', None):
|
base_meta, "global_key_prefix", ""
|
||||||
|
)
|
||||||
|
if not getattr(new_class._meta, "model_key_prefix", None):
|
||||||
# Don't look at the base class for this.
|
# Don't look at the base class for this.
|
||||||
new_class._meta.model_key_prefix = f"{new_class.__module__}.{new_class.__name__}"
|
new_class._meta.model_key_prefix = (
|
||||||
if not getattr(new_class._meta, 'primary_key_pattern', None):
|
f"{new_class.__module__}.{new_class.__name__}"
|
||||||
new_class._meta.primary_key_pattern = getattr(base_meta, "primary_key_pattern",
|
)
|
||||||
"{pk}")
|
if not getattr(new_class._meta, "primary_key_pattern", None):
|
||||||
if not getattr(new_class._meta, 'database', None):
|
new_class._meta.primary_key_pattern = getattr(
|
||||||
new_class._meta.database = getattr(base_meta, "database",
|
base_meta, "primary_key_pattern", "{pk}"
|
||||||
redis.Redis(decode_responses=True))
|
)
|
||||||
if not getattr(new_class._meta, 'primary_key_creator_cls', None):
|
if not getattr(new_class._meta, "database", None):
|
||||||
new_class._meta.primary_key_creator_cls = getattr(base_meta, "primary_key_creator_cls",
|
new_class._meta.database = getattr(
|
||||||
UlidPrimaryKey)
|
base_meta, "database", redis.Redis(decode_responses=True)
|
||||||
if not getattr(new_class._meta, 'index_name', None):
|
)
|
||||||
new_class._meta.index_name = f"{new_class._meta.global_key_prefix}:" \
|
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
|
||||||
|
)
|
||||||
|
if not getattr(new_class._meta, "index_name", None):
|
||||||
|
new_class._meta.index_name = (
|
||||||
|
f"{new_class._meta.global_key_prefix}:"
|
||||||
f"{new_class._meta.model_key_prefix}:index"
|
f"{new_class._meta.model_key_prefix}:index"
|
||||||
|
)
|
||||||
|
|
||||||
# Not an abstract model class or embedded model, so we should let the
|
# Not an abstract model class or embedded model, so we should let the
|
||||||
# Migrator create indexes for it.
|
# Migrator create indexes for it.
|
||||||
if abc.ABC not in bases and not getattr(new_class._meta, 'embedded', False):
|
if abc.ABC not in bases and not getattr(new_class._meta, "embedded", False):
|
||||||
key = f"{new_class.__module__}.{new_class.__qualname__}"
|
key = f"{new_class.__module__}.{new_class.__qualname__}"
|
||||||
model_registry[key] = new_class
|
model_registry[key] = new_class
|
||||||
|
|
||||||
|
@ -931,7 +1026,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
class Config:
|
class Config:
|
||||||
orm_mode = True
|
orm_mode = True
|
||||||
arbitrary_types_allowed = True
|
arbitrary_types_allowed = True
|
||||||
extra = 'allow'
|
extra = "allow"
|
||||||
|
|
||||||
def __init__(__pydantic_self__, **data: Any) -> None:
|
def __init__(__pydantic_self__, **data: Any) -> None:
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
|
@ -953,7 +1048,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
"""Update this model instance with the specified key-value pairs."""
|
"""Update this model instance with the specified key-value pairs."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def save(self, *args, **kwargs) -> 'RedisModel':
|
def save(self, pipeline: Optional[Pipeline] = None) -> "RedisModel":
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@validator("pk", always=True)
|
@validator("pk", always=True)
|
||||||
|
@ -967,7 +1062,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
"""Check for a primary key. We need one (and only one)."""
|
"""Check for a primary key. We need one (and only one)."""
|
||||||
primary_keys = 0
|
primary_keys = 0
|
||||||
for name, field in cls.__fields__.items():
|
for name, field in cls.__fields__.items():
|
||||||
if getattr(field.field_info, 'primary_key', None):
|
if getattr(field.field_info, "primary_key", None):
|
||||||
primary_keys += 1
|
primary_keys += 1
|
||||||
if primary_keys == 0:
|
if primary_keys == 0:
|
||||||
raise RedisModelError("You must define a primary key for the model")
|
raise RedisModelError("You must define a primary key for the model")
|
||||||
|
@ -976,8 +1071,8 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_key(cls, part: str):
|
def make_key(cls, part: str):
|
||||||
global_prefix = getattr(cls._meta, 'global_key_prefix', '').strip(":")
|
global_prefix = getattr(cls._meta, "global_key_prefix", "").strip(":")
|
||||||
model_prefix = getattr(cls._meta, 'model_key_prefix', '').strip(":")
|
model_prefix = getattr(cls._meta, "model_key_prefix", "").strip(":")
|
||||||
return f"{global_prefix}:{model_prefix}:{part}"
|
return f"{global_prefix}:{model_prefix}:{part}"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -997,13 +1092,14 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
def from_redis(cls, res: Any):
|
def from_redis(cls, res: Any):
|
||||||
# TODO: Parsing logic copied from redisearch-py. Evaluate.
|
# TODO: Parsing logic copied from redisearch-py. Evaluate.
|
||||||
import six
|
import six
|
||||||
from six.moves import xrange, zip as izip
|
from six.moves import xrange
|
||||||
|
from six.moves import zip as izip
|
||||||
|
|
||||||
def to_string(s):
|
def to_string(s):
|
||||||
if isinstance(s, six.string_types):
|
if isinstance(s, six.string_types):
|
||||||
return s
|
return s
|
||||||
elif isinstance(s, six.binary_type):
|
elif isinstance(s, six.binary_type):
|
||||||
return s.decode('utf-8', 'ignore')
|
return s.decode("utf-8", "ignore")
|
||||||
else:
|
else:
|
||||||
return s # Not a string we care about
|
return s # Not a string we care about
|
||||||
|
|
||||||
|
@ -1015,23 +1111,27 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
fields_offset = offset
|
fields_offset = offset
|
||||||
|
|
||||||
fields = dict(
|
fields = dict(
|
||||||
dict(izip(map(to_string, res[i + fields_offset][::2]),
|
dict(
|
||||||
map(to_string, res[i + fields_offset][1::2])))
|
izip(
|
||||||
|
map(to_string, res[i + fields_offset][::2]),
|
||||||
|
map(to_string, res[i + fields_offset][1::2]),
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
del fields['id']
|
del fields["id"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
fields['json'] = fields['$']
|
fields["json"] = fields["$"]
|
||||||
del fields['$']
|
del fields["$"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if 'json' in fields:
|
if "json" in fields:
|
||||||
json_fields = json.loads(fields['json'])
|
json_fields = json.loads(fields["json"])
|
||||||
doc = cls(**json_fields)
|
doc = cls(**json_fields)
|
||||||
else:
|
else:
|
||||||
doc = cls(**fields)
|
doc = cls(**fields)
|
||||||
|
@ -1039,7 +1139,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
||||||
return docs
|
return docs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, models: Sequence['RedisModel']) -> Sequence['RedisModel']:
|
def add(cls, models: Sequence["RedisModel"]) -> Sequence["RedisModel"]:
|
||||||
# TODO: Add transaction support
|
# TODO: Add transaction support
|
||||||
return [model.save() for model in models]
|
return [model.save() for model in models]
|
||||||
|
|
||||||
|
@ -1059,15 +1159,18 @@ class HashModel(RedisModel, abc.ABC):
|
||||||
|
|
||||||
for name, field in cls.__fields__.items():
|
for name, field in cls.__fields__.items():
|
||||||
if issubclass(field.outer_type_, RedisModel):
|
if issubclass(field.outer_type_, RedisModel):
|
||||||
raise RedisModelError(f"HashModels cannot have embedded model "
|
raise RedisModelError(
|
||||||
f"fields. Field: {name}")
|
f"HashModels cannot have embedded model " f"fields. Field: {name}"
|
||||||
|
)
|
||||||
|
|
||||||
for typ in (Set, Mapping, List):
|
for typ in (Set, Mapping, List):
|
||||||
if issubclass(field.outer_type_, typ):
|
if issubclass(field.outer_type_, typ):
|
||||||
raise RedisModelError(f"HashModels cannot have set, list,"
|
raise RedisModelError(
|
||||||
f" or mapping fields. Field: {name}")
|
f"HashModels cannot have set, list,"
|
||||||
|
f" or mapping fields. Field: {name}"
|
||||||
|
)
|
||||||
|
|
||||||
def save(self, pipeline: Optional[Pipeline] = None) -> 'HashModel':
|
def save(self, pipeline: Optional[Pipeline] = None) -> "HashModel":
|
||||||
if pipeline is None:
|
if pipeline is None:
|
||||||
db = self.db()
|
db = self.db()
|
||||||
else:
|
else:
|
||||||
|
@ -1077,7 +1180,7 @@ class HashModel(RedisModel, abc.ABC):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, pk: Any) -> 'HashModel':
|
def get(cls, pk: Any) -> "HashModel":
|
||||||
document = cls.db().hgetall(cls.make_primary_key(pk))
|
document = cls.db().hgetall(cls.make_primary_key(pk))
|
||||||
if not document:
|
if not document:
|
||||||
raise NotFoundError
|
raise NotFoundError
|
||||||
|
@ -1111,13 +1214,17 @@ class HashModel(RedisModel, abc.ABC):
|
||||||
for name, field in cls.__fields__.items():
|
for name, field in cls.__fields__.items():
|
||||||
# TODO: Merge this code with schema_for_type()?
|
# TODO: Merge this code with schema_for_type()?
|
||||||
_type = field.outer_type_
|
_type = field.outer_type_
|
||||||
if getattr(field.field_info, 'primary_key', None):
|
if getattr(field.field_info, "primary_key", None):
|
||||||
if issubclass(_type, str):
|
if issubclass(_type, str):
|
||||||
redisearch_field = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
redisearch_field = (
|
||||||
|
f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
redisearch_field = cls.schema_for_type(name, _type, field.field_info)
|
redisearch_field = cls.schema_for_type(
|
||||||
|
name, _type, field.field_info
|
||||||
|
)
|
||||||
schema_parts.append(redisearch_field)
|
schema_parts.append(redisearch_field)
|
||||||
elif getattr(field.field_info, 'index', None) is True:
|
elif getattr(field.field_info, "index", None) is True:
|
||||||
schema_parts.append(cls.schema_for_type(name, _type, field.field_info))
|
schema_parts.append(cls.schema_for_type(name, _type, field.field_info))
|
||||||
elif is_supported_container_type(_type):
|
elif is_supported_container_type(_type):
|
||||||
embedded_cls = get_args(_type)
|
embedded_cls = get_args(_type)
|
||||||
|
@ -1126,8 +1233,9 @@ class HashModel(RedisModel, abc.ABC):
|
||||||
log.warning("Model %s defined an empty list field: %s", cls, name)
|
log.warning("Model %s defined an empty list field: %s", cls, name)
|
||||||
continue
|
continue
|
||||||
embedded_cls = embedded_cls[0]
|
embedded_cls = embedded_cls[0]
|
||||||
schema_parts.append(cls.schema_for_type(name, embedded_cls,
|
schema_parts.append(
|
||||||
field.field_info))
|
cls.schema_for_type(name, embedded_cls, field.field_info)
|
||||||
|
)
|
||||||
elif issubclass(_type, RedisModel):
|
elif issubclass(_type, RedisModel):
|
||||||
schema_parts.append(cls.schema_for_type(name, _type, field.field_info))
|
schema_parts.append(cls.schema_for_type(name, _type, field.field_info))
|
||||||
return schema_parts
|
return schema_parts
|
||||||
|
@ -1141,29 +1249,36 @@ class HashModel(RedisModel, abc.ABC):
|
||||||
# as sortable.
|
# as sortable.
|
||||||
# TODO: Abstract string-building logic for each type (TAG, etc.) into
|
# TODO: Abstract string-building logic for each type (TAG, etc.) into
|
||||||
# classes that take a field name.
|
# classes that take a field name.
|
||||||
sortable = getattr(field_info, 'sortable', False)
|
sortable = getattr(field_info, "sortable", False)
|
||||||
|
|
||||||
if is_supported_container_type(typ):
|
if is_supported_container_type(typ):
|
||||||
embedded_cls = get_args(typ)
|
embedded_cls = get_args(typ)
|
||||||
if not embedded_cls:
|
if not embedded_cls:
|
||||||
# TODO: Test if this can really happen.
|
# TODO: Test if this can really happen.
|
||||||
log.warning("Model %s defined an empty list or tuple field: %s", cls, name)
|
log.warning(
|
||||||
|
"Model %s defined an empty list or tuple field: %s", cls, name
|
||||||
|
)
|
||||||
return ""
|
return ""
|
||||||
embedded_cls = embedded_cls[0]
|
embedded_cls = embedded_cls[0]
|
||||||
schema = cls.schema_for_type(name, embedded_cls, field_info)
|
schema = cls.schema_for_type(name, embedded_cls, field_info)
|
||||||
elif any(issubclass(typ, t) for t in NUMERIC_TYPES):
|
elif any(issubclass(typ, t) for t in NUMERIC_TYPES):
|
||||||
schema = f"{name} NUMERIC"
|
schema = f"{name} NUMERIC"
|
||||||
elif issubclass(typ, str):
|
elif issubclass(typ, str):
|
||||||
if getattr(field_info, 'full_text_search', False) is True:
|
if getattr(field_info, "full_text_search", False) is True:
|
||||||
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} " \
|
schema = (
|
||||||
|
f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} "
|
||||||
f"{name}_fts TEXT"
|
f"{name}_fts TEXT"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||||
elif issubclass(typ, RedisModel):
|
elif issubclass(typ, RedisModel):
|
||||||
sub_fields = []
|
sub_fields = []
|
||||||
for embedded_name, field in typ.__fields__.items():
|
for embedded_name, field in typ.__fields__.items():
|
||||||
sub_fields.append(cls.schema_for_type(f"{name}_{embedded_name}", field.outer_type_,
|
sub_fields.append(
|
||||||
field.field_info))
|
cls.schema_for_type(
|
||||||
|
f"{name}_{embedded_name}", field.outer_type_, field.field_info
|
||||||
|
)
|
||||||
|
)
|
||||||
schema = " ".join(sub_fields)
|
schema = " ".join(sub_fields)
|
||||||
else:
|
else:
|
||||||
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||||
|
@ -1177,12 +1292,12 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
# Generate the RediSearch schema once to validate fields.
|
# Generate the RediSearch schema once to validate fields.
|
||||||
cls.redisearch_schema()
|
cls.redisearch_schema()
|
||||||
|
|
||||||
def save(self, pipeline: Optional[Pipeline] = None) -> 'JsonModel':
|
def save(self, pipeline: Optional[Pipeline] = None) -> "JsonModel":
|
||||||
if pipeline is None:
|
if pipeline is None:
|
||||||
db = self.db()
|
db = self.db()
|
||||||
else:
|
else:
|
||||||
db = pipeline
|
db = pipeline
|
||||||
db.execute_command('JSON.SET', self.key(), ".", self.json())
|
db.execute_command("JSON.SET", self.key(), ".", self.json())
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def update(self, **field_values):
|
def update(self, **field_values):
|
||||||
|
@ -1192,7 +1307,7 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, pk: Any) -> 'JsonModel':
|
def get(cls, pk: Any) -> "JsonModel":
|
||||||
document = cls.db().execute_command("JSON.GET", cls.make_primary_key(pk))
|
document = cls.db().execute_command("JSON.GET", cls.make_primary_key(pk))
|
||||||
if not document:
|
if not document:
|
||||||
raise NotFoundError
|
raise NotFoundError
|
||||||
|
@ -1212,21 +1327,31 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
|
|
||||||
for name, field in cls.__fields__.items():
|
for name, field in cls.__fields__.items():
|
||||||
_type = field.outer_type_
|
_type = field.outer_type_
|
||||||
schema_parts.append(cls.schema_for_type(
|
schema_parts.append(
|
||||||
json_path, name, "", _type, field.field_info))
|
cls.schema_for_type(json_path, name, "", _type, field.field_info)
|
||||||
|
)
|
||||||
return schema_parts
|
return schema_parts
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def schema_for_type(cls, json_path: str, name: str, name_prefix: str, typ: Any,
|
def schema_for_type(
|
||||||
|
cls,
|
||||||
|
json_path: str,
|
||||||
|
name: str,
|
||||||
|
name_prefix: str,
|
||||||
|
typ: Any,
|
||||||
field_info: PydanticFieldInfo,
|
field_info: PydanticFieldInfo,
|
||||||
parent_type: Optional[Any] = None) -> str:
|
parent_type: Optional[Any] = None,
|
||||||
should_index = getattr(field_info, 'index', False)
|
) -> str:
|
||||||
|
should_index = getattr(field_info, "index", False)
|
||||||
is_container_type = is_supported_container_type(typ)
|
is_container_type = is_supported_container_type(typ)
|
||||||
parent_is_container_type = is_supported_container_type(parent_type)
|
parent_is_container_type = is_supported_container_type(parent_type)
|
||||||
|
parent_is_model = False
|
||||||
|
|
||||||
|
if parent_type:
|
||||||
try:
|
try:
|
||||||
parent_is_model = issubclass(parent_type, RedisModel)
|
parent_is_model = issubclass(parent_type, RedisModel)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
parent_is_model = False
|
pass
|
||||||
|
|
||||||
# TODO: We need a better way to know that we're indexing a value
|
# TODO: We need a better way to know that we're indexing a value
|
||||||
# discovered in a model within an array.
|
# discovered in a model within an array.
|
||||||
|
@ -1253,11 +1378,19 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
field_type = get_origin(typ)
|
field_type = get_origin(typ)
|
||||||
embedded_cls = get_args(typ)
|
embedded_cls = get_args(typ)
|
||||||
if not embedded_cls:
|
if not embedded_cls:
|
||||||
log.warning("Model %s defined an empty list or tuple field: %s", cls, name)
|
log.warning(
|
||||||
|
"Model %s defined an empty list or tuple field: %s", cls, name
|
||||||
|
)
|
||||||
return ""
|
return ""
|
||||||
embedded_cls = embedded_cls[0]
|
embedded_cls = embedded_cls[0]
|
||||||
return cls.schema_for_type(f"{json_path}.{name}[*]", name, name_prefix,
|
return cls.schema_for_type(
|
||||||
embedded_cls, field_info, parent_type=field_type)
|
f"{json_path}.{name}[*]",
|
||||||
|
name,
|
||||||
|
name_prefix,
|
||||||
|
embedded_cls,
|
||||||
|
field_info,
|
||||||
|
parent_type=field_type,
|
||||||
|
)
|
||||||
elif field_is_model:
|
elif field_is_model:
|
||||||
name_prefix = f"{name_prefix}_{name}" if name_prefix else name
|
name_prefix = f"{name_prefix}_{name}" if name_prefix else name
|
||||||
sub_fields = []
|
sub_fields = []
|
||||||
|
@ -1273,12 +1406,16 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
# current field name and "embedded" field name, e.g.,
|
# current field name and "embedded" field name, e.g.,
|
||||||
# order.address.street_line_1.
|
# order.address.street_line_1.
|
||||||
path = f"{json_path}.{name}"
|
path = f"{json_path}.{name}"
|
||||||
sub_fields.append(cls.schema_for_type(path,
|
sub_fields.append(
|
||||||
|
cls.schema_for_type(
|
||||||
|
path,
|
||||||
embedded_name,
|
embedded_name,
|
||||||
name_prefix,
|
name_prefix,
|
||||||
field.outer_type_,
|
field.outer_type_,
|
||||||
field.field_info,
|
field.field_info,
|
||||||
parent_type=typ))
|
parent_type=typ,
|
||||||
|
)
|
||||||
|
)
|
||||||
return " ".join(filter(None, sub_fields))
|
return " ".join(filter(None, sub_fields))
|
||||||
# NOTE: This is the termination point for recursion. We've descended
|
# NOTE: This is the termination point for recursion. We've descended
|
||||||
# into models and lists until we found an actual value to index.
|
# into models and lists until we found an actual value to index.
|
||||||
|
@ -1291,20 +1428,26 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
path = json_path
|
path = json_path
|
||||||
else:
|
else:
|
||||||
path = f"{json_path}.{name}"
|
path = f"{json_path}.{name}"
|
||||||
sortable = getattr(field_info, 'sortable', False)
|
sortable = getattr(field_info, "sortable", False)
|
||||||
full_text_search = getattr(field_info, 'full_text_search', False)
|
full_text_search = getattr(field_info, "full_text_search", False)
|
||||||
sortable_tag_error = RedisModelError("In this Preview release, TAG fields cannot "
|
sortable_tag_error = RedisModelError(
|
||||||
|
"In this Preview release, TAG fields cannot "
|
||||||
f"be marked as sortable. Problem field: {name}. "
|
f"be marked as sortable. Problem field: {name}. "
|
||||||
"See docs: TODO")
|
"See docs: TODO"
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: GEO field
|
# TODO: GEO field
|
||||||
if parent_is_container_type or parent_is_model_in_container:
|
if parent_is_container_type or parent_is_model_in_container:
|
||||||
if typ is not str:
|
if typ is not str:
|
||||||
raise RedisModelError("In this Preview release, list and tuple fields can only "
|
raise RedisModelError(
|
||||||
f"contain strings. Problem field: {name}. See docs: TODO")
|
"In this Preview release, list and tuple fields can only "
|
||||||
|
f"contain strings. Problem field: {name}. See docs: TODO"
|
||||||
|
)
|
||||||
if full_text_search is True:
|
if full_text_search is True:
|
||||||
raise RedisModelError("List and tuple fields cannot be indexed for full-text "
|
raise RedisModelError(
|
||||||
f"search. Problem field: {name}. See docs: TODO")
|
"List and tuple fields cannot be indexed for full-text "
|
||||||
|
f"search. Problem field: {name}. See docs: TODO"
|
||||||
|
)
|
||||||
schema = f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
schema = f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||||
if sortable is True:
|
if sortable is True:
|
||||||
raise sortable_tag_error
|
raise sortable_tag_error
|
||||||
|
@ -1312,8 +1455,10 @@ class JsonModel(RedisModel, abc.ABC):
|
||||||
schema = f"{path} AS {index_field_name} NUMERIC"
|
schema = f"{path} AS {index_field_name} NUMERIC"
|
||||||
elif issubclass(typ, str):
|
elif issubclass(typ, str):
|
||||||
if full_text_search is True:
|
if full_text_search is True:
|
||||||
schema = f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} " \
|
schema = (
|
||||||
|
f"{path} AS {index_field_name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} "
|
||||||
f"{path} AS {index_field_name}_fts TEXT"
|
f"{path} AS {index_field_name}_fts TEXT"
|
||||||
|
)
|
||||||
if sortable is True:
|
if sortable is True:
|
||||||
# NOTE: With the current preview release, making a field
|
# NOTE: With the current preview release, making a field
|
||||||
# full-text searchable and sortable only makes the TEXT
|
# full-text searchable and sortable only makes the TEXT
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import abc
|
import abc
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from redis_developer.model.model import JsonModel, HashModel
|
from redis_developer.model.model import HashModel, JsonModel
|
||||||
|
|
||||||
|
|
||||||
class BaseJsonModel(JsonModel, abc.ABC):
|
class BaseJsonModel(JsonModel, abc.ABC):
|
||||||
|
@ -22,6 +22,7 @@ class BaseHashModel(HashModel, abc.ABC):
|
||||||
# postal_code: str
|
# postal_code: str
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class AddressHash(BaseHashModel):
|
class AddressHash(BaseHashModel):
|
||||||
address_line_1: str
|
address_line_1: str
|
||||||
address_line_2: Optional[str]
|
address_line_2: Optional[str]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from collections import Sequence
|
from collections import Sequence
|
||||||
from typing import Any, Dict, Mapping, Union, List
|
from typing import Any, Dict, List, Mapping, Union
|
||||||
|
|
||||||
from redis_developer.model.model import Expression
|
from redis_developer.model.model import Expression
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ class Not(LogicalOperatorForListOfExpressions):
|
||||||
-(@price:[-inf 10]) -(@category:{Sweets})
|
-(@price:[-inf 10]) -(@category:{Sweets})
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def query(self):
|
def query(self):
|
||||||
return "-(expression1) -(expression2)"
|
return "-(expression1) -(expression2)"
|
||||||
|
@ -102,5 +103,3 @@ class QueryResolver:
|
||||||
|
|
||||||
def resolve(self) -> str:
|
def resolve(self) -> str:
|
||||||
"""Resolve expressions to a RediSearch query string."""
|
"""Resolve expressions to a RediSearch query string."""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,15 @@ and released under the MIT license: https://github.com/clemtoy/pptree
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
|
||||||
def render_tree(current_node, nameattr='name', left_child='left',
|
def render_tree(
|
||||||
right_child='right', indent='', last='updown',
|
current_node,
|
||||||
buffer=None):
|
nameattr="name",
|
||||||
|
left_child="left",
|
||||||
|
right_child="right",
|
||||||
|
indent="",
|
||||||
|
last="updown",
|
||||||
|
buffer=None,
|
||||||
|
):
|
||||||
"""Print a tree-like structure, `current_node`.
|
"""Print a tree-like structure, `current_node`.
|
||||||
|
|
||||||
This is a mostly-direct-copy of the print_tree() function from the ppbtree
|
This is a mostly-direct-copy of the print_tree() function from the ppbtree
|
||||||
|
@ -18,42 +24,52 @@ def render_tree(current_node, nameattr='name', left_child='left',
|
||||||
if buffer is None:
|
if buffer is None:
|
||||||
buffer = io.StringIO()
|
buffer = io.StringIO()
|
||||||
if hasattr(current_node, nameattr):
|
if hasattr(current_node, nameattr):
|
||||||
name = lambda node: getattr(node, nameattr)
|
name = lambda node: getattr(node, nameattr) # noqa: E731
|
||||||
else:
|
else:
|
||||||
name = lambda node: str(node)
|
name = lambda node: str(node) # noqa: E731
|
||||||
|
|
||||||
up = getattr(current_node, left_child, None)
|
up = getattr(current_node, left_child, None)
|
||||||
down = getattr(current_node, right_child, None)
|
down = getattr(current_node, right_child, None)
|
||||||
|
|
||||||
if up is not None:
|
if up is not None:
|
||||||
next_last = 'up'
|
next_last = "up"
|
||||||
next_indent = '{0}{1}{2}'.format(indent, ' ' if 'up' in last else '|', ' ' * len(str(name(current_node))))
|
next_indent = "{0}{1}{2}".format(
|
||||||
render_tree(up, nameattr, left_child, right_child, next_indent, next_last, buffer)
|
indent, " " if "up" in last else "|", " " * len(str(name(current_node)))
|
||||||
|
)
|
||||||
|
render_tree(
|
||||||
|
up, nameattr, left_child, right_child, next_indent, next_last, buffer
|
||||||
|
)
|
||||||
|
|
||||||
if last == 'up':
|
if last == "up":
|
||||||
start_shape = '┌'
|
start_shape = "┌"
|
||||||
elif last == 'down':
|
elif last == "down":
|
||||||
start_shape = '└'
|
start_shape = "└"
|
||||||
elif last == 'updown':
|
elif last == "updown":
|
||||||
start_shape = ' '
|
start_shape = " "
|
||||||
else:
|
else:
|
||||||
start_shape = '├'
|
start_shape = "├"
|
||||||
|
|
||||||
if up is not None and down is not None:
|
if up is not None and down is not None:
|
||||||
end_shape = '┤'
|
end_shape = "┤"
|
||||||
elif up:
|
elif up:
|
||||||
end_shape = '┘'
|
end_shape = "┘"
|
||||||
elif down:
|
elif down:
|
||||||
end_shape = '┐'
|
end_shape = "┐"
|
||||||
else:
|
else:
|
||||||
end_shape = ''
|
end_shape = ""
|
||||||
|
|
||||||
print('{0}{1}{2}{3}'.format(indent, start_shape, name(current_node), end_shape),
|
print(
|
||||||
file=buffer)
|
"{0}{1}{2}{3}".format(indent, start_shape, name(current_node), end_shape),
|
||||||
|
file=buffer,
|
||||||
|
)
|
||||||
|
|
||||||
if down is not None:
|
if down is not None:
|
||||||
next_last = 'down'
|
next_last = "down"
|
||||||
next_indent = '{0}{1}{2}'.format(indent, ' ' if 'down' in last else '|', ' ' * len(str(name(current_node))))
|
next_indent = "{0}{1}{2}".format(
|
||||||
render_tree(down, nameattr, left_child, right_child, next_indent, next_last, buffer)
|
indent, " " if "down" in last else "|", " " * len(str(name(current_node)))
|
||||||
|
)
|
||||||
|
render_tree(
|
||||||
|
down, nameattr, left_child, right_child, next_indent, next_last, buffer
|
||||||
|
)
|
||||||
|
|
||||||
return f"\n{buffer.getvalue()}"
|
return f"\n{buffer.getvalue()}"
|
|
@ -6,6 +6,7 @@ class TokenEscaper:
|
||||||
"""
|
"""
|
||||||
Escape punctuation within an input string.
|
Escape punctuation within an input string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Characters that RediSearch requires us to escape during queries.
|
# Characters that RediSearch requires us to escape during queries.
|
||||||
# Source: https://oss.redis.com/redisearch/Escaping/#the_rules_of_text_field_tokenization
|
# Source: https://oss.redis.com/redisearch/Escaping/#the_rules_of_text_field_tokenization
|
||||||
DEFAULT_ESCAPED_CHARS = r"[,.<>{}\[\]\\\"\':;!@#$%^&*()\-+=~\ ]"
|
DEFAULT_ESCAPED_CHARS = r"[,.<>{}\[\]\\\"\':;!@#$%^&*()\-+=~\ ]"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import abc
|
import abc
|
||||||
import decimal
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import decimal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
@ -8,11 +8,13 @@ import pytest
|
||||||
import redis
|
import redis
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from redis_developer.model import (
|
from redis_developer.model import Field, HashModel
|
||||||
HashModel,
|
from redis_developer.model.model import (
|
||||||
Field,
|
NotFoundError,
|
||||||
|
QueryNotSupportedError,
|
||||||
|
RedisModelError,
|
||||||
)
|
)
|
||||||
from redis_developer.model.model import RedisModelError, QueryNotSupportedError, NotFoundError
|
|
||||||
|
|
||||||
r = redis.Redis()
|
r = redis.Redis()
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
|
@ -48,7 +50,7 @@ def members():
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
age=38,
|
age=38,
|
||||||
join_date=today
|
join_date=today,
|
||||||
)
|
)
|
||||||
|
|
||||||
member2 = Member(
|
member2 = Member(
|
||||||
|
@ -56,7 +58,7 @@ def members():
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
email="k@example.com",
|
email="k@example.com",
|
||||||
age=34,
|
age=34,
|
||||||
join_date=today
|
join_date=today,
|
||||||
)
|
)
|
||||||
|
|
||||||
member3 = Member(
|
member3 = Member(
|
||||||
|
@ -64,7 +66,7 @@ def members():
|
||||||
last_name="Smith",
|
last_name="Smith",
|
||||||
email="as@example.com",
|
email="as@example.com",
|
||||||
age=100,
|
age=100,
|
||||||
join_date=today
|
join_date=today,
|
||||||
)
|
)
|
||||||
member1.save()
|
member1.save()
|
||||||
member2.save()
|
member2.save()
|
||||||
|
@ -76,21 +78,13 @@ def members():
|
||||||
def test_validates_required_fields():
|
def test_validates_required_fields():
|
||||||
# Raises ValidationError: last_name is required
|
# Raises ValidationError: last_name is required
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
Member(
|
Member(first_name="Andrew", zipcode="97086", join_date=today)
|
||||||
first_name="Andrew",
|
|
||||||
zipcode="97086",
|
|
||||||
join_date=today
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_validates_field():
|
def test_validates_field():
|
||||||
# Raises ValidationError: join_date is not a date
|
# Raises ValidationError: join_date is not a date
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
Member(
|
Member(first_name="Andrew", last_name="Brookins", join_date="yesterday")
|
||||||
first_name="Andrew",
|
|
||||||
last_name="Brookins",
|
|
||||||
join_date="yesterday"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Passes validation
|
# Passes validation
|
||||||
|
@ -100,7 +94,7 @@ def test_validation_passes():
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
join_date=today,
|
join_date=today,
|
||||||
age=38
|
age=38,
|
||||||
)
|
)
|
||||||
assert member.first_name == "Andrew"
|
assert member.first_name == "Andrew"
|
||||||
|
|
||||||
|
@ -111,7 +105,7 @@ def test_saves_model_and_creates_pk():
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
join_date=today,
|
join_date=today,
|
||||||
age=38
|
age=38,
|
||||||
)
|
)
|
||||||
# Save a model instance to Redis
|
# Save a model instance to Redis
|
||||||
member.save()
|
member.save()
|
||||||
|
@ -129,6 +123,7 @@ def test_raises_error_with_embedded_models():
|
||||||
postal_code: str
|
postal_code: str
|
||||||
|
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class InvalidMember(BaseHashModel):
|
class InvalidMember(BaseHashModel):
|
||||||
address: Address
|
address: Address
|
||||||
|
|
||||||
|
@ -140,14 +135,14 @@ def test_saves_many():
|
||||||
first_name="Andrew",
|
first_name="Andrew",
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
join_date=today
|
join_date=today,
|
||||||
),
|
),
|
||||||
Member(
|
Member(
|
||||||
first_name="Kim",
|
first_name="Kim",
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
email="k@example.com",
|
email="k@example.com",
|
||||||
join_date=today
|
join_date=today,
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
Member.add(members)
|
Member.add(members)
|
||||||
|
|
||||||
|
@ -174,21 +169,21 @@ def test_paginate_query(members):
|
||||||
|
|
||||||
def test_access_result_by_index_cached(members):
|
def test_access_result_by_index_cached(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
query = Member.find().sort_by('age')
|
query = Member.find().sort_by("age")
|
||||||
# Load the cache, throw away the result.
|
# Load the cache, throw away the result.
|
||||||
assert query._model_cache == []
|
assert query._model_cache == []
|
||||||
query.execute()
|
query.execute()
|
||||||
assert query._model_cache == [member2, member1, member3]
|
assert query._model_cache == [member2, member1, member3]
|
||||||
|
|
||||||
# Access an item that should be in the cache.
|
# Access an item that should be in the cache.
|
||||||
with mock.patch.object(query.model, 'db') as mock_db:
|
with mock.patch.object(query.model, "db") as mock_db:
|
||||||
assert query[0] == member2
|
assert query[0] == member2
|
||||||
assert not mock_db.called
|
assert not mock_db.called
|
||||||
|
|
||||||
|
|
||||||
def test_access_result_by_index_not_cached(members):
|
def test_access_result_by_index_not_cached(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
query = Member.find().sort_by('age')
|
query = Member.find().sort_by("age")
|
||||||
|
|
||||||
# Assert that we don't have any models in the cache yet -- we
|
# Assert that we don't have any models in the cache yet -- we
|
||||||
# haven't made any requests of Redis.
|
# haven't made any requests of Redis.
|
||||||
|
@ -205,7 +200,8 @@ def test_exact_match_queries(members):
|
||||||
assert actual == [member1, member2]
|
assert actual == [member1, member2]
|
||||||
|
|
||||||
actual = Member.find(
|
actual = Member.find(
|
||||||
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")).all()
|
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")
|
||||||
|
).all()
|
||||||
assert actual == [member2]
|
assert actual == [member2]
|
||||||
|
|
||||||
actual = Member.find(~(Member.last_name == "Brookins")).all()
|
actual = Member.find(~(Member.last_name == "Brookins")).all()
|
||||||
|
@ -220,16 +216,19 @@ def test_exact_match_queries(members):
|
||||||
).all()
|
).all()
|
||||||
assert actual == [member1, member2]
|
assert actual == [member1, member2]
|
||||||
|
|
||||||
actual = Member.find(Member.first_name == "Kim", Member.last_name == "Brookins").all()
|
actual = Member.find(
|
||||||
|
Member.first_name == "Kim", Member.last_name == "Brookins"
|
||||||
|
).all()
|
||||||
assert actual == [member2]
|
assert actual == [member2]
|
||||||
|
|
||||||
|
|
||||||
def test_recursive_query_resolution(members):
|
def test_recursive_query_resolution(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
|
||||||
actual = Member.find((Member.last_name == "Brookins") | (
|
actual = Member.find(
|
||||||
Member.age == 100
|
(Member.last_name == "Brookins")
|
||||||
) & (Member.last_name == "Smith")).all()
|
| (Member.age == 100) & (Member.last_name == "Smith")
|
||||||
|
).all()
|
||||||
assert actual == [member1, member2, member3]
|
assert actual == [member1, member2, member3]
|
||||||
|
|
||||||
|
|
||||||
|
@ -237,8 +236,9 @@ def test_tag_queries_boolean_logic(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
|
||||||
actual = Member.find(
|
actual = Member.find(
|
||||||
(Member.first_name == "Andrew") &
|
(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith")).all()
|
| (Member.last_name == "Smith")
|
||||||
|
).all()
|
||||||
assert actual == [member1, member3]
|
assert actual == [member1, member3]
|
||||||
|
|
||||||
|
|
||||||
|
@ -281,9 +281,7 @@ def test_tag_queries_negation(members):
|
||||||
└Andrew
|
└Andrew
|
||||||
|
|
||||||
"""
|
"""
|
||||||
query = Member.find(
|
query = Member.find(~(Member.first_name == "Andrew"))
|
||||||
~(Member.first_name == "Andrew")
|
|
||||||
)
|
|
||||||
assert query.all() == [member2]
|
assert query.all() == [member2]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -315,8 +313,9 @@ def test_tag_queries_negation(members):
|
||||||
└Smith
|
└Smith
|
||||||
"""
|
"""
|
||||||
query = Member.find(
|
query = Member.find(
|
||||||
~(Member.first_name == "Andrew") &
|
~(Member.first_name == "Andrew")
|
||||||
((Member.last_name == "Brookins") | (Member.last_name == "Smith")))
|
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||||
|
)
|
||||||
assert query.all() == [member2]
|
assert query.all() == [member2]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -333,12 +332,14 @@ def test_tag_queries_negation(members):
|
||||||
└Smith
|
└Smith
|
||||||
"""
|
"""
|
||||||
query = Member.find(
|
query = Member.find(
|
||||||
~(Member.first_name == "Andrew") &
|
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
| (Member.last_name == "Smith")
|
||||||
|
)
|
||||||
assert query.all() == [member2, member3]
|
assert query.all() == [member2, member3]
|
||||||
|
|
||||||
actual = Member.find(
|
actual = Member.find(
|
||||||
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")).all()
|
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
|
||||||
|
).all()
|
||||||
assert actual == [member3]
|
assert actual == [member3]
|
||||||
|
|
||||||
|
|
||||||
|
@ -373,19 +374,19 @@ def test_numeric_queries(members):
|
||||||
def test_sorting(members):
|
def test_sorting(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
|
||||||
actual = Member.find(Member.age > 34).sort_by('age').all()
|
actual = Member.find(Member.age > 34).sort_by("age").all()
|
||||||
assert actual == [member1, member3]
|
assert actual == [member1, member3]
|
||||||
|
|
||||||
actual = Member.find(Member.age > 34).sort_by('-age').all()
|
actual = Member.find(Member.age > 34).sort_by("-age").all()
|
||||||
assert actual == [member3, member1]
|
assert actual == [member3, member1]
|
||||||
|
|
||||||
with pytest.raises(QueryNotSupportedError):
|
with pytest.raises(QueryNotSupportedError):
|
||||||
# This field does not exist.
|
# This field does not exist.
|
||||||
Member.find().sort_by('not-a-real-field').all()
|
Member.find().sort_by("not-a-real-field").all()
|
||||||
|
|
||||||
with pytest.raises(QueryNotSupportedError):
|
with pytest.raises(QueryNotSupportedError):
|
||||||
# This field is not sortable.
|
# This field is not sortable.
|
||||||
Member.find().sort_by('join_date').all()
|
Member.find().sort_by("join_date").all()
|
||||||
|
|
||||||
|
|
||||||
def test_not_found():
|
def test_not_found():
|
||||||
|
@ -403,4 +404,7 @@ def test_schema():
|
||||||
another_integer: int
|
another_integer: int
|
||||||
another_float: float
|
another_float: float
|
||||||
|
|
||||||
assert Address.redisearch_schema() == "ON HASH PREFIX 1 redis-developer:tests.test_hash_model.Address: SCHEMA pk TAG SEPARATOR | a_string TAG SEPARATOR | a_full_text_string TAG SEPARATOR | a_full_text_string_fts TEXT an_integer NUMERIC SORTABLE a_float NUMERIC"
|
assert (
|
||||||
|
Address.redisearch_schema()
|
||||||
|
== "ON HASH PREFIX 1 redis-developer:tests.test_hash_model.Address: SCHEMA pk TAG SEPARATOR | a_string TAG SEPARATOR | a_full_text_string TAG SEPARATOR | a_full_text_string_fts TEXT an_integer NUMERIC SORTABLE a_float NUMERIC"
|
||||||
|
)
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import abc
|
import abc
|
||||||
import decimal
|
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Optional, List
|
import decimal
|
||||||
|
from typing import List, Optional
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import redis
|
import redis
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from redis_developer.model import (
|
from redis_developer.model import EmbeddedJsonModel, Field, JsonModel
|
||||||
EmbeddedJsonModel,
|
|
||||||
JsonModel,
|
|
||||||
Field,
|
|
||||||
)
|
|
||||||
from redis_developer.model.migrations.migrator import Migrator
|
from redis_developer.model.migrations.migrator import Migrator
|
||||||
from redis_developer.model.model import QueryNotSupportedError, NotFoundError, RedisModelError
|
from redis_developer.model.model import (
|
||||||
|
NotFoundError,
|
||||||
|
QueryNotSupportedError,
|
||||||
|
RedisModelError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
r = redis.Redis()
|
r = redis.Redis()
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
|
@ -75,7 +76,7 @@ def address():
|
||||||
city="Portland",
|
city="Portland",
|
||||||
state="OR",
|
state="OR",
|
||||||
country="USA",
|
country="USA",
|
||||||
postal_code=11111
|
postal_code=11111,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ def members(address):
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
age=38,
|
age=38,
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
|
|
||||||
member2 = Member(
|
member2 = Member(
|
||||||
|
@ -96,7 +97,7 @@ def members(address):
|
||||||
email="k@example.com",
|
email="k@example.com",
|
||||||
age=34,
|
age=34,
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
|
|
||||||
member3 = Member(
|
member3 = Member(
|
||||||
|
@ -105,7 +106,7 @@ def members(address):
|
||||||
email="as@example.com",
|
email="as@example.com",
|
||||||
age=100,
|
age=100,
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
|
|
||||||
member1.save()
|
member1.save()
|
||||||
|
@ -133,7 +134,7 @@ def test_validates_field(address):
|
||||||
first_name="Andrew",
|
first_name="Andrew",
|
||||||
last_name="Brookins",
|
last_name="Brookins",
|
||||||
join_date="yesterday",
|
join_date="yesterday",
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,7 +146,7 @@ def test_validation_passes(address):
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
join_date=today,
|
join_date=today,
|
||||||
age=38,
|
age=38,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
assert member.first_name == "Andrew"
|
assert member.first_name == "Andrew"
|
||||||
|
|
||||||
|
@ -157,7 +158,7 @@ def test_saves_model_and_creates_pk(address):
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
join_date=today,
|
join_date=today,
|
||||||
age=38,
|
age=38,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
# Save a model instance to Redis
|
# Save a model instance to Redis
|
||||||
member.save()
|
member.save()
|
||||||
|
@ -176,7 +177,7 @@ def test_saves_many(address):
|
||||||
email="a@example.com",
|
email="a@example.com",
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address,
|
address=address,
|
||||||
age=38
|
age=38,
|
||||||
),
|
),
|
||||||
Member(
|
Member(
|
||||||
first_name="Kim",
|
first_name="Kim",
|
||||||
|
@ -184,8 +185,8 @@ def test_saves_many(address):
|
||||||
email="k@example.com",
|
email="k@example.com",
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address,
|
address=address,
|
||||||
age=34
|
age=34,
|
||||||
)
|
),
|
||||||
]
|
]
|
||||||
Member.add(members)
|
Member.add(members)
|
||||||
|
|
||||||
|
@ -216,21 +217,21 @@ def test_paginate_query(members):
|
||||||
|
|
||||||
def test_access_result_by_index_cached(members):
|
def test_access_result_by_index_cached(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
query = Member.find().sort_by('age')
|
query = Member.find().sort_by("age")
|
||||||
# Load the cache, throw away the result.
|
# Load the cache, throw away the result.
|
||||||
assert query._model_cache == []
|
assert query._model_cache == []
|
||||||
query.execute()
|
query.execute()
|
||||||
assert query._model_cache == [member2, member1, member3]
|
assert query._model_cache == [member2, member1, member3]
|
||||||
|
|
||||||
# Access an item that should be in the cache.
|
# Access an item that should be in the cache.
|
||||||
with mock.patch.object(query.model, 'db') as mock_db:
|
with mock.patch.object(query.model, "db") as mock_db:
|
||||||
assert query[0] == member2
|
assert query[0] == member2
|
||||||
assert not mock_db.called
|
assert not mock_db.called
|
||||||
|
|
||||||
|
|
||||||
def test_access_result_by_index_not_cached(members):
|
def test_access_result_by_index_not_cached(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
query = Member.find().sort_by('age')
|
query = Member.find().sort_by("age")
|
||||||
|
|
||||||
# Assert that we don't have any models in the cache yet -- we
|
# Assert that we don't have any models in the cache yet -- we
|
||||||
# haven't made any requests of Redis.
|
# haven't made any requests of Redis.
|
||||||
|
@ -252,8 +253,11 @@ def test_update_query(members):
|
||||||
Member.find(Member.pk << [member1.pk, member2.pk, member3.pk]).update(
|
Member.find(Member.pk << [member1.pk, member2.pk, member3.pk]).update(
|
||||||
first_name="Bobby"
|
first_name="Bobby"
|
||||||
)
|
)
|
||||||
actual = Member.find(
|
actual = (
|
||||||
Member.pk << [member1.pk, member2.pk, member3.pk]).sort_by('age').all()
|
Member.find(Member.pk << [member1.pk, member2.pk, member3.pk])
|
||||||
|
.sort_by("age")
|
||||||
|
.all()
|
||||||
|
)
|
||||||
assert actual == [member1, member2, member3]
|
assert actual == [member1, member2, member3]
|
||||||
assert all([m.name == "Bobby" for m in actual])
|
assert all([m.name == "Bobby" for m in actual])
|
||||||
|
|
||||||
|
@ -265,7 +269,8 @@ def test_exact_match_queries(members):
|
||||||
assert actual == [member1, member2]
|
assert actual == [member1, member2]
|
||||||
|
|
||||||
actual = Member.find(
|
actual = Member.find(
|
||||||
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")).all()
|
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")
|
||||||
|
).all()
|
||||||
assert actual == [member2]
|
assert actual == [member2]
|
||||||
|
|
||||||
actual = Member.find(~(Member.last_name == "Brookins")).all()
|
actual = Member.find(~(Member.last_name == "Brookins")).all()
|
||||||
|
@ -280,7 +285,9 @@ def test_exact_match_queries(members):
|
||||||
).all()
|
).all()
|
||||||
assert actual == [member1, member2]
|
assert actual == [member1, member2]
|
||||||
|
|
||||||
actual = Member.find(Member.first_name == "Kim", Member.last_name == "Brookins").all()
|
actual = Member.find(
|
||||||
|
Member.first_name == "Kim", Member.last_name == "Brookins"
|
||||||
|
).all()
|
||||||
assert actual == [member2]
|
assert actual == [member2]
|
||||||
|
|
||||||
actual = Member.find(Member.address.city == "Portland").all()
|
actual = Member.find(Member.address.city == "Portland").all()
|
||||||
|
@ -290,24 +297,28 @@ def test_exact_match_queries(members):
|
||||||
def test_recursive_query_expression_resolution(members):
|
def test_recursive_query_expression_resolution(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
|
||||||
actual = Member.find((Member.last_name == "Brookins") | (
|
actual = Member.find(
|
||||||
Member.age == 100
|
(Member.last_name == "Brookins")
|
||||||
) & (Member.last_name == "Smith")).all()
|
| (Member.age == 100) & (Member.last_name == "Smith")
|
||||||
|
).all()
|
||||||
assert actual == [member1, member2, member3]
|
assert actual == [member1, member2, member3]
|
||||||
|
|
||||||
|
|
||||||
def test_recursive_query_field_resolution(members):
|
def test_recursive_query_field_resolution(members):
|
||||||
member1, _, _ = members
|
member1, _, _ = members
|
||||||
member1.address.note = Note(description="Weird house",
|
member1.address.note = Note(
|
||||||
created_on=datetime.datetime.now())
|
description="Weird house", created_on=datetime.datetime.now()
|
||||||
|
)
|
||||||
member1.save()
|
member1.save()
|
||||||
actual = Member.find(Member.address.note.description == "Weird house").all()
|
actual = Member.find(Member.address.note.description == "Weird house").all()
|
||||||
assert actual == [member1]
|
assert actual == [member1]
|
||||||
|
|
||||||
member1.orders = [
|
member1.orders = [
|
||||||
Order(items=[Item(price=10.99, name="Ball")],
|
Order(
|
||||||
|
items=[Item(price=10.99, name="Ball")],
|
||||||
total=10.99,
|
total=10.99,
|
||||||
created_on=datetime.datetime.now())
|
created_on=datetime.datetime.now(),
|
||||||
|
)
|
||||||
]
|
]
|
||||||
member1.save()
|
member1.save()
|
||||||
actual = Member.find(Member.orders.items.name == "Ball").all()
|
actual = Member.find(Member.orders.items.name == "Ball").all()
|
||||||
|
@ -331,8 +342,9 @@ def test_tag_queries_boolean_logic(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
|
||||||
actual = Member.find(
|
actual = Member.find(
|
||||||
(Member.first_name == "Andrew") &
|
(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith")).all()
|
| (Member.last_name == "Smith")
|
||||||
|
).all()
|
||||||
assert actual == [member1, member3]
|
assert actual == [member1, member3]
|
||||||
|
|
||||||
|
|
||||||
|
@ -343,7 +355,7 @@ def test_tag_queries_punctuation(address):
|
||||||
email="a|b@example.com", # NOTE: This string uses the TAG field separator.
|
email="a|b@example.com", # NOTE: This string uses the TAG field separator.
|
||||||
age=38,
|
age=38,
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
member1.save()
|
member1.save()
|
||||||
|
|
||||||
|
@ -353,7 +365,7 @@ def test_tag_queries_punctuation(address):
|
||||||
email="a|villain@example.com", # NOTE: This string uses the TAG field separator.
|
email="a|villain@example.com", # NOTE: This string uses the TAG field separator.
|
||||||
age=38,
|
age=38,
|
||||||
join_date=today,
|
join_date=today,
|
||||||
address=address
|
address=address,
|
||||||
)
|
)
|
||||||
member2.save()
|
member2.save()
|
||||||
|
|
||||||
|
@ -377,9 +389,7 @@ def test_tag_queries_negation(members):
|
||||||
└Andrew
|
└Andrew
|
||||||
|
|
||||||
"""
|
"""
|
||||||
query = Member.find(
|
query = Member.find(~(Member.first_name == "Andrew"))
|
||||||
~(Member.first_name == "Andrew")
|
|
||||||
)
|
|
||||||
assert query.all() == [member2]
|
assert query.all() == [member2]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -411,8 +421,9 @@ def test_tag_queries_negation(members):
|
||||||
└Smith
|
└Smith
|
||||||
"""
|
"""
|
||||||
query = Member.find(
|
query = Member.find(
|
||||||
~(Member.first_name == "Andrew") &
|
~(Member.first_name == "Andrew")
|
||||||
((Member.last_name == "Brookins") | (Member.last_name == "Smith")))
|
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||||
|
)
|
||||||
assert query.all() == [member2]
|
assert query.all() == [member2]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -429,12 +440,14 @@ def test_tag_queries_negation(members):
|
||||||
└Smith
|
└Smith
|
||||||
"""
|
"""
|
||||||
query = Member.find(
|
query = Member.find(
|
||||||
~(Member.first_name == "Andrew") &
|
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
| (Member.last_name == "Smith")
|
||||||
|
)
|
||||||
assert query.all() == [member2, member3]
|
assert query.all() == [member2, member3]
|
||||||
|
|
||||||
actual = Member.find(
|
actual = Member.find(
|
||||||
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")).all()
|
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
|
||||||
|
).all()
|
||||||
assert actual == [member3]
|
assert actual == [member3]
|
||||||
|
|
||||||
|
|
||||||
|
@ -469,19 +482,19 @@ def test_numeric_queries(members):
|
||||||
def test_sorting(members):
|
def test_sorting(members):
|
||||||
member1, member2, member3 = members
|
member1, member2, member3 = members
|
||||||
|
|
||||||
actual = Member.find(Member.age > 34).sort_by('age').all()
|
actual = Member.find(Member.age > 34).sort_by("age").all()
|
||||||
assert actual == [member1, member3]
|
assert actual == [member1, member3]
|
||||||
|
|
||||||
actual = Member.find(Member.age > 34).sort_by('-age').all()
|
actual = Member.find(Member.age > 34).sort_by("-age").all()
|
||||||
assert actual == [member3, member1]
|
assert actual == [member3, member1]
|
||||||
|
|
||||||
with pytest.raises(QueryNotSupportedError):
|
with pytest.raises(QueryNotSupportedError):
|
||||||
# This field does not exist.
|
# This field does not exist.
|
||||||
Member.find().sort_by('not-a-real-field').all()
|
Member.find().sort_by("not-a-real-field").all()
|
||||||
|
|
||||||
with pytest.raises(QueryNotSupportedError):
|
with pytest.raises(QueryNotSupportedError):
|
||||||
# This field is not sortable.
|
# This field is not sortable.
|
||||||
Member.find().sort_by('join_date').all()
|
Member.find().sort_by("join_date").all()
|
||||||
|
|
||||||
|
|
||||||
def test_not_found():
|
def test_not_found():
|
||||||
|
@ -492,24 +505,28 @@ def test_not_found():
|
||||||
|
|
||||||
def test_list_field_limitations():
|
def test_list_field_limitations():
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class SortableTarotWitch(BaseJsonModel):
|
class SortableTarotWitch(BaseJsonModel):
|
||||||
# We support indexing lists of strings for quality and membership
|
# We support indexing lists of strings for quality and membership
|
||||||
# queries. Sorting is not supported, but is planned.
|
# queries. Sorting is not supported, but is planned.
|
||||||
tarot_cards: List[str] = Field(index=True, sortable=True)
|
tarot_cards: List[str] = Field(index=True, sortable=True)
|
||||||
|
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class SortableFullTextSearchAlchemicalWitch(BaseJsonModel):
|
class SortableFullTextSearchAlchemicalWitch(BaseJsonModel):
|
||||||
# We don't support indexing a list of strings for full-text search
|
# We don't support indexing a list of strings for full-text search
|
||||||
# queries. Support for this feature is not planned.
|
# queries. Support for this feature is not planned.
|
||||||
potions: List[str] = Field(index=True, full_text_search=True)
|
potions: List[str] = Field(index=True, full_text_search=True)
|
||||||
|
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class NumerologyWitch(BaseJsonModel):
|
class NumerologyWitch(BaseJsonModel):
|
||||||
# We don't support indexing a list of numbers. Support for this
|
# We don't support indexing a list of numbers. Support for this
|
||||||
# feature is To Be Determined.
|
# feature is To Be Determined.
|
||||||
lucky_numbers: List[int] = Field(index=True)
|
lucky_numbers: List[int] = Field(index=True)
|
||||||
|
|
||||||
with pytest.raises(RedisModelError):
|
with pytest.raises(RedisModelError):
|
||||||
|
|
||||||
class ReadingWithPrice(EmbeddedJsonModel):
|
class ReadingWithPrice(EmbeddedJsonModel):
|
||||||
gold_coins_charged: int = Field(index=True)
|
gold_coins_charged: int = Field(index=True)
|
||||||
|
|
||||||
|
@ -532,13 +549,14 @@ def test_list_field_limitations():
|
||||||
# suite's migrator has already looked for migrations to run.
|
# suite's migrator has already looked for migrations to run.
|
||||||
Migrator().run()
|
Migrator().run()
|
||||||
|
|
||||||
witch = TarotWitch(
|
witch = TarotWitch(tarot_cards=["death"])
|
||||||
tarot_cards=['death']
|
|
||||||
)
|
|
||||||
witch.save()
|
witch.save()
|
||||||
actual = TarotWitch.find(TarotWitch.tarot_cards << 'death').all()
|
actual = TarotWitch.find(TarotWitch.tarot_cards << "death").all()
|
||||||
assert actual == [witch]
|
assert actual == [witch]
|
||||||
|
|
||||||
|
|
||||||
def test_schema():
|
def test_schema():
|
||||||
assert Member.redisearch_schema() == "ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.bio AS bio TAG SEPARATOR | $.bio AS bio_fts TEXT $.address.pk AS address_pk TAG SEPARATOR | $.address.city AS address_city TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address_note_pk TAG SEPARATOR | $.address.note.description AS address_note_description TAG SEPARATOR | $.orders[*].pk AS orders_pk TAG SEPARATOR | $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR |"
|
assert (
|
||||||
|
Member.redisearch_schema()
|
||||||
|
== "ON JSON PREFIX 1 redis-developer:tests.test_json_model.Member: SCHEMA $.pk AS pk TAG SEPARATOR | $.first_name AS first_name TAG SEPARATOR | $.last_name AS last_name TAG SEPARATOR | $.email AS email TAG SEPARATOR | $.age AS age NUMERIC $.bio AS bio TAG SEPARATOR | $.bio AS bio_fts TEXT $.address.pk AS address_pk TAG SEPARATOR | $.address.city AS address_city TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address_note_pk TAG SEPARATOR | $.address.note.description AS address_note_description TAG SEPARATOR | $.orders[*].pk AS orders_pk TAG SEPARATOR | $.orders[*].items[*].pk AS orders_items_pk TAG SEPARATOR | $.orders[*].items[*].name AS orders_items_name TAG SEPARATOR |"
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue