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]]
|
||||
name = "astroid"
|
||||
version = "2.8.0"
|
||||
version = "2.8.3"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "~=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
lazy-object-proxy = ">=1.4.0"
|
||||
typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""}
|
||||
wrapt = ">=1.11,<1.13"
|
||||
wrapt = ">=1.11,<1.14"
|
||||
|
||||
[[package]]
|
||||
name = "async-timeout"
|
||||
|
@ -72,9 +72,51 @@ category = "dev"
|
|||
optional = false
|
||||
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]]
|
||||
name = "click"
|
||||
version = "8.0.1"
|
||||
version = "8.0.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -99,6 +141,42 @@ category = "dev"
|
|||
optional = false
|
||||
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]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
|
@ -122,7 +200,7 @@ toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""}
|
|||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "7.27.0"
|
||||
version = "7.28.0"
|
||||
description = "IPython: Productive Interactive Computing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
|
@ -156,7 +234,7 @@ test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipyk
|
|||
name = "isort"
|
||||
version = "5.9.3"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
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"
|
||||
version = "1.6.0"
|
||||
description = "A fast and thorough lazy object proxy."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||
|
||||
|
@ -204,7 +282,7 @@ traitlets = "*"
|
|||
name = "mccabe"
|
||||
version = "0.6.1"
|
||||
description = "McCabe checker, plugin for flake8"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
|
@ -212,7 +290,7 @@ python-versions = "*"
|
|||
name = "mypy"
|
||||
version = "0.910"
|
||||
description = "Optional static typing for Python"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
|
@ -229,7 +307,7 @@ python2 = ["typed-ast (>=1.4.0,<1.5.0)"]
|
|||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
|
@ -256,6 +334,22 @@ python-versions = ">=3.6"
|
|||
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
||||
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]]
|
||||
name = "pexpect"
|
||||
version = "4.8.0"
|
||||
|
@ -279,7 +373,7 @@ python-versions = "*"
|
|||
name = "platformdirs"
|
||||
version = "2.4.0"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
|
@ -334,6 +428,14 @@ category = "dev"
|
|||
optional = false
|
||||
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]]
|
||||
name = "pydantic"
|
||||
version = "1.8.2"
|
||||
|
@ -349,6 +451,14 @@ typing-extensions = ">=3.7.4.3"
|
|||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
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]]
|
||||
name = "pygments"
|
||||
version = "2.10.0"
|
||||
|
@ -361,7 +471,7 @@ python-versions = ">=3.5"
|
|||
name = "pylint"
|
||||
version = "2.11.1"
|
||||
description = "python code static checker"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "~=3.6"
|
||||
|
||||
|
@ -411,6 +521,14 @@ category = "main"
|
|||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0"
|
||||
description = "YAML parser and emitter for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "redis"
|
||||
version = "3.5.3"
|
||||
|
@ -422,6 +540,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "six"
|
||||
version = "1.16.0"
|
||||
|
@ -430,14 +556,41 @@ category = "main"
|
|||
optional = false
|
||||
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]]
|
||||
name = "toml"
|
||||
version = "0.10.2"
|
||||
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
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]]
|
||||
name = "traitlets"
|
||||
version = "5.1.0"
|
||||
|
@ -451,7 +604,7 @@ test = ["pytest"]
|
|||
|
||||
[[package]]
|
||||
name = "types-redis"
|
||||
version = "3.5.9"
|
||||
version = "3.5.15"
|
||||
description = "Typing stubs for redis"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -459,7 +612,7 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "types-six"
|
||||
version = "1.16.1"
|
||||
version = "1.16.2"
|
||||
description = "Typing stubs for six"
|
||||
category = "main"
|
||||
optional = false
|
||||
|
@ -483,16 +636,16 @@ python-versions = "*"
|
|||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "1.12.1"
|
||||
version = "1.13.2"
|
||||
description = "Module for decorators, wrappers and monkey patching."
|
||||
category = "main"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.8"
|
||||
content-hash = "e643c8bcc3f54c414e388a8c62256c3c0fe9e2fb0374c3f3b4140e2b0684b654"
|
||||
content-hash = "f1ccd73314f307ce41497d093ddce99cfb96ebf1814e854a94e37d5156647967"
|
||||
|
||||
[metadata.files]
|
||||
aioredis = [
|
||||
|
@ -504,8 +657,8 @@ appnope = [
|
|||
{file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"},
|
||||
]
|
||||
astroid = [
|
||||
{file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"},
|
||||
{file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"},
|
||||
{file = "astroid-2.8.3-py3-none-any.whl", hash = "sha256:f9d66e3a4a0e5b52819b2ff41ac2b179df9d180697db71c92beb33a60c661794"},
|
||||
{file = "astroid-2.8.3.tar.gz", hash = "sha256:0e361da0744d5011d4f5d57e64473ba9b7ab4da1e2d45d6631ebd67dd28c3cce"},
|
||||
]
|
||||
async-timeout = [
|
||||
{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.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 = [
|
||||
{file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"},
|
||||
{file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"},
|
||||
{file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"},
|
||||
{file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"},
|
||||
]
|
||||
colorama = [
|
||||
{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.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 = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{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"},
|
||||
]
|
||||
ipython = [
|
||||
{file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"},
|
||||
{file = "ipython-7.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"},
|
||||
{file = "ipython-7.28.0-py3-none-any.whl", hash = "sha256:f16148f9163e1e526f1008d7c8d966d9c15600ca20d1a754287cf96d00ba6f1d"},
|
||||
{file = "ipython-7.28.0.tar.gz", hash = "sha256:2097be5c814d1b974aea57673176a924c4c8c9583890e7a5f082f547b9975b11"},
|
||||
]
|
||||
isort = [
|
||||
{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.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 = [
|
||||
{file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"},
|
||||
{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.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 = [
|
||||
{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"},
|
||||
|
@ -678,6 +863,10 @@ pydantic = [
|
|||
{file = "pydantic-1.8.2-py3-none-any.whl", hash = "sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"},
|
||||
{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 = [
|
||||
{file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
|
||||
{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-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 = [
|
||||
{file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"},
|
||||
{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 = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{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 = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
{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 = [
|
||||
{file = "traitlets-5.1.0-py3-none-any.whl", hash = "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4"},
|
||||
{file = "traitlets-5.1.0.tar.gz", hash = "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d"},
|
||||
]
|
||||
types-redis = [
|
||||
{file = "types-redis-3.5.9.tar.gz", hash = "sha256:f142c48f4080757ca2a9441ec40213bda3b1535eebebfc4f3519e5aa46498076"},
|
||||
{file = "types_redis-3.5.9-py3-none-any.whl", hash = "sha256:5f5648ffc025708858097173cf695164c20f2b5e3f57177de14e352cae8cc335"},
|
||||
{file = "types-redis-3.5.15.tar.gz", hash = "sha256:e52be0077ca1189d8cce813a20c2a70e9e577f34ab898371c6cbed696a88bdee"},
|
||||
{file = "types_redis-3.5.15-py3-none-any.whl", hash = "sha256:e617c08bff88449b52f6dbdaa9bb81a806f27c89fd30bbf98fe9683ed5d1046a"},
|
||||
]
|
||||
types-six = [
|
||||
{file = "types-six-1.16.1.tar.gz", hash = "sha256:a9e6769cb0808f920958ac95f75c5191f49e21e041eac127fa62e286e1005616"},
|
||||
{file = "types_six-1.16.1-py2.py3-none-any.whl", hash = "sha256:b14f5abe26c0997bd41a1a32d6816af25932f7bfbc54246dfdc8f6f6404fd1d4"},
|
||||
{file = "types-six-1.16.2.tar.gz", hash = "sha256:b96bd911f87d15258c38e10ee3f0921c32887a5d22e41c39d15707b4d0e4d0f1"},
|
||||
{file = "types_six-1.16.2-py2.py3-none-any.whl", hash = "sha256:606dd8c7edff3100fae8277c270e65285e5cdb6a7819c0b1ea6a8973690e68da"},
|
||||
]
|
||||
typing-extensions = [
|
||||
{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"},
|
||||
]
|
||||
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"
|
||||
six = "^1.16.0"
|
||||
pptree = "^3.1"
|
||||
mypy = "^0.910"
|
||||
types-redis = "^3.5.9"
|
||||
types-six = "^1.16.1"
|
||||
python-ulid = "^1.0.3"
|
||||
pylint = "^2.11.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
mypy = "^0.910"
|
||||
pytest = "^6.2.4"
|
||||
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]
|
||||
migrate = "redis_developer.orm.cli.migrate:migrate"
|
||||
|
|
|
@ -1,7 +1 @@
|
|||
from .model import (
|
||||
RedisModel,
|
||||
HashModel,
|
||||
JsonModel,
|
||||
EmbeddedJsonModel,
|
||||
Field
|
||||
)
|
||||
from .model import EmbeddedJsonModel, Field, HashModel, JsonModel, RedisModel
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import click
|
||||
|
||||
from redis_developer.model.migrations.migrator import Migrator
|
||||
|
||||
|
||||
|
@ -12,5 +13,5 @@ def migrate(module):
|
|||
for migration in migrator.migrations:
|
||||
print(migration)
|
||||
|
||||
if input(f"Run migrations? (y/n) ") == "y":
|
||||
if input("Run migrations? (y/n) ") == "y":
|
||||
migrator.run()
|
||||
|
|
|
@ -34,6 +34,7 @@ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|||
from pydantic import BaseModel
|
||||
from pydantic.json import ENCODERS_BY_TYPE
|
||||
|
||||
|
||||
SetIntStr = Set[Union[int, str]]
|
||||
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.model.model import model_registry
|
||||
|
||||
|
||||
redis = get_redis_connection()
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
import importlib # noqa: E402
|
||||
import pkgutil # noqa: E402
|
||||
|
||||
|
||||
class MigrationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def import_submodules(root_module_name: str):
|
||||
"""Import all submodules of a module, recursively."""
|
||||
# TODO: Call this without specifying a module name, to import everything?
|
||||
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(
|
||||
root_module.__path__, root_module.__name__ + '.'):
|
||||
root_module.__path__, root_module.__name__ + "." # type: ignore
|
||||
):
|
||||
importlib.import_module(module_name)
|
||||
|
||||
|
||||
|
@ -77,14 +90,20 @@ class Migrator:
|
|||
except NotImplementedError:
|
||||
log.info("Skipping migrations for %s", name)
|
||||
continue
|
||||
current_hash = hashlib.sha1(schema.encode("utf-8")).hexdigest()
|
||||
current_hash = hashlib.sha1(schema.encode("utf-8")).hexdigest() # nosec
|
||||
|
||||
try:
|
||||
redis.execute_command("ft.info", cls.Meta.index_name)
|
||||
except ResponseError:
|
||||
self.migrations.append(
|
||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
||||
MigrationAction.CREATE))
|
||||
IndexMigration(
|
||||
name,
|
||||
cls.Meta.index_name,
|
||||
schema,
|
||||
current_hash,
|
||||
MigrationAction.CREATE,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
stored_hash = redis.get(hash_key)
|
||||
|
@ -93,11 +112,25 @@ class Migrator:
|
|||
if schema_out_of_date:
|
||||
# TODO: Switch out schema with an alias to avoid downtime -- separate migration?
|
||||
self.migrations.append(
|
||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
||||
MigrationAction.DROP, stored_hash))
|
||||
IndexMigration(
|
||||
name,
|
||||
cls.Meta.index_name,
|
||||
schema,
|
||||
current_hash,
|
||||
MigrationAction.DROP,
|
||||
stored_hash,
|
||||
)
|
||||
)
|
||||
self.migrations.append(
|
||||
IndexMigration(name, cls.Meta.index_name, schema, current_hash,
|
||||
MigrationAction.CREATE, stored_hash))
|
||||
IndexMigration(
|
||||
name,
|
||||
cls.Meta.index_name,
|
||||
schema,
|
||||
current_hash,
|
||||
MigrationAction.CREATE,
|
||||
stored_hash,
|
||||
)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
# TODO: Migration history
|
||||
|
|
|
@ -4,7 +4,7 @@ import decimal
|
|||
import json
|
||||
import logging
|
||||
import operator
|
||||
from copy import deepcopy, copy
|
||||
from copy import copy, deepcopy
|
||||
from enum import Enum
|
||||
from functools import reduce
|
||||
from typing import (
|
||||
|
@ -12,18 +12,19 @@ from typing import (
|
|||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Mapping,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
Sequence,
|
||||
no_type_check,
|
||||
Protocol,
|
||||
List,
|
||||
get_args,
|
||||
get_origin,
|
||||
get_args, Type
|
||||
no_type_check,
|
||||
)
|
||||
|
||||
import redis
|
||||
|
@ -40,6 +41,7 @@ from .encoders import jsonable_encoder
|
|||
from .render_tree import render_tree
|
||||
from .token_escaper import TokenEscaper
|
||||
|
||||
|
||||
model_registry = {}
|
||||
_T = TypeVar("_T")
|
||||
log = logging.getLogger(__name__)
|
||||
|
@ -92,7 +94,7 @@ class Operators(Enum):
|
|||
return str(self.name)
|
||||
|
||||
|
||||
ExpressionOrModelField = Union['Expression', 'NegatedExpression', ModelField]
|
||||
ExpressionOrModelField = Union["Expression", "NegatedExpression", ModelField]
|
||||
|
||||
|
||||
def embedded(cls):
|
||||
|
@ -100,20 +102,22 @@ def embedded(cls):
|
|||
Mark a model as embedded to avoid creating multiple indexes if the model is
|
||||
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:
|
||||
return True
|
||||
unwrapped = get_origin(typ)
|
||||
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():
|
||||
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):
|
||||
|
@ -121,7 +125,7 @@ class ExpressionProtocol(Protocol):
|
|||
left: ExpressionOrModelField
|
||||
right: ExpressionOrModelField
|
||||
|
||||
def __invert__(self) -> 'Expression':
|
||||
def __invert__(self) -> "Expression":
|
||||
pass
|
||||
|
||||
def __and__(self, other: ExpressionOrModelField):
|
||||
|
@ -148,16 +152,21 @@ class NegatedExpression:
|
|||
responsible for querying) to negate the logic in the wrapped Expression. A
|
||||
better design is probably possible, maybe at least an ExpressionProtocol?
|
||||
"""
|
||||
expression: 'Expression'
|
||||
|
||||
expression: "Expression"
|
||||
|
||||
def __invert__(self):
|
||||
return self.expression
|
||||
|
||||
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):
|
||||
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
|
||||
def left(self):
|
||||
|
@ -188,13 +197,15 @@ class Expression:
|
|||
op: Operators
|
||||
left: Optional[ExpressionOrModelField]
|
||||
right: Optional[ExpressionOrModelField]
|
||||
parents: List[Tuple[str, 'RedisModel']]
|
||||
parents: List[Tuple[str, "RedisModel"]]
|
||||
|
||||
def __invert__(self):
|
||||
return NegatedExpression(self)
|
||||
|
||||
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):
|
||||
return Expression(left=self, op=Operators.OR, right=other, parents=self.parents)
|
||||
|
@ -212,41 +223,59 @@ ExpressionOrNegated = Union[Expression, NegatedExpression]
|
|||
|
||||
|
||||
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.parents = parents
|
||||
|
||||
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]
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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):
|
||||
if is_supported_container_type(self.field.outer_type_):
|
||||
embedded_cls = get_args(self.field.outer_type_)
|
||||
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: "
|
||||
"orders: List[Order]. Docs: TODO")
|
||||
"orders: List[Order]. Docs: TODO"
|
||||
)
|
||||
embedded_cls = embedded_cls[0]
|
||||
attr = getattr(embedded_cls, item)
|
||||
else:
|
||||
|
@ -266,10 +295,10 @@ class QueryNotSupportedError(Exception):
|
|||
|
||||
|
||||
class RediSearchFieldTypes(Enum):
|
||||
TEXT = 'TEXT'
|
||||
TAG = 'TAG'
|
||||
NUMERIC = 'NUMERIC'
|
||||
GEO = 'GEO'
|
||||
TEXT = "TEXT"
|
||||
TAG = "TAG"
|
||||
NUMERIC = "NUMERIC"
|
||||
GEO = "GEO"
|
||||
|
||||
|
||||
# TODO: How to handle Geo fields?
|
||||
|
@ -278,13 +307,15 @@ DEFAULT_PAGE_SIZE = 10
|
|||
|
||||
|
||||
class FindQuery:
|
||||
def __init__(self,
|
||||
def __init__(
|
||||
self,
|
||||
expressions: Sequence[ExpressionOrNegated],
|
||||
model: Type['RedisModel'],
|
||||
model: Type["RedisModel"],
|
||||
offset: int = 0,
|
||||
limit: 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.model = model
|
||||
self.offset = offset
|
||||
|
@ -308,7 +339,7 @@ class FindQuery:
|
|||
page_size=self.page_size,
|
||||
limit=self.limit,
|
||||
expressions=copy(self.expressions),
|
||||
sort_fields=copy(self.sort_fields)
|
||||
sort_fields=copy(self.sort_fields),
|
||||
)
|
||||
|
||||
def copy(self, **kwargs):
|
||||
|
@ -330,7 +361,9 @@ class FindQuery:
|
|||
if self.expressions:
|
||||
self._expression = reduce(operator.and_, self.expressions)
|
||||
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
|
||||
|
||||
@property
|
||||
|
@ -350,24 +383,30 @@ class FindQuery:
|
|||
for sort_field in sort_fields:
|
||||
field_name = sort_field.lstrip("-")
|
||||
if field_name not in self.model.__fields__:
|
||||
raise QueryNotSupportedError(f"You tried sort by {field_name}, but that field "
|
||||
f"does not exist on the model {self.model}")
|
||||
raise QueryNotSupportedError(
|
||||
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)
|
||||
if not getattr(field_proxy.field.field_info, 'sortable', False):
|
||||
raise QueryNotSupportedError(f"You tried sort by {field_name}, but {self.model} does "
|
||||
"not define that field as sortable. See docs: XXX")
|
||||
if not getattr(field_proxy.field.field_info, "sortable", False):
|
||||
raise QueryNotSupportedError(
|
||||
f"You tried sort by {field_name}, but {self.model} does "
|
||||
"not define that field as sortable. See docs: XXX"
|
||||
)
|
||||
return sort_fields
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
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
|
||||
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"full_text_search=True option. Docs: TODO")
|
||||
f"full_text_search=True option. Docs: TODO"
|
||||
)
|
||||
return RediSearchFieldTypes.TEXT
|
||||
|
||||
field_type = field.outer_type_
|
||||
|
@ -391,8 +430,10 @@ class FindQuery:
|
|||
# within the model inside the list marked as `index=True`.
|
||||
return RediSearchFieldTypes.TAG
|
||||
elif container_type is not None:
|
||||
raise QuerySyntaxError("Only lists and tuples are supported for multi-value fields. "
|
||||
"See docs: TODO")
|
||||
raise QuerySyntaxError(
|
||||
"Only lists and tuples are supported for multi-value fields. "
|
||||
"See docs: TODO"
|
||||
)
|
||||
elif any(issubclass(field_type, t) for t in NUMERIC_TYPES):
|
||||
# Index numeric Python types as NUMERIC fields, so we can support
|
||||
# range queries.
|
||||
|
@ -419,14 +460,23 @@ class FindQuery:
|
|||
try:
|
||||
return "|".join([escaper.escape(str(v)) for v in value])
|
||||
except TypeError:
|
||||
log.debug("Escaping single non-iterable value used for an IN or "
|
||||
"NOT_IN query: %s", value)
|
||||
log.debug(
|
||||
"Escaping single non-iterable value used for an IN or "
|
||||
"NOT_IN query: %s",
|
||||
value,
|
||||
)
|
||||
return escaper.escape(str(value))
|
||||
|
||||
@classmethod
|
||||
def resolve_value(cls, field_name: str, field_type: RediSearchFieldTypes,
|
||||
field_info: PydanticFieldInfo, op: Operators, value: Any,
|
||||
parents: List[Tuple[str, 'RedisModel']]) -> str:
|
||||
def resolve_value(
|
||||
cls,
|
||||
field_name: str,
|
||||
field_type: RediSearchFieldTypes,
|
||||
field_info: PydanticFieldInfo,
|
||||
op: Operators,
|
||||
value: Any,
|
||||
parents: List[Tuple[str, "RedisModel"]],
|
||||
) -> str:
|
||||
if parents:
|
||||
prefix = "_".join([p[0] for p in parents])
|
||||
field_name = f"{prefix}_{field_name}"
|
||||
|
@ -440,9 +490,11 @@ class FindQuery:
|
|||
elif op is Operators.LIKE:
|
||||
result += value
|
||||
else:
|
||||
raise QueryNotSupportedError("Only equals (=), not-equals (!=), and like() "
|
||||
raise QueryNotSupportedError(
|
||||
"Only equals (=), not-equals (!=), and like() "
|
||||
"comparisons are supported for TEXT fields. See "
|
||||
"docs: TODO.")
|
||||
"docs: TODO."
|
||||
)
|
||||
elif field_type is RediSearchFieldTypes.NUMERIC:
|
||||
if op is Operators.EQ:
|
||||
result += f"@{field_name}:[{value} {value}]"
|
||||
|
@ -460,16 +512,22 @@ class FindQuery:
|
|||
# field and our hidden use of TAG for exact-match queries?
|
||||
elif field_type is RediSearchFieldTypes.TAG:
|
||||
if op is Operators.EQ:
|
||||
separator_char = getattr(field_info, 'separator',
|
||||
SINGLE_VALUE_TAG_FIELD_SEPARATOR)
|
||||
separator_char = getattr(
|
||||
field_info, "separator", SINGLE_VALUE_TAG_FIELD_SEPARATOR
|
||||
)
|
||||
if value == separator_char:
|
||||
# The value is ONLY the TAG field separator character --
|
||||
# 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 "
|
||||
"this portion of the query. Please review your query to find "
|
||||
"an alternative query that uses a string containing more than "
|
||||
"just the character %s.", field_name, separator_char, separator_char)
|
||||
"just the character %s.",
|
||||
field_name,
|
||||
separator_char,
|
||||
separator_char,
|
||||
)
|
||||
return ""
|
||||
if separator_char in value:
|
||||
# The value contains the TAG field separator. We can work
|
||||
|
@ -506,8 +564,8 @@ class FindQuery:
|
|||
return
|
||||
fields = []
|
||||
for f in self.sort_fields:
|
||||
direction = "desc" if f.startswith('-') else 'asc'
|
||||
fields.extend([f.lstrip('-'), direction])
|
||||
direction = "desc" if f.startswith("-") else "asc"
|
||||
fields.extend([f.lstrip("-"), direction])
|
||||
if self.sort_fields:
|
||||
return ["SORTBY", *fields]
|
||||
|
||||
|
@ -550,23 +608,30 @@ class FindQuery:
|
|||
if encompassing_expression_is_negated:
|
||||
# TODO: Is there a use case for this, perhaps for dynamic
|
||||
# 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 "*"
|
||||
|
||||
if isinstance(expression.left, Expression) or \
|
||||
isinstance(expression.left, NegatedExpression):
|
||||
if isinstance(expression.left, Expression) or isinstance(
|
||||
expression.left, NegatedExpression
|
||||
):
|
||||
result += f"({cls.resolve_redisearch_query(expression.left)})"
|
||||
elif isinstance(expression.left, ModelField):
|
||||
field_type = cls.resolve_field_type(expression.left, expression.op)
|
||||
field_name = expression.left.name
|
||||
field_info = expression.left.field_info
|
||||
if not field_info or not getattr(field_info, "index", None):
|
||||
raise QueryNotSupportedError(f"You tried to query by a field ({field_name}) "
|
||||
f"that isn't indexed. See docs: TODO")
|
||||
raise QueryNotSupportedError(
|
||||
f"You tried to query by a field ({field_name}) "
|
||||
f"that isn't indexed. See docs: TODO"
|
||||
)
|
||||
else:
|
||||
raise QueryNotSupportedError(f"A query expression should start with either a field "
|
||||
f"or an expression enclosed in parenthesis. See docs: "
|
||||
f"TODO")
|
||||
raise QueryNotSupportedError(
|
||||
"A query expression should start with either a field "
|
||||
"or an expression enclosed in parenthesis. See docs: "
|
||||
"TODO"
|
||||
)
|
||||
|
||||
right = expression.right
|
||||
|
||||
|
@ -576,8 +641,10 @@ class FindQuery:
|
|||
elif expression.op == Operators.OR:
|
||||
result += "| "
|
||||
else:
|
||||
raise QueryNotSupportedError("You can only combine two query expressions with"
|
||||
"AND (&) or OR (|). See docs: TODO")
|
||||
raise QueryNotSupportedError(
|
||||
"You can only combine two query expressions with"
|
||||
"AND (&) or OR (|). See docs: TODO"
|
||||
)
|
||||
|
||||
if isinstance(right, NegatedExpression):
|
||||
result += "-"
|
||||
|
@ -594,10 +661,18 @@ class FindQuery:
|
|||
elif not field_info:
|
||||
raise QuerySyntaxError("Could not resolve field info. See docs: TODO")
|
||||
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:
|
||||
result += cls.resolve_value(field_name, field_type, field_info,
|
||||
expression.op, right, expression.parents)
|
||||
result += cls.resolve_value(
|
||||
field_name,
|
||||
field_type,
|
||||
field_info,
|
||||
expression.op,
|
||||
right,
|
||||
expression.parents,
|
||||
)
|
||||
|
||||
if encompassing_expression_is_negated:
|
||||
result = f"-({result})"
|
||||
|
@ -658,7 +733,7 @@ class FindQuery:
|
|||
return self
|
||||
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.
|
||||
|
||||
|
@ -672,11 +747,14 @@ class FindQuery:
|
|||
for model in self.all():
|
||||
for field, value in field_values.items():
|
||||
setattr(model, field, value)
|
||||
# TODO: In the non-transaction case, can we do more to detect
|
||||
# failure responses from Redis?
|
||||
model.save(pipeline=pipeline)
|
||||
|
||||
if pipeline:
|
||||
# TODO: Better response type, error detection
|
||||
return pipeline.execute()
|
||||
# TODO: Response type?
|
||||
# TODO: Better error detection for transactions.
|
||||
pipeline.execute()
|
||||
|
||||
def delete(self):
|
||||
"""Delete all matching records in this query."""
|
||||
|
@ -722,6 +800,7 @@ class UlidPrimaryKey:
|
|||
"""A client-side generated primary key that follows the ULID spec.
|
||||
https://github.com/ulid/javascript#specification
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def create_pk(*args, **kwargs) -> str:
|
||||
return str(ULID())
|
||||
|
@ -848,6 +927,7 @@ class DefaultMeta:
|
|||
TODO: Revisit whether this is really necessary, and whether making
|
||||
these all optional here is the right choice.
|
||||
"""
|
||||
|
||||
global_key_prefix: Optional[str] = None
|
||||
model_key_prefix: Optional[str] = None
|
||||
primary_key_pattern: Optional[str] = None
|
||||
|
@ -863,28 +943,32 @@ class ModelMeta(ModelMetaclass):
|
|||
_meta: MetaProtocol
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
# we might have inherited _meta from a parent class, and should
|
||||
# therefore use some of the inherited fields.
|
||||
meta = meta or getattr(new_class, 'Meta', None)
|
||||
base_meta = getattr(new_class, '_meta', None)
|
||||
meta = meta or getattr(new_class, "Meta", None)
|
||||
base_meta = getattr(new_class, "_meta", None)
|
||||
|
||||
if meta and meta != DefaultMeta and meta != base_meta:
|
||||
new_class.Meta = meta
|
||||
new_class._meta = 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
|
||||
# Unset inherited values we don't want to reuse (typically based on
|
||||
# the model name).
|
||||
new_class._meta.model_key_prefix = None
|
||||
new_class._meta.index_name = None
|
||||
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
|
||||
|
||||
# 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.
|
||||
if isinstance(field.field_info, FieldInfo):
|
||||
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):
|
||||
new_class._meta.global_key_prefix = getattr(base_meta, "global_key_prefix", "")
|
||||
if not getattr(new_class._meta, 'model_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", ""
|
||||
)
|
||||
if not getattr(new_class._meta, "model_key_prefix", None):
|
||||
# Don't look at the base class for this.
|
||||
new_class._meta.model_key_prefix = f"{new_class.__module__}.{new_class.__name__}"
|
||||
if not getattr(new_class._meta, 'primary_key_pattern', None):
|
||||
new_class._meta.primary_key_pattern = getattr(base_meta, "primary_key_pattern",
|
||||
"{pk}")
|
||||
if not getattr(new_class._meta, 'database', None):
|
||||
new_class._meta.database = getattr(base_meta, "database",
|
||||
redis.Redis(decode_responses=True))
|
||||
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}:" \
|
||||
new_class._meta.model_key_prefix = (
|
||||
f"{new_class.__module__}.{new_class.__name__}"
|
||||
)
|
||||
if not getattr(new_class._meta, "primary_key_pattern", None):
|
||||
new_class._meta.primary_key_pattern = getattr(
|
||||
base_meta, "primary_key_pattern", "{pk}"
|
||||
)
|
||||
if not getattr(new_class._meta, "database", None):
|
||||
new_class._meta.database = getattr(
|
||||
base_meta, "database", redis.Redis(decode_responses=True)
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
||||
# Not an abstract model class or embedded model, so we should let the
|
||||
# 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__}"
|
||||
model_registry[key] = new_class
|
||||
|
||||
|
@ -931,7 +1026,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
class Config:
|
||||
orm_mode = True
|
||||
arbitrary_types_allowed = True
|
||||
extra = 'allow'
|
||||
extra = "allow"
|
||||
|
||||
def __init__(__pydantic_self__, **data: Any) -> None:
|
||||
super().__init__(**data)
|
||||
|
@ -953,7 +1048,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
"""Update this model instance with the specified key-value pairs."""
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, *args, **kwargs) -> 'RedisModel':
|
||||
def save(self, pipeline: Optional[Pipeline] = None) -> "RedisModel":
|
||||
raise NotImplementedError
|
||||
|
||||
@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)."""
|
||||
primary_keys = 0
|
||||
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
|
||||
if primary_keys == 0:
|
||||
raise RedisModelError("You must define a primary key for the model")
|
||||
|
@ -976,8 +1071,8 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
|
||||
@classmethod
|
||||
def make_key(cls, part: str):
|
||||
global_prefix = getattr(cls._meta, 'global_key_prefix', '').strip(":")
|
||||
model_prefix = getattr(cls._meta, 'model_key_prefix', '').strip(":")
|
||||
global_prefix = getattr(cls._meta, "global_key_prefix", "").strip(":")
|
||||
model_prefix = getattr(cls._meta, "model_key_prefix", "").strip(":")
|
||||
return f"{global_prefix}:{model_prefix}:{part}"
|
||||
|
||||
@classmethod
|
||||
|
@ -997,13 +1092,14 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
def from_redis(cls, res: Any):
|
||||
# TODO: Parsing logic copied from redisearch-py. Evaluate.
|
||||
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):
|
||||
if isinstance(s, six.string_types):
|
||||
return s
|
||||
elif isinstance(s, six.binary_type):
|
||||
return s.decode('utf-8', 'ignore')
|
||||
return s.decode("utf-8", "ignore")
|
||||
else:
|
||||
return s # Not a string we care about
|
||||
|
||||
|
@ -1015,23 +1111,27 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
fields_offset = offset
|
||||
|
||||
fields = dict(
|
||||
dict(izip(map(to_string, res[i + fields_offset][::2]),
|
||||
map(to_string, res[i + fields_offset][1::2])))
|
||||
dict(
|
||||
izip(
|
||||
map(to_string, res[i + fields_offset][::2]),
|
||||
map(to_string, res[i + fields_offset][1::2]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
del fields['id']
|
||||
del fields["id"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
fields['json'] = fields['$']
|
||||
del fields['$']
|
||||
fields["json"] = fields["$"]
|
||||
del fields["$"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if 'json' in fields:
|
||||
json_fields = json.loads(fields['json'])
|
||||
if "json" in fields:
|
||||
json_fields = json.loads(fields["json"])
|
||||
doc = cls(**json_fields)
|
||||
else:
|
||||
doc = cls(**fields)
|
||||
|
@ -1039,7 +1139,7 @@ class RedisModel(BaseModel, abc.ABC, metaclass=ModelMeta):
|
|||
return docs
|
||||
|
||||
@classmethod
|
||||
def add(cls, models: Sequence['RedisModel']) -> Sequence['RedisModel']:
|
||||
def add(cls, models: Sequence["RedisModel"]) -> Sequence["RedisModel"]:
|
||||
# TODO: Add transaction support
|
||||
return [model.save() for model in models]
|
||||
|
||||
|
@ -1059,15 +1159,18 @@ class HashModel(RedisModel, abc.ABC):
|
|||
|
||||
for name, field in cls.__fields__.items():
|
||||
if issubclass(field.outer_type_, RedisModel):
|
||||
raise RedisModelError(f"HashModels cannot have embedded model "
|
||||
f"fields. Field: {name}")
|
||||
raise RedisModelError(
|
||||
f"HashModels cannot have embedded model " f"fields. Field: {name}"
|
||||
)
|
||||
|
||||
for typ in (Set, Mapping, List):
|
||||
if issubclass(field.outer_type_, typ):
|
||||
raise RedisModelError(f"HashModels cannot have set, list,"
|
||||
f" or mapping fields. Field: {name}")
|
||||
raise RedisModelError(
|
||||
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:
|
||||
db = self.db()
|
||||
else:
|
||||
|
@ -1077,7 +1180,7 @@ class HashModel(RedisModel, abc.ABC):
|
|||
return self
|
||||
|
||||
@classmethod
|
||||
def get(cls, pk: Any) -> 'HashModel':
|
||||
def get(cls, pk: Any) -> "HashModel":
|
||||
document = cls.db().hgetall(cls.make_primary_key(pk))
|
||||
if not document:
|
||||
raise NotFoundError
|
||||
|
@ -1111,13 +1214,17 @@ class HashModel(RedisModel, abc.ABC):
|
|||
for name, field in cls.__fields__.items():
|
||||
# TODO: Merge this code with schema_for_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):
|
||||
redisearch_field = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||
redisearch_field = (
|
||||
f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||
)
|
||||
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)
|
||||
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))
|
||||
elif is_supported_container_type(_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)
|
||||
continue
|
||||
embedded_cls = embedded_cls[0]
|
||||
schema_parts.append(cls.schema_for_type(name, embedded_cls,
|
||||
field.field_info))
|
||||
schema_parts.append(
|
||||
cls.schema_for_type(name, embedded_cls, field.field_info)
|
||||
)
|
||||
elif issubclass(_type, RedisModel):
|
||||
schema_parts.append(cls.schema_for_type(name, _type, field.field_info))
|
||||
return schema_parts
|
||||
|
@ -1141,29 +1249,36 @@ class HashModel(RedisModel, abc.ABC):
|
|||
# as sortable.
|
||||
# TODO: Abstract string-building logic for each type (TAG, etc.) into
|
||||
# classes that take a field name.
|
||||
sortable = getattr(field_info, 'sortable', False)
|
||||
sortable = getattr(field_info, "sortable", False)
|
||||
|
||||
if is_supported_container_type(typ):
|
||||
embedded_cls = get_args(typ)
|
||||
if not embedded_cls:
|
||||
# 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 ""
|
||||
embedded_cls = embedded_cls[0]
|
||||
schema = cls.schema_for_type(name, embedded_cls, field_info)
|
||||
elif any(issubclass(typ, t) for t in NUMERIC_TYPES):
|
||||
schema = f"{name} NUMERIC"
|
||||
elif issubclass(typ, str):
|
||||
if getattr(field_info, 'full_text_search', False) is True:
|
||||
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} " \
|
||||
if getattr(field_info, "full_text_search", False) is True:
|
||||
schema = (
|
||||
f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR} "
|
||||
f"{name}_fts TEXT"
|
||||
)
|
||||
else:
|
||||
schema = f"{name} TAG SEPARATOR {SINGLE_VALUE_TAG_FIELD_SEPARATOR}"
|
||||
elif issubclass(typ, RedisModel):
|
||||
sub_fields = []
|
||||
for embedded_name, field in typ.__fields__.items():
|
||||
sub_fields.append(cls.schema_for_type(f"{name}_{embedded_name}", field.outer_type_,
|
||||
field.field_info))
|
||||
sub_fields.append(
|
||||
cls.schema_for_type(
|
||||
f"{name}_{embedded_name}", field.outer_type_, field.field_info
|
||||
)
|
||||
)
|
||||
schema = " ".join(sub_fields)
|
||||
else:
|
||||
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.
|
||||
cls.redisearch_schema()
|
||||
|
||||
def save(self, pipeline: Optional[Pipeline] = None) -> 'JsonModel':
|
||||
def save(self, pipeline: Optional[Pipeline] = None) -> "JsonModel":
|
||||
if pipeline is None:
|
||||
db = self.db()
|
||||
else:
|
||||
db = pipeline
|
||||
db.execute_command('JSON.SET', self.key(), ".", self.json())
|
||||
db.execute_command("JSON.SET", self.key(), ".", self.json())
|
||||
return self
|
||||
|
||||
def update(self, **field_values):
|
||||
|
@ -1192,7 +1307,7 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
self.save()
|
||||
|
||||
@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))
|
||||
if not document:
|
||||
raise NotFoundError
|
||||
|
@ -1212,21 +1327,31 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
|
||||
for name, field in cls.__fields__.items():
|
||||
_type = field.outer_type_
|
||||
schema_parts.append(cls.schema_for_type(
|
||||
json_path, name, "", _type, field.field_info))
|
||||
schema_parts.append(
|
||||
cls.schema_for_type(json_path, name, "", _type, field.field_info)
|
||||
)
|
||||
return schema_parts
|
||||
|
||||
@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,
|
||||
parent_type: Optional[Any] = None) -> str:
|
||||
should_index = getattr(field_info, 'index', False)
|
||||
parent_type: Optional[Any] = None,
|
||||
) -> str:
|
||||
should_index = getattr(field_info, "index", False)
|
||||
is_container_type = is_supported_container_type(typ)
|
||||
parent_is_container_type = is_supported_container_type(parent_type)
|
||||
parent_is_model = False
|
||||
|
||||
if parent_type:
|
||||
try:
|
||||
parent_is_model = issubclass(parent_type, RedisModel)
|
||||
except TypeError:
|
||||
parent_is_model = False
|
||||
pass
|
||||
|
||||
# TODO: We need a better way to know that we're indexing a value
|
||||
# discovered in a model within an array.
|
||||
|
@ -1253,11 +1378,19 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
field_type = get_origin(typ)
|
||||
embedded_cls = get_args(typ)
|
||||
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 ""
|
||||
embedded_cls = embedded_cls[0]
|
||||
return cls.schema_for_type(f"{json_path}.{name}[*]", name, name_prefix,
|
||||
embedded_cls, field_info, parent_type=field_type)
|
||||
return cls.schema_for_type(
|
||||
f"{json_path}.{name}[*]",
|
||||
name,
|
||||
name_prefix,
|
||||
embedded_cls,
|
||||
field_info,
|
||||
parent_type=field_type,
|
||||
)
|
||||
elif field_is_model:
|
||||
name_prefix = f"{name_prefix}_{name}" if name_prefix else name
|
||||
sub_fields = []
|
||||
|
@ -1273,12 +1406,16 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
# current field name and "embedded" field name, e.g.,
|
||||
# order.address.street_line_1.
|
||||
path = f"{json_path}.{name}"
|
||||
sub_fields.append(cls.schema_for_type(path,
|
||||
sub_fields.append(
|
||||
cls.schema_for_type(
|
||||
path,
|
||||
embedded_name,
|
||||
name_prefix,
|
||||
field.outer_type_,
|
||||
field.field_info,
|
||||
parent_type=typ))
|
||||
parent_type=typ,
|
||||
)
|
||||
)
|
||||
return " ".join(filter(None, sub_fields))
|
||||
# NOTE: This is the termination point for recursion. We've descended
|
||||
# into models and lists until we found an actual value to index.
|
||||
|
@ -1291,20 +1428,26 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
path = json_path
|
||||
else:
|
||||
path = f"{json_path}.{name}"
|
||||
sortable = getattr(field_info, 'sortable', False)
|
||||
full_text_search = getattr(field_info, 'full_text_search', False)
|
||||
sortable_tag_error = RedisModelError("In this Preview release, TAG fields cannot "
|
||||
sortable = getattr(field_info, "sortable", False)
|
||||
full_text_search = getattr(field_info, "full_text_search", False)
|
||||
sortable_tag_error = RedisModelError(
|
||||
"In this Preview release, TAG fields cannot "
|
||||
f"be marked as sortable. Problem field: {name}. "
|
||||
"See docs: TODO")
|
||||
"See docs: TODO"
|
||||
)
|
||||
|
||||
# TODO: GEO field
|
||||
if parent_is_container_type or parent_is_model_in_container:
|
||||
if typ is not str:
|
||||
raise RedisModelError("In this Preview release, list and tuple fields can only "
|
||||
f"contain strings. Problem field: {name}. See docs: TODO")
|
||||
raise RedisModelError(
|
||||
"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:
|
||||
raise RedisModelError("List and tuple fields cannot be indexed for full-text "
|
||||
f"search. Problem field: {name}. See docs: TODO")
|
||||
raise RedisModelError(
|
||||
"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}"
|
||||
if sortable is True:
|
||||
raise sortable_tag_error
|
||||
|
@ -1312,8 +1455,10 @@ class JsonModel(RedisModel, abc.ABC):
|
|||
schema = f"{path} AS {index_field_name} NUMERIC"
|
||||
elif issubclass(typ, str):
|
||||
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"
|
||||
)
|
||||
if sortable is True:
|
||||
# NOTE: With the current preview release, making a field
|
||||
# full-text searchable and sortable only makes the TEXT
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import abc
|
||||
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):
|
||||
|
@ -22,6 +22,7 @@ class BaseHashModel(HashModel, abc.ABC):
|
|||
# postal_code: str
|
||||
#
|
||||
|
||||
|
||||
class AddressHash(BaseHashModel):
|
||||
address_line_1: str
|
||||
address_line_2: Optional[str]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
||||
|
@ -91,6 +91,7 @@ class Not(LogicalOperatorForListOfExpressions):
|
|||
-(@price:[-inf 10]) -(@category:{Sweets})
|
||||
```
|
||||
"""
|
||||
|
||||
@property
|
||||
def query(self):
|
||||
return "-(expression1) -(expression2)"
|
||||
|
@ -102,5 +103,3 @@ class QueryResolver:
|
|||
|
||||
def resolve(self) -> str:
|
||||
"""Resolve expressions to a RediSearch query string."""
|
||||
|
||||
|
||||
|
|
|
@ -5,9 +5,15 @@ and released under the MIT license: https://github.com/clemtoy/pptree
|
|||
import io
|
||||
|
||||
|
||||
def render_tree(current_node, nameattr='name', left_child='left',
|
||||
right_child='right', indent='', last='updown',
|
||||
buffer=None):
|
||||
def render_tree(
|
||||
current_node,
|
||||
nameattr="name",
|
||||
left_child="left",
|
||||
right_child="right",
|
||||
indent="",
|
||||
last="updown",
|
||||
buffer=None,
|
||||
):
|
||||
"""Print a tree-like structure, `current_node`.
|
||||
|
||||
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:
|
||||
buffer = io.StringIO()
|
||||
if hasattr(current_node, nameattr):
|
||||
name = lambda node: getattr(node, nameattr)
|
||||
name = lambda node: getattr(node, nameattr) # noqa: E731
|
||||
else:
|
||||
name = lambda node: str(node)
|
||||
name = lambda node: str(node) # noqa: E731
|
||||
|
||||
up = getattr(current_node, left_child, None)
|
||||
down = getattr(current_node, right_child, None)
|
||||
|
||||
if up is not None:
|
||||
next_last = 'up'
|
||||
next_indent = '{0}{1}{2}'.format(indent, ' ' if 'up' in last else '|', ' ' * len(str(name(current_node))))
|
||||
render_tree(up, nameattr, left_child, right_child, next_indent, next_last, buffer)
|
||||
next_last = "up"
|
||||
next_indent = "{0}{1}{2}".format(
|
||||
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':
|
||||
start_shape = '┌'
|
||||
elif last == 'down':
|
||||
start_shape = '└'
|
||||
elif last == 'updown':
|
||||
start_shape = ' '
|
||||
if last == "up":
|
||||
start_shape = "┌"
|
||||
elif last == "down":
|
||||
start_shape = "└"
|
||||
elif last == "updown":
|
||||
start_shape = " "
|
||||
else:
|
||||
start_shape = '├'
|
||||
start_shape = "├"
|
||||
|
||||
if up is not None and down is not None:
|
||||
end_shape = '┤'
|
||||
end_shape = "┤"
|
||||
elif up:
|
||||
end_shape = '┘'
|
||||
end_shape = "┘"
|
||||
elif down:
|
||||
end_shape = '┐'
|
||||
end_shape = "┐"
|
||||
else:
|
||||
end_shape = ''
|
||||
end_shape = ""
|
||||
|
||||
print('{0}{1}{2}{3}'.format(indent, start_shape, name(current_node), end_shape),
|
||||
file=buffer)
|
||||
print(
|
||||
"{0}{1}{2}{3}".format(indent, start_shape, name(current_node), end_shape),
|
||||
file=buffer,
|
||||
)
|
||||
|
||||
if down is not None:
|
||||
next_last = 'down'
|
||||
next_indent = '{0}{1}{2}'.format(indent, ' ' if 'down' in last else '|', ' ' * len(str(name(current_node))))
|
||||
render_tree(down, nameattr, left_child, right_child, next_indent, next_last, buffer)
|
||||
next_last = "down"
|
||||
next_indent = "{0}{1}{2}".format(
|
||||
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()}"
|
|
@ -6,6 +6,7 @@ class TokenEscaper:
|
|||
"""
|
||||
Escape punctuation within an input string.
|
||||
"""
|
||||
|
||||
# Characters that RediSearch requires us to escape during queries.
|
||||
# Source: https://oss.redis.com/redisearch/Escaping/#the_rules_of_text_field_tokenization
|
||||
DEFAULT_ESCAPED_CHARS = r"[,.<>{}\[\]\\\"\':;!@#$%^&*()\-+=~\ ]"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import abc
|
||||
import decimal
|
||||
import datetime
|
||||
import decimal
|
||||
from typing import Optional
|
||||
from unittest import mock
|
||||
|
||||
|
@ -8,11 +8,13 @@ import pytest
|
|||
import redis
|
||||
from pydantic import ValidationError
|
||||
|
||||
from redis_developer.model import (
|
||||
HashModel,
|
||||
Field,
|
||||
from redis_developer.model import Field, HashModel
|
||||
from redis_developer.model.model import (
|
||||
NotFoundError,
|
||||
QueryNotSupportedError,
|
||||
RedisModelError,
|
||||
)
|
||||
from redis_developer.model.model import RedisModelError, QueryNotSupportedError, NotFoundError
|
||||
|
||||
|
||||
r = redis.Redis()
|
||||
today = datetime.date.today()
|
||||
|
@ -48,7 +50,7 @@ def members():
|
|||
last_name="Brookins",
|
||||
email="a@example.com",
|
||||
age=38,
|
||||
join_date=today
|
||||
join_date=today,
|
||||
)
|
||||
|
||||
member2 = Member(
|
||||
|
@ -56,7 +58,7 @@ def members():
|
|||
last_name="Brookins",
|
||||
email="k@example.com",
|
||||
age=34,
|
||||
join_date=today
|
||||
join_date=today,
|
||||
)
|
||||
|
||||
member3 = Member(
|
||||
|
@ -64,7 +66,7 @@ def members():
|
|||
last_name="Smith",
|
||||
email="as@example.com",
|
||||
age=100,
|
||||
join_date=today
|
||||
join_date=today,
|
||||
)
|
||||
member1.save()
|
||||
member2.save()
|
||||
|
@ -76,21 +78,13 @@ def members():
|
|||
def test_validates_required_fields():
|
||||
# Raises ValidationError: last_name is required
|
||||
with pytest.raises(ValidationError):
|
||||
Member(
|
||||
first_name="Andrew",
|
||||
zipcode="97086",
|
||||
join_date=today
|
||||
)
|
||||
Member(first_name="Andrew", zipcode="97086", join_date=today)
|
||||
|
||||
|
||||
def test_validates_field():
|
||||
# Raises ValidationError: join_date is not a date
|
||||
with pytest.raises(ValidationError):
|
||||
Member(
|
||||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
join_date="yesterday"
|
||||
)
|
||||
Member(first_name="Andrew", last_name="Brookins", join_date="yesterday")
|
||||
|
||||
|
||||
# Passes validation
|
||||
|
@ -100,7 +94,7 @@ def test_validation_passes():
|
|||
last_name="Brookins",
|
||||
email="a@example.com",
|
||||
join_date=today,
|
||||
age=38
|
||||
age=38,
|
||||
)
|
||||
assert member.first_name == "Andrew"
|
||||
|
||||
|
@ -111,7 +105,7 @@ def test_saves_model_and_creates_pk():
|
|||
last_name="Brookins",
|
||||
email="a@example.com",
|
||||
join_date=today,
|
||||
age=38
|
||||
age=38,
|
||||
)
|
||||
# Save a model instance to Redis
|
||||
member.save()
|
||||
|
@ -129,6 +123,7 @@ def test_raises_error_with_embedded_models():
|
|||
postal_code: str
|
||||
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class InvalidMember(BaseHashModel):
|
||||
address: Address
|
||||
|
||||
|
@ -140,14 +135,14 @@ def test_saves_many():
|
|||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
email="a@example.com",
|
||||
join_date=today
|
||||
join_date=today,
|
||||
),
|
||||
Member(
|
||||
first_name="Kim",
|
||||
last_name="Brookins",
|
||||
email="k@example.com",
|
||||
join_date=today
|
||||
)
|
||||
join_date=today,
|
||||
),
|
||||
]
|
||||
Member.add(members)
|
||||
|
||||
|
@ -174,21 +169,21 @@ def test_paginate_query(members):
|
|||
|
||||
def test_access_result_by_index_cached(members):
|
||||
member1, member2, member3 = members
|
||||
query = Member.find().sort_by('age')
|
||||
query = Member.find().sort_by("age")
|
||||
# Load the cache, throw away the result.
|
||||
assert query._model_cache == []
|
||||
query.execute()
|
||||
assert query._model_cache == [member2, member1, member3]
|
||||
|
||||
# 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 not mock_db.called
|
||||
|
||||
|
||||
def test_access_result_by_index_not_cached(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
|
||||
# haven't made any requests of Redis.
|
||||
|
@ -205,7 +200,8 @@ def test_exact_match_queries(members):
|
|||
assert actual == [member1, member2]
|
||||
|
||||
actual = Member.find(
|
||||
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")).all()
|
||||
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")
|
||||
).all()
|
||||
assert actual == [member2]
|
||||
|
||||
actual = Member.find(~(Member.last_name == "Brookins")).all()
|
||||
|
@ -220,16 +216,19 @@ def test_exact_match_queries(members):
|
|||
).all()
|
||||
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]
|
||||
|
||||
|
||||
def test_recursive_query_resolution(members):
|
||||
member1, member2, member3 = members
|
||||
|
||||
actual = Member.find((Member.last_name == "Brookins") | (
|
||||
Member.age == 100
|
||||
) & (Member.last_name == "Smith")).all()
|
||||
actual = Member.find(
|
||||
(Member.last_name == "Brookins")
|
||||
| (Member.age == 100) & (Member.last_name == "Smith")
|
||||
).all()
|
||||
assert actual == [member1, member2, member3]
|
||||
|
||||
|
||||
|
@ -237,8 +236,9 @@ def test_tag_queries_boolean_logic(members):
|
|||
member1, member2, member3 = members
|
||||
|
||||
actual = Member.find(
|
||||
(Member.first_name == "Andrew") &
|
||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith")).all()
|
||||
(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||
| (Member.last_name == "Smith")
|
||||
).all()
|
||||
assert actual == [member1, member3]
|
||||
|
||||
|
||||
|
@ -281,9 +281,7 @@ def test_tag_queries_negation(members):
|
|||
└Andrew
|
||||
|
||||
"""
|
||||
query = Member.find(
|
||||
~(Member.first_name == "Andrew")
|
||||
)
|
||||
query = Member.find(~(Member.first_name == "Andrew"))
|
||||
assert query.all() == [member2]
|
||||
|
||||
"""
|
||||
|
@ -315,8 +313,9 @@ def test_tag_queries_negation(members):
|
|||
└Smith
|
||||
"""
|
||||
query = Member.find(
|
||||
~(Member.first_name == "Andrew") &
|
||||
((Member.last_name == "Brookins") | (Member.last_name == "Smith")))
|
||||
~(Member.first_name == "Andrew")
|
||||
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||
)
|
||||
assert query.all() == [member2]
|
||||
|
||||
"""
|
||||
|
@ -333,12 +332,14 @@ def test_tag_queries_negation(members):
|
|||
└Smith
|
||||
"""
|
||||
query = Member.find(
|
||||
~(Member.first_name == "Andrew") &
|
||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||
| (Member.last_name == "Smith")
|
||||
)
|
||||
assert query.all() == [member2, member3]
|
||||
|
||||
actual = Member.find(
|
||||
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")).all()
|
||||
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
|
||||
).all()
|
||||
assert actual == [member3]
|
||||
|
||||
|
||||
|
@ -373,19 +374,19 @@ def test_numeric_queries(members):
|
|||
def test_sorting(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]
|
||||
|
||||
actual = Member.find(Member.age > 34).sort_by('-age').all()
|
||||
actual = Member.find(Member.age > 34).sort_by("-age").all()
|
||||
assert actual == [member3, member1]
|
||||
|
||||
with pytest.raises(QueryNotSupportedError):
|
||||
# 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):
|
||||
# This field is not sortable.
|
||||
Member.find().sort_by('join_date').all()
|
||||
Member.find().sort_by("join_date").all()
|
||||
|
||||
|
||||
def test_not_found():
|
||||
|
@ -403,4 +404,7 @@ def test_schema():
|
|||
another_integer: int
|
||||
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 decimal
|
||||
import datetime
|
||||
from typing import Optional, List
|
||||
import decimal
|
||||
from typing import List, Optional
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import redis
|
||||
from pydantic import ValidationError
|
||||
|
||||
from redis_developer.model import (
|
||||
EmbeddedJsonModel,
|
||||
JsonModel,
|
||||
Field,
|
||||
)
|
||||
from redis_developer.model import EmbeddedJsonModel, Field, JsonModel
|
||||
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()
|
||||
today = datetime.date.today()
|
||||
|
@ -75,7 +76,7 @@ def address():
|
|||
city="Portland",
|
||||
state="OR",
|
||||
country="USA",
|
||||
postal_code=11111
|
||||
postal_code=11111,
|
||||
)
|
||||
|
||||
|
||||
|
@ -87,7 +88,7 @@ def members(address):
|
|||
email="a@example.com",
|
||||
age=38,
|
||||
join_date=today,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
|
||||
member2 = Member(
|
||||
|
@ -96,7 +97,7 @@ def members(address):
|
|||
email="k@example.com",
|
||||
age=34,
|
||||
join_date=today,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
|
||||
member3 = Member(
|
||||
|
@ -105,7 +106,7 @@ def members(address):
|
|||
email="as@example.com",
|
||||
age=100,
|
||||
join_date=today,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
|
||||
member1.save()
|
||||
|
@ -133,7 +134,7 @@ def test_validates_field(address):
|
|||
first_name="Andrew",
|
||||
last_name="Brookins",
|
||||
join_date="yesterday",
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
|
||||
|
||||
|
@ -145,7 +146,7 @@ def test_validation_passes(address):
|
|||
email="a@example.com",
|
||||
join_date=today,
|
||||
age=38,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
assert member.first_name == "Andrew"
|
||||
|
||||
|
@ -157,7 +158,7 @@ def test_saves_model_and_creates_pk(address):
|
|||
email="a@example.com",
|
||||
join_date=today,
|
||||
age=38,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
# Save a model instance to Redis
|
||||
member.save()
|
||||
|
@ -176,7 +177,7 @@ def test_saves_many(address):
|
|||
email="a@example.com",
|
||||
join_date=today,
|
||||
address=address,
|
||||
age=38
|
||||
age=38,
|
||||
),
|
||||
Member(
|
||||
first_name="Kim",
|
||||
|
@ -184,8 +185,8 @@ def test_saves_many(address):
|
|||
email="k@example.com",
|
||||
join_date=today,
|
||||
address=address,
|
||||
age=34
|
||||
)
|
||||
age=34,
|
||||
),
|
||||
]
|
||||
Member.add(members)
|
||||
|
||||
|
@ -216,21 +217,21 @@ def test_paginate_query(members):
|
|||
|
||||
def test_access_result_by_index_cached(members):
|
||||
member1, member2, member3 = members
|
||||
query = Member.find().sort_by('age')
|
||||
query = Member.find().sort_by("age")
|
||||
# Load the cache, throw away the result.
|
||||
assert query._model_cache == []
|
||||
query.execute()
|
||||
assert query._model_cache == [member2, member1, member3]
|
||||
|
||||
# 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 not mock_db.called
|
||||
|
||||
|
||||
def test_access_result_by_index_not_cached(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
|
||||
# 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(
|
||||
first_name="Bobby"
|
||||
)
|
||||
actual = Member.find(
|
||||
Member.pk << [member1.pk, member2.pk, member3.pk]).sort_by('age').all()
|
||||
actual = (
|
||||
Member.find(Member.pk << [member1.pk, member2.pk, member3.pk])
|
||||
.sort_by("age")
|
||||
.all()
|
||||
)
|
||||
assert actual == [member1, member2, member3]
|
||||
assert all([m.name == "Bobby" for m in actual])
|
||||
|
||||
|
@ -265,7 +269,8 @@ def test_exact_match_queries(members):
|
|||
assert actual == [member1, member2]
|
||||
|
||||
actual = Member.find(
|
||||
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")).all()
|
||||
(Member.last_name == "Brookins") & ~(Member.first_name == "Andrew")
|
||||
).all()
|
||||
assert actual == [member2]
|
||||
|
||||
actual = Member.find(~(Member.last_name == "Brookins")).all()
|
||||
|
@ -280,7 +285,9 @@ def test_exact_match_queries(members):
|
|||
).all()
|
||||
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]
|
||||
|
||||
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):
|
||||
member1, member2, member3 = members
|
||||
|
||||
actual = Member.find((Member.last_name == "Brookins") | (
|
||||
Member.age == 100
|
||||
) & (Member.last_name == "Smith")).all()
|
||||
actual = Member.find(
|
||||
(Member.last_name == "Brookins")
|
||||
| (Member.age == 100) & (Member.last_name == "Smith")
|
||||
).all()
|
||||
assert actual == [member1, member2, member3]
|
||||
|
||||
|
||||
def test_recursive_query_field_resolution(members):
|
||||
member1, _, _ = members
|
||||
member1.address.note = Note(description="Weird house",
|
||||
created_on=datetime.datetime.now())
|
||||
member1.address.note = Note(
|
||||
description="Weird house", created_on=datetime.datetime.now()
|
||||
)
|
||||
member1.save()
|
||||
actual = Member.find(Member.address.note.description == "Weird house").all()
|
||||
assert actual == [member1]
|
||||
|
||||
member1.orders = [
|
||||
Order(items=[Item(price=10.99, name="Ball")],
|
||||
Order(
|
||||
items=[Item(price=10.99, name="Ball")],
|
||||
total=10.99,
|
||||
created_on=datetime.datetime.now())
|
||||
created_on=datetime.datetime.now(),
|
||||
)
|
||||
]
|
||||
member1.save()
|
||||
actual = Member.find(Member.orders.items.name == "Ball").all()
|
||||
|
@ -331,8 +342,9 @@ def test_tag_queries_boolean_logic(members):
|
|||
member1, member2, member3 = members
|
||||
|
||||
actual = Member.find(
|
||||
(Member.first_name == "Andrew") &
|
||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith")).all()
|
||||
(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||
| (Member.last_name == "Smith")
|
||||
).all()
|
||||
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.
|
||||
age=38,
|
||||
join_date=today,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
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.
|
||||
age=38,
|
||||
join_date=today,
|
||||
address=address
|
||||
address=address,
|
||||
)
|
||||
member2.save()
|
||||
|
||||
|
@ -377,9 +389,7 @@ def test_tag_queries_negation(members):
|
|||
└Andrew
|
||||
|
||||
"""
|
||||
query = Member.find(
|
||||
~(Member.first_name == "Andrew")
|
||||
)
|
||||
query = Member.find(~(Member.first_name == "Andrew"))
|
||||
assert query.all() == [member2]
|
||||
|
||||
"""
|
||||
|
@ -411,8 +421,9 @@ def test_tag_queries_negation(members):
|
|||
└Smith
|
||||
"""
|
||||
query = Member.find(
|
||||
~(Member.first_name == "Andrew") &
|
||||
((Member.last_name == "Brookins") | (Member.last_name == "Smith")))
|
||||
~(Member.first_name == "Andrew")
|
||||
& ((Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||
)
|
||||
assert query.all() == [member2]
|
||||
|
||||
"""
|
||||
|
@ -429,12 +440,14 @@ def test_tag_queries_negation(members):
|
|||
└Smith
|
||||
"""
|
||||
query = Member.find(
|
||||
~(Member.first_name == "Andrew") &
|
||||
(Member.last_name == "Brookins") | (Member.last_name == "Smith"))
|
||||
~(Member.first_name == "Andrew") & (Member.last_name == "Brookins")
|
||||
| (Member.last_name == "Smith")
|
||||
)
|
||||
assert query.all() == [member2, member3]
|
||||
|
||||
actual = Member.find(
|
||||
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")).all()
|
||||
(Member.first_name == "Andrew") & ~(Member.last_name == "Brookins")
|
||||
).all()
|
||||
assert actual == [member3]
|
||||
|
||||
|
||||
|
@ -469,19 +482,19 @@ def test_numeric_queries(members):
|
|||
def test_sorting(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]
|
||||
|
||||
actual = Member.find(Member.age > 34).sort_by('-age').all()
|
||||
actual = Member.find(Member.age > 34).sort_by("-age").all()
|
||||
assert actual == [member3, member1]
|
||||
|
||||
with pytest.raises(QueryNotSupportedError):
|
||||
# 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):
|
||||
# This field is not sortable.
|
||||
Member.find().sort_by('join_date').all()
|
||||
Member.find().sort_by("join_date").all()
|
||||
|
||||
|
||||
def test_not_found():
|
||||
|
@ -492,24 +505,28 @@ def test_not_found():
|
|||
|
||||
def test_list_field_limitations():
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class SortableTarotWitch(BaseJsonModel):
|
||||
# We support indexing lists of strings for quality and membership
|
||||
# queries. Sorting is not supported, but is planned.
|
||||
tarot_cards: List[str] = Field(index=True, sortable=True)
|
||||
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class SortableFullTextSearchAlchemicalWitch(BaseJsonModel):
|
||||
# We don't support indexing a list of strings for full-text search
|
||||
# queries. Support for this feature is not planned.
|
||||
potions: List[str] = Field(index=True, full_text_search=True)
|
||||
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class NumerologyWitch(BaseJsonModel):
|
||||
# We don't support indexing a list of numbers. Support for this
|
||||
# feature is To Be Determined.
|
||||
lucky_numbers: List[int] = Field(index=True)
|
||||
|
||||
with pytest.raises(RedisModelError):
|
||||
|
||||
class ReadingWithPrice(EmbeddedJsonModel):
|
||||
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.
|
||||
Migrator().run()
|
||||
|
||||
witch = TarotWitch(
|
||||
tarot_cards=['death']
|
||||
)
|
||||
witch = TarotWitch(tarot_cards=["death"])
|
||||
witch.save()
|
||||
actual = TarotWitch.find(TarotWitch.tarot_cards << 'death').all()
|
||||
actual = TarotWitch.find(TarotWitch.tarot_cards << "death").all()
|
||||
assert actual == [witch]
|
||||
|
||||
|
||||
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