Broken schema generation

This commit is contained in:
Andrew Brookins 2021-10-12 14:22:57 -07:00
parent 8f32b359f0
commit 5d05de95f8
5 changed files with 234 additions and 58 deletions

129
poetry.lock generated
View file

@ -21,6 +21,19 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "astroid"
version = "2.8.0"
description = "An abstract syntax tree for Python with inference support."
category = "main"
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"
[[package]] [[package]]
name = "async-timeout" name = "async-timeout"
version = "3.0.1" version = "3.0.1"
@ -139,6 +152,20 @@ parallel = ["ipyparallel"]
qtconsole = ["qtconsole"] qtconsole = ["qtconsole"]
test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"]
[[package]]
name = "isort"
version = "5.9.3"
description = "A Python utility / library to sort Python imports."
category = "main"
optional = false
python-versions = ">=3.6.1,<4.0"
[package.extras]
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]] [[package]]
name = "jedi" name = "jedi"
version = "0.18.0" version = "0.18.0"
@ -154,6 +181,14 @@ parso = ">=0.8.0,<0.9.0"
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
[[package]]
name = "lazy-object-proxy"
version = "1.6.0"
description = "A fast and thorough lazy object proxy."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[[package]] [[package]]
name = "matplotlib-inline" name = "matplotlib-inline"
version = "0.1.3" version = "0.1.3"
@ -165,6 +200,14 @@ python-versions = ">=3.5"
[package.dependencies] [package.dependencies]
traitlets = "*" traitlets = "*"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "main"
optional = false
python-versions = "*"
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "0.910" version = "0.910"
@ -232,6 +275,18 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
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"
optional = false
python-versions = ">=3.6"
[package.extras]
docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.0.0" version = "1.0.0"
@ -302,6 +357,23 @@ category = "dev"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
[[package]]
name = "pylint"
version = "2.11.1"
description = "python code static checker"
category = "main"
optional = false
python-versions = "~=3.6"
[package.dependencies]
astroid = ">=2.8.0,<2.9"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
isort = ">=4.2.5,<6"
mccabe = ">=0.6,<0.7"
platformdirs = ">=2.2.0"
toml = ">=0.7.1"
typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""}
[[package]] [[package]]
name = "pyparsing" name = "pyparsing"
version = "2.4.7" version = "2.4.7"
@ -409,10 +481,18 @@ category = "dev"
optional = false optional = false
python-versions = "*" python-versions = "*"
[[package]]
name = "wrapt"
version = "1.12.1"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
python-versions = "*"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.8" python-versions = "^3.8"
content-hash = "baa4bd3c38445c3325bdd317ecbfe99ccaf4bef438970ed31f5c49cc782d575e" content-hash = "e643c8bcc3f54c414e388a8c62256c3c0fe9e2fb0374c3f3b4140e2b0684b654"
[metadata.files] [metadata.files]
aioredis = [ aioredis = [
@ -423,6 +503,10 @@ appnope = [
{file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"},
{file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, {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"},
]
async-timeout = [ async-timeout = [
{file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"},
{file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"},
@ -462,14 +546,46 @@ ipython = [
{file = "ipython-7.27.0-py3-none-any.whl", hash = "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02"}, {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.27.0.tar.gz", hash = "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13"},
] ]
isort = [
{file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"},
{file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"},
]
jedi = [ jedi = [
{file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"},
{file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
] ]
lazy-object-proxy = [
{file = "lazy-object-proxy-1.6.0.tar.gz", hash = "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726"},
{file = "lazy_object_proxy-1.6.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b"},
{file = "lazy_object_proxy-1.6.0-cp27-cp27m-win32.whl", hash = "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e"},
{file = "lazy_object_proxy-1.6.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93"},
{file = "lazy_object_proxy-1.6.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741"},
{file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587"},
{file = "lazy_object_proxy-1.6.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4"},
{file = "lazy_object_proxy-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f"},
{file = "lazy_object_proxy-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3"},
{file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981"},
{file = "lazy_object_proxy-1.6.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2"},
{file = "lazy_object_proxy-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd"},
{file = "lazy_object_proxy-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837"},
{file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653"},
{file = "lazy_object_proxy-1.6.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3"},
{file = "lazy_object_proxy-1.6.0-cp38-cp38-win32.whl", hash = "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8"},
{file = "lazy_object_proxy-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win32.whl", hash = "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61"},
{file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"},
]
matplotlib-inline = [ matplotlib-inline = [
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
{file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"}, {file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
] ]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
mypy = [ mypy = [
{file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"},
{file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"},
@ -515,6 +631,10 @@ pickleshare = [
{file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"},
{file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"},
] ]
platformdirs = [
{file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"},
{file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"},
]
pluggy = [ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
@ -562,6 +682,10 @@ pygments = [
{file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"}, {file = "Pygments-2.10.0-py3-none-any.whl", hash = "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380"},
{file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"},
] ]
pylint = [
{file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"},
{file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"},
]
pyparsing = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@ -607,3 +731,6 @@ wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
] ]
wrapt = [
{file = "wrapt-1.12.1.tar.gz", hash = "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"},
]

View file

@ -17,6 +17,7 @@ mypy = "^0.910"
types-redis = "^3.5.9" types-redis = "^3.5.9"
types-six = "^1.16.1" types-six = "^1.16.1"
python-ulid = "^1.0.3" python-ulid = "^1.0.3"
pylint = "^2.11.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
pytest = "^6.2.4" pytest = "^6.2.4"

View file

@ -94,6 +94,14 @@ class Operators(Enum):
ExpressionOrModelField = Union['Expression', 'NegatedExpression', ModelField] ExpressionOrModelField = Union['Expression', 'NegatedExpression', ModelField]
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)
class ExpressionProtocol(Protocol): class ExpressionProtocol(Protocol):
op: Operators op: Operators
left: ExpressionOrModelField left: ExpressionOrModelField
@ -166,15 +174,16 @@ class Expression:
op: Operators op: Operators
left: ExpressionOrModelField left: ExpressionOrModelField
right: ExpressionOrModelField right: ExpressionOrModelField
parents: List[Tuple[str, 'RedisModel']]
def __invert__(self): def __invert__(self):
return NegatedExpression(self) return NegatedExpression(self)
def __and__(self, other: ExpressionOrModelField): def __and__(self, other: ExpressionOrModelField):
return Expression(left=self, op=Operators.AND, right=other) return Expression(left=self, op=Operators.AND, right=other, parents=self.parents)
def __or__(self, other: ExpressionOrModelField): def __or__(self, other: ExpressionOrModelField):
return Expression(left=self, op=Operators.OR, right=other) return Expression(left=self, op=Operators.OR, right=other, parents=self.parents)
@property @property
def name(self): def name(self):
@ -189,26 +198,34 @@ ExpressionOrNegated = Union[Expression, NegatedExpression]
class ExpressionProxy: class ExpressionProxy:
def __init__(self, field: ModelField): def __init__(self, field: ModelField, parents: List[Tuple[str, 'RedisModel']]):
self.field = field self.field = field
self.parents = parents
def __eq__(self, other: Any) -> Expression: # type: ignore[override] def __eq__(self, other: Any) -> Expression: # type: ignore[override]
return Expression(left=self.field, op=Operators.EQ, right=other) return Expression(left=self.field, op=Operators.EQ, right=other, parents=self.parents)
def __ne__(self, other: Any) -> Expression: # type: ignore[override] def __ne__(self, other: Any) -> Expression: # type: ignore[override]
return Expression(left=self.field, op=Operators.NE, right=other) return Expression(left=self.field, op=Operators.NE, right=other, parents=self.parents)
def __lt__(self, other: Any) -> Expression: # type: ignore[override] def __lt__(self, other: Any) -> Expression: # type: ignore[override]
return Expression(left=self.field, op=Operators.LT, right=other) return Expression(left=self.field, op=Operators.LT, right=other, parents=self.parents)
def __le__(self, other: Any) -> Expression: # type: ignore[override] def __le__(self, other: Any) -> Expression: # type: ignore[override]
return Expression(left=self.field, op=Operators.LE, right=other) return Expression(left=self.field, op=Operators.LE, right=other, parents=self.parents)
def __gt__(self, other: Any) -> Expression: # type: ignore[override] def __gt__(self, other: Any) -> Expression: # type: ignore[override]
return Expression(left=self.field, op=Operators.GT, right=other) return Expression(left=self.field, op=Operators.GT, right=other, parents=self.parents)
def __ge__(self, other: Any) -> Expression: # type: ignore[override] def __ge__(self, other: Any) -> Expression: # type: ignore[override]
return Expression(left=self.field, op=Operators.GE, right=other) return Expression(left=self.field, op=Operators.GE, right=other, parents=self.parents)
def __getattr__(self, item):
attr = getattr(self.field.outer_type_, item)
if isinstance(attr, self.__class__):
attr.parents.insert(0, (self.field.name, self.field.outer_type_))
attr.parents = attr.parents + self.parents
return attr
class QueryNotSupportedError(Exception): class QueryNotSupportedError(Exception):
@ -265,7 +282,10 @@ class FindQuery:
if self.expressions: if self.expressions:
self._expression = reduce(operator.and_, self.expressions) self._expression = reduce(operator.and_, self.expressions)
else: else:
self._expression = Expression(left=None, right=None, op=Operators.ALL) # TODO: Is there a better way to support the "give me all records" query?
# Also -- if we do it this way, we need different type annotations.
self._expression = Expression(left=None, right=None, op=Operators.ALL,
parents=[])
return self._expression return self._expression
@property @property
@ -316,7 +336,11 @@ class FindQuery:
@classmethod @classmethod
def resolve_value(cls, field_name: str, field_type: RediSearchFieldTypes, def resolve_value(cls, field_name: str, field_type: RediSearchFieldTypes,
field_info: PydanticFieldInfo, op: Operators, value: Any) -> str: 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}"
result = "" result = ""
if field_type is RediSearchFieldTypes.TEXT: if field_type is RediSearchFieldTypes.TEXT:
result = f"@{field_name}:" result = f"@{field_name}:"
@ -427,6 +451,9 @@ class FindQuery:
field_type = cls.resolve_field_type(expression.left) field_type = cls.resolve_field_type(expression.left)
field_name = expression.left.name field_name = expression.left.name
field_info = expression.left.field_info field_info = expression.left.field_info
if not field_info or not getattr(field_info, "index", None):
raise QueryNotSupportedError(f"You tried to query by a field ({field_name}) "
f"that isn't indexed. See docs: TODO")
else: else:
raise QueryNotSupportedError(f"A query expression should start with either a field " raise QueryNotSupportedError(f"A query expression should start with either a field "
f"or an expression enclosed in parenthesis. See docs: " f"or an expression enclosed in parenthesis. See docs: "
@ -454,7 +481,8 @@ class FindQuery:
if isinstance(right, ModelField): if isinstance(right, ModelField):
raise QueryNotSupportedError("Comparing fields is not supported. See docs: TODO") raise QueryNotSupportedError("Comparing fields is not supported. See docs: TODO")
else: else:
result += cls.resolve_value(field_name, field_type, field_info, expression.op, right) result += cls.resolve_value(field_name, field_type, field_info,
expression.op, right, expression.parents)
if encompassing_expression_is_negated: if encompassing_expression_is_negated:
result = f"-({result})" result = f"-({result})"
@ -705,6 +733,7 @@ class MetaProtocol(Protocol):
primary_key_creator_cls: Type[PrimaryKeyCreator] primary_key_creator_cls: Type[PrimaryKeyCreator]
index_name: str index_name: str
abstract: bool abstract: bool
embedded: bool
@dataclasses.dataclass @dataclasses.dataclass
@ -722,6 +751,7 @@ class DefaultMeta:
primary_key_creator_cls: Optional[Type[PrimaryKeyCreator]] = None primary_key_creator_cls: Optional[Type[PrimaryKeyCreator]] = None
index_name: Optional[str] = None index_name: Optional[str] = None
abstract: Optional[bool] = False abstract: Optional[bool] = False
embedded: Optional[bool] = False
class ModelMeta(ModelMetaclass): class ModelMeta(ModelMetaclass):
@ -730,6 +760,11 @@ class ModelMeta(ModelMetaclass):
def __new__(cls, name, bases, attrs, **kwargs): # noqa C901 def __new__(cls, name, bases, attrs, **kwargs): # noqa C901
meta = attrs.pop('Meta', None) meta = attrs.pop('Meta', None)
new_class = super().__new__(cls, name, bases, attrs, **kwargs) new_class = super().__new__(cls, name, bases, attrs, **kwargs)
# The fact that there is a Meta field and _meta field is important: a
# 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) meta = meta or getattr(new_class, 'Meta', None)
base_meta = getattr(new_class, '_meta', None) base_meta = getattr(new_class, '_meta', None)
@ -739,8 +774,9 @@ class ModelMeta(ModelMetaclass):
elif base_meta: elif base_meta:
new_class._meta = deepcopy(base_meta) new_class._meta = deepcopy(base_meta)
new_class.Meta = new_class._meta new_class.Meta = new_class._meta
# Unset inherited values we don't want to reuse (typically based on the model name). # Unset inherited values we don't want to reuse (typically based on
new_class._meta.abstract = False # the model name).
new_class._meta.embedded = False
new_class._meta.model_key_prefix = None new_class._meta.model_key_prefix = None
new_class._meta.index_name = None new_class._meta.index_name = None
else: else:
@ -750,7 +786,7 @@ class ModelMeta(ModelMetaclass):
# Create proxies for each model field so that we can use the field # Create proxies for each model field so that we can use the field
# in queries, like Model.get(Model.field_name == 1) # in queries, like Model.get(Model.field_name == 1)
for field_name, field in new_class.__fields__.items(): for field_name, field in new_class.__fields__.items():
setattr(new_class, field_name, ExpressionProxy(field)) setattr(new_class, field_name, ExpressionProxy(field, []))
# Check if this is our FieldInfo version with extended ORM metadata. # Check if this is our FieldInfo version with extended ORM metadata.
if isinstance(field.field_info, FieldInfo): if isinstance(field.field_info, FieldInfo):
if field.field_info.primary_key: if field.field_info.primary_key:
@ -774,8 +810,9 @@ class ModelMeta(ModelMetaclass):
new_class._meta.index_name = f"{new_class._meta.global_key_prefix}:" \ new_class._meta.index_name = f"{new_class._meta.global_key_prefix}:" \
f"{new_class._meta.model_key_prefix}:index" f"{new_class._meta.model_key_prefix}:index"
# Not an abstract model class # Not an abstract model class or embedded model, so we should let the
if abc.ABC not in bases: # Migrator create indexes for it.
if abc.ABC not in bases and not new_class._meta.embedded:
key = f"{new_class.__module__}.{new_class.__qualname__}" key = f"{new_class.__module__}.{new_class.__qualname__}"
model_registry[key] = new_class model_registry[key] = new_class
@ -967,7 +1004,7 @@ class HashModel(RedisModel, abc.ABC):
schema_parts = [] schema_parts = []
for name, field in cls.__fields__.items(): for name, field in cls.__fields__.items():
# TODO: Merge this code with schema_for_type() # TODO: Merge this code with schema_for_type()?
_type = field.outer_type_ _type = field.outer_type_
if getattr(field.field_info, 'primary_key', None): if getattr(field.field_info, 'primary_key', None):
if issubclass(_type, str): if issubclass(_type, str):
@ -1047,6 +1084,8 @@ class JsonModel(RedisModel, abc.ABC):
schema_parts = [] schema_parts = []
json_path = "$" json_path = "$"
if cls.__name__ == "Address":
import ipdb; ipdb.set_trace()
for name, field in cls.__fields__.items(): for name, field in cls.__fields__.items():
# TODO: Merge this code with schema_for_type()? # TODO: Merge this code with schema_for_type()?
_type = field.outer_type_ _type = field.outer_type_
@ -1070,21 +1109,20 @@ class JsonModel(RedisModel, abc.ABC):
log.warning("Model %s defined an empty list field: %s", cls, name) log.warning("Model %s defined an empty list field: %s", cls, name)
continue continue
embedded_cls = embedded_cls[0] embedded_cls = embedded_cls[0]
schema_parts.append(cls.schema_for_type(f"{json_path}.{name}[]", name, f"{name}", # TODO: Should this have a name prefix?
schema_parts.append(cls.schema_for_type(f"{json_path}.{name}[]", name, name,
embedded_cls, field.field_info)) embedded_cls, field.field_info))
elif issubclass(_type, RedisModel): elif issubclass(_type, RedisModel):
schema_parts.append(cls.schema_for_type(f"{json_path}.{name}", name, f"{name}", _type, schema_parts.append(cls.schema_for_type(f"{json_path}.{name}", name, name, _type,
field.field_info)) field.field_info))
return schema_parts return schema_parts
@classmethod @classmethod
# TODO: We need both the "name" of the field (address_line_1) as we'll
# find it in the JSON document, AND the name of the field as it should
# be in the redisearch schema (address_address_line_1). Maybe both "name"
# and "name_prefix"?
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) -> str: field_info: PydanticFieldInfo) -> str:
index_field_name = f"{name_prefix}{name}" if name == "description":
import ipdb; ipdb.set_trace()
index_field_name = f"{name_prefix}_{name}"
should_index = getattr(field_info, 'index', False) should_index = getattr(field_info, 'index', False)
if get_origin(typ) == list: if get_origin(typ) == list:
@ -1094,15 +1132,14 @@ class JsonModel(RedisModel, abc.ABC):
log.warning("Model %s defined an empty list field: %s", cls, name) log.warning("Model %s defined an empty list field: %s", cls, name)
return "" return ""
embedded_cls = embedded_cls[0] embedded_cls = embedded_cls[0]
# TODO: We need to pass the "JSON Path so far" which should include the return cls.schema_for_type(f"{json_path}[]", name, f"{name_prefix}{name}",
# correct syntax for an array. embedded_cls, field_info)
return cls.schema_for_type(f"{json_path}[]", name, f"{name_prefix}{name}", embedded_cls, field_info)
elif issubclass(typ, RedisModel): elif issubclass(typ, RedisModel):
sub_fields = [] sub_fields = []
for embedded_name, field in typ.__fields__.items(): for embedded_name, field in typ.__fields__.items():
sub_fields.append(cls.schema_for_type(f"{json_path}.{embedded_name}", sub_fields.append(cls.schema_for_type(f"{json_path}.{embedded_name}",
embedded_name, embedded_name,
f"{name_prefix}_", f"{name_prefix}_{embedded_name}",
field.outer_type_, field.outer_type_,
field.field_info)) field.field_info))
return " ".join(filter(None, sub_fields)) return " ".join(filter(None, sub_fields))

View file

@ -397,7 +397,4 @@ def test_schema():
another_integer: int another_integer: int
another_float: float another_float: float
assert Address.redisearch_schema() == "ON HASH PREFIX 1 redis-developer:tests.test_hash_model.Address: " \ 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"
"SCHEMA pk TAG a_string TAG a_full_text_string TAG " \
"a_full_text_string_fts TEXT an_integer NUMERIC SORTABLE " \
"a_float NUMERIC"

View file

@ -12,7 +12,7 @@ from redis_developer.orm import (
JsonModel, JsonModel,
Field, Field,
) )
from redis_developer.orm.model import RedisModelError, QueryNotSupportedError, NotFoundError from redis_developer.orm.model import RedisModelError, QueryNotSupportedError, NotFoundError, embedded
r = redis.Redis() r = redis.Redis()
today = datetime.date.today() today = datetime.date.today()
@ -23,21 +23,32 @@ class BaseJsonModel(JsonModel, abc.ABC):
global_key_prefix = "redis-developer" global_key_prefix = "redis-developer"
class Address(BaseJsonModel): class EmbeddedJsonModel(BaseJsonModel, abc.ABC):
class Meta:
embedded = True
class Note(EmbeddedJsonModel):
description: str = Field(index=True)
created_on: datetime.datetime
class Address(EmbeddedJsonModel):
address_line_1: str address_line_1: str
address_line_2: Optional[str] address_line_2: Optional[str]
city: str city: str = Field(index=True)
state: str state: str
country: str country: str
postal_code: str = Field(index=True) postal_code: str = Field(index=True)
note: Optional[Note]
class Item(BaseJsonModel): class Item(EmbeddedJsonModel):
price: decimal.Decimal price: decimal.Decimal
name: str = Field(index=True, full_text_search=True) name: str = Field(index=True, full_text_search=True)
class Order(BaseJsonModel): class Order(EmbeddedJsonModel):
items: List[Item] items: List[Item]
total: decimal.Decimal total: decimal.Decimal
created_on: datetime.datetime created_on: datetime.datetime
@ -249,7 +260,7 @@ def test_exact_match_queries(members):
(Member.last_name == "Brookins") & (Member.first_name == "Andrew") (Member.last_name == "Brookins") & (Member.first_name == "Andrew")
| (Member.first_name == "Kim") | (Member.first_name == "Kim")
).all() ).all()
assert actual == [member2, member1] assert actual == [member1, member2]
actual = Member.find(Member.first_name == "Kim", Member.last_name == "Brookins").all() actual = Member.find(Member.first_name == "Kim", Member.last_name == "Brookins").all()
assert actual == [member2] assert actual == [member2]
@ -257,6 +268,21 @@ def test_exact_match_queries(members):
actual = Member.find(Member.address.city == "Portland").all() actual = Member.find(Member.address.city == "Portland").all()
assert actual == [member1, member2, member3] assert actual == [member1, member2, member3]
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")],
total=10.99,
created_on=datetime.datetime.now())
]
member1.save()
actual = Member.find(Member.orders.items.name == "Ball").all()
assert actual == [member1]
def test_recursive_query_resolution(members): def test_recursive_query_resolution(members):
member1, member2, member3 = members member1, member2, member3 = members
@ -425,16 +451,4 @@ def test_not_found():
def test_schema(): def test_schema():
assert Member.redisearch_schema() == "ON JSON PREFIX 1 " \ 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 $.address.pk AS address_pk TAG SEPARATOR | $.address.postal_code AS address_postal_code TAG SEPARATOR | $.address.note.pk AS address__pk TAG SEPARATOR | $.address.note.description AS address__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 | $.orders[].items[].name AS orders_items_name_fts TEXT"
"redis-developer:tests.test_json_model.Member: " \
"SCHEMA $.pk AS pk TAG " \
"$.first_name AS first_name TAG " \
"$.last_name AS last_name TAG " \
"$.email AS email TAG " \
"$.age AS age NUMERIC " \
"$.address.pk AS address_pk TAG " \
"$.address.postal_code AS address_postal_code TAG " \
"$.orders[].pk AS orders_pk TAG " \
"$.orders[].items[].pk AS orders_items_pk TAG " \
"$.orders[].items[].name AS orders_items_name TAG " \
"$.orders[].items[].name AS orders_items_name_fts TEXT"