diff --git a/README.md b/README.md index 9dcdceb..394e792 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ To add and install this package as a dependency of your project, run `poetry add ### A simple example: ```python from pydantic import BaseModel, EmailStr -from querytyper import MongoModelMetaclass, MongoQuery +from querytyper import MongoFilterMeta, MongoQuery class User(BaseModel): """User database model.""" @@ -25,7 +25,7 @@ class User(BaseModel): age: int email: EmailStr -class UserFilter(User, metaclass=MongoModelMetaclass): +class UserFilter(User, metaclass=MongoFilterMeta): """User query filter.""" query = MongoQuery( diff --git a/poetry.lock b/poetry.lock index ec03628..d24a437 100644 --- a/poetry.lock +++ b/poetry.lock @@ -659,6 +659,21 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mongomock" +version = "4.1.2" +description = "Fake pymongo stub for testing simple MongoDB-dependent code" +optional = false +python-versions = "*" +files = [ + {file = "mongomock-4.1.2-py2.py3-none-any.whl", hash = "sha256:08a24938a05c80c69b6b8b19a09888d38d8c6e7328547f94d46cadb7f47209f2"}, + {file = "mongomock-4.1.2.tar.gz", hash = "sha256:f06cd62afb8ae3ef63ba31349abd220a657ef0dd4f0243a29587c5213f931b7d"}, +] + +[package.dependencies] +packaging = "*" +sentinels = "*" + [[package]] name = "mypy" version = "1.8.0" @@ -932,6 +947,108 @@ files = [ plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymongo" +version = "4.6.1" +description = "Python driver for MongoDB " +optional = false +python-versions = ">=3.7" +files = [ + {file = "pymongo-4.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4344c30025210b9fa80ec257b0e0aab5aa1d5cca91daa70d82ab97b482cc038e"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux1_i686.whl", hash = "sha256:1c5654bb8bb2bdb10e7a0bc3c193dd8b49a960b9eebc4381ff5a2043f4c3c441"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:eaf2f65190c506def2581219572b9c70b8250615dc918b3b7c218361a51ec42e"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:262356ea5fcb13d35fb2ab6009d3927bafb9504ef02339338634fffd8a9f1ae4"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:2dd2f6960ee3c9360bed7fb3c678be0ca2d00f877068556785ec2eb6b73d2414"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:ff925f1cca42e933376d09ddc254598f8c5fcd36efc5cac0118bb36c36217c41"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:3cadf7f4c8e94d8a77874b54a63c80af01f4d48c4b669c8b6867f86a07ba994f"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55dac73316e7e8c2616ba2e6f62b750918e9e0ae0b2053699d66ca27a7790105"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:154b361dcb358ad377d5d40df41ee35f1cc14c8691b50511547c12404f89b5cb"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2940aa20e9cc328e8ddeacea8b9a6f5ddafe0b087fedad928912e787c65b4909"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:010bc9aa90fd06e5cc52c8fac2c2fd4ef1b5f990d9638548dde178005770a5e8"}, + {file = "pymongo-4.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e470fa4bace5f50076c32f4b3cc182b31303b4fefb9b87f990144515d572820b"}, + {file = "pymongo-4.6.1-cp310-cp310-win32.whl", hash = "sha256:da08ea09eefa6b960c2dd9a68ec47949235485c623621eb1d6c02b46765322ac"}, + {file = "pymongo-4.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:13d613c866f9f07d51180f9a7da54ef491d130f169e999c27e7633abe8619ec9"}, + {file = "pymongo-4.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6a0ae7a48a6ef82ceb98a366948874834b86c84e288dbd55600c1abfc3ac1d88"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd94c503271e79917b27c6e77f7c5474da6930b3fb9e70a12e68c2dff386b9a"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d4ccac3053b84a09251da8f5350bb684cbbf8c8c01eda6b5418417d0a8ab198"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:349093675a2d3759e4fb42b596afffa2b2518c890492563d7905fac503b20daa"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88beb444fb438385e53dc9110852910ec2a22f0eab7dd489e827038fdc19ed8d"}, + {file = "pymongo-4.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8e62d06e90f60ea2a3d463ae51401475568b995bafaffd81767d208d84d7bb1"}, + {file = "pymongo-4.6.1-cp311-cp311-win32.whl", hash = "sha256:5556e306713e2522e460287615d26c0af0fe5ed9d4f431dad35c6624c5d277e9"}, + {file = "pymongo-4.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:b10d8cda9fc2fcdcfa4a000aa10413a2bf8b575852cd07cb8a595ed09689ca98"}, + {file = "pymongo-4.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b435b13bb8e36be11b75f7384a34eefe487fe87a6267172964628e2b14ecf0a7"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e438417ce1dc5b758742e12661d800482200b042d03512a8f31f6aaa9137ad40"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b47ebd89e69fbf33d1c2df79759d7162fc80c7652dacfec136dae1c9b3afac7"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbed8cccebe1169d45cedf00461b2842652d476d2897fd1c42cf41b635d88746"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30a9e06041fbd7a7590693ec5e407aa8737ad91912a1e70176aff92e5c99d20"}, + {file = "pymongo-4.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8729dbf25eb32ad0dc0b9bd5e6a0d0b7e5c2dc8ec06ad171088e1896b522a74"}, + {file = "pymongo-4.6.1-cp312-cp312-win32.whl", hash = "sha256:3177f783ae7e08aaf7b2802e0df4e4b13903520e8380915e6337cdc7a6ff01d8"}, + {file = "pymongo-4.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:00c199e1c593e2c8b033136d7a08f0c376452bac8a896c923fcd6f419e07bdd2"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:13552ca505366df74e3e2f0a4f27c363928f3dff0eef9f281eb81af7f29bc3c5"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:77e0df59b1a4994ad30c6d746992ae887f9756a43fc25dec2db515d94cf0222d"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3a7f02a58a0c2912734105e05dedbee4f7507e6f1bd132ebad520be0b11d46fd"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:026a24a36394dc8930cbcb1d19d5eb35205ef3c838a7e619e04bd170713972e7"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:3b287e814a01deddb59b88549c1e0c87cefacd798d4afc0c8bd6042d1c3d48aa"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:9a710c184ba845afb05a6f876edac8f27783ba70e52d5eaf939f121fc13b2f59"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:30b2c9caf3e55c2e323565d1f3b7e7881ab87db16997dc0cbca7c52885ed2347"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff62ba8ff70f01ab4fe0ae36b2cb0b5d1f42e73dfc81ddf0758cd9f77331ad25"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:547dc5d7f834b1deefda51aedb11a7af9c51c45e689e44e14aa85d44147c7657"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1de3c6faf948f3edd4e738abdb4b76572b4f4fdfc1fed4dad02427e70c5a6219"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2831e05ce0a4df10c4ac5399ef50b9a621f90894c2a4d2945dc5658765514ed"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:144a31391a39a390efce0c5ebcaf4bf112114af4384c90163f402cec5ede476b"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33bb16a07d3cc4e0aea37b242097cd5f7a156312012455c2fa8ca396953b11c4"}, + {file = "pymongo-4.6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b7b1a83ce514700276a46af3d9e481ec381f05b64939effc9065afe18456a6b9"}, + {file = "pymongo-4.6.1-cp37-cp37m-win32.whl", hash = "sha256:3071ec998cc3d7b4944377e5f1217c2c44b811fae16f9a495c7a1ce9b42fb038"}, + {file = "pymongo-4.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2346450a075625c4d6166b40a013b605a38b6b6168ce2232b192a37fb200d588"}, + {file = "pymongo-4.6.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:061598cbc6abe2f382ab64c9caa83faa2f4c51256f732cdd890bcc6e63bfb67e"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:d483793a384c550c2d12cb794ede294d303b42beff75f3b3081f57196660edaf"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f9756f1d25454ba6a3c2f1ef8b7ddec23e5cdeae3dc3c3377243ae37a383db00"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:1ed23b0e2dac6f84f44c8494fbceefe6eb5c35db5c1099f56ab78fc0d94ab3af"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:3d18a9b9b858ee140c15c5bfcb3e66e47e2a70a03272c2e72adda2482f76a6ad"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:c258dbacfff1224f13576147df16ce3c02024a0d792fd0323ac01bed5d3c545d"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:f7acc03a4f1154ba2643edeb13658d08598fe6e490c3dd96a241b94f09801626"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:76013fef1c9cd1cd00d55efde516c154aa169f2bf059b197c263a255ba8a9ddf"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0e6a6c807fa887a0c51cc24fe7ea51bb9e496fe88f00d7930063372c3664c3"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd1fa413f8b9ba30140de198e4f408ffbba6396864c7554e0867aa7363eb58b2"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d219b4508f71d762368caec1fc180960569766049bbc4d38174f05e8ef2fe5b"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27b81ecf18031998ad7db53b960d1347f8f29e8b7cb5ea7b4394726468e4295e"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56816e43c92c2fa8c11dc2a686f0ca248bea7902f4a067fa6cbc77853b0f041e"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef801027629c5b511cf2ba13b9be29bfee36ae834b2d95d9877818479cdc99ea"}, + {file = "pymongo-4.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d4c2be9760b112b1caf649b4977b81b69893d75aa86caf4f0f398447be871f3c"}, + {file = "pymongo-4.6.1-cp38-cp38-win32.whl", hash = "sha256:39d77d8bbb392fa443831e6d4ae534237b1f4eee6aa186f0cdb4e334ba89536e"}, + {file = "pymongo-4.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:4497d49d785482cc1a44a0ddf8830b036a468c088e72a05217f5b60a9e025012"}, + {file = "pymongo-4.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:69247f7a2835fc0984bbf0892e6022e9a36aec70e187fcfe6cae6a373eb8c4de"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7bb0e9049e81def6829d09558ad12d16d0454c26cabe6efc3658e544460688d9"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6a1810c2cbde714decf40f811d1edc0dae45506eb37298fd9d4247b8801509fe"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e2aced6fb2f5261b47d267cb40060b73b6527e64afe54f6497844c9affed5fd0"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:d0355cff58a4ed6d5e5f6b9c3693f52de0784aa0c17119394e2a8e376ce489d4"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:3c74f4725485f0a7a3862cfd374cc1b740cebe4c133e0c1425984bcdcce0f4bb"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:9c79d597fb3a7c93d7c26924db7497eba06d58f88f58e586aa69b2ad89fee0f8"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8ec75f35f62571a43e31e7bd11749d974c1b5cd5ea4a8388725d579263c0fdf6"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e641f931c5cd95b376fd3c59db52770e17bec2bf86ef16cc83b3906c054845"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9aafd036f6f2e5ad109aec92f8dbfcbe76cff16bad683eb6dd18013739c0b3ae"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f2b856518bfcfa316c8dae3d7b412aecacf2e8ba30b149f5eb3b63128d703b9"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec31adc2e988fd7db3ab509954791bbc5a452a03c85e45b804b4bfc31fa221d"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9167e735379ec43d8eafa3fd675bfbb12e2c0464f98960586e9447d2cf2c7a83"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1461199b07903fc1424709efafe379205bf5f738144b1a50a08b0396357b5abf"}, + {file = "pymongo-4.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3094c7d2f820eecabadae76bfec02669567bbdd1730eabce10a5764778564f7b"}, + {file = "pymongo-4.6.1-cp39-cp39-win32.whl", hash = "sha256:c91ea3915425bd4111cb1b74511cdc56d1d16a683a48bf2a5a96b6a6c0f297f7"}, + {file = "pymongo-4.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:ef102a67ede70e1721fe27f75073b5314911dbb9bc27cde0a1c402a11531e7bd"}, + {file = "pymongo-4.6.1.tar.gz", hash = "sha256:31dab1f3e1d0cdd57e8df01b645f52d43cc1b653ed3afd535d2891f4fc4f9712"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (<2.0.0)"] +encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] +gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] +ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +test = ["pytest (>=7)"] +zstd = ["zstandard"] + [[package]] name = "pytest" version = "7.4.4" @@ -1176,6 +1293,16 @@ files = [ {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, ] +[[package]] +name = "sentinels" +version = "1.0.0" +description = "Various objects to denote special meanings in python" +optional = false +python-versions = "*" +files = [ + {file = "sentinels-1.0.0.tar.gz", hash = "sha256:7be0704d7fe1925e397e92d18669ace2f619c92b5d4eb21a89f31e026f9ff4b1"}, +] + [[package]] name = "setuptools" version = "69.0.3" @@ -1383,4 +1510,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "f7eb27fb4476786359c4df8d1b5cef354641c6647db94f9efadecf266f618d55" +content-hash = "cf882055c2dd5de8f48cf90ca0ec9edc0f96a9bfbad242e04411432c300a8000" diff --git a/pyproject.toml b/pyproject.toml index 8a3fb7e..c72682e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ pytest-clarity = ">=1.0.1" pytest-mock = ">=3.10.0" pytest-xdist = ">=3.2.1" ruff = ">=0.1.3" +pymongo = "^4.6.1" +mongomock = "^4.1.2" [tool.poetry.group.dev.dependencies] # https://python-poetry.org/docs/master/managing-dependencies/ cruft = ">=2.14.0" diff --git a/src/querytyper/__init__.py b/src/querytyper/__init__.py index de1b9bf..249ac66 100644 --- a/src/querytyper/__init__.py +++ b/src/querytyper/__init__.py @@ -1,9 +1,9 @@ """querytyper package.""" -from querytyper.mongo import MongoModelMetaclass, MongoQuery, exists, regex_query +from querytyper.mongo import MongoFilterMeta, MongoQuery, exists, regex_query __all__ = [ "MongoQuery", - "MongoModelMetaclass", + "MongoFilterMeta", "regex_query", "exists", ] diff --git a/src/querytyper/mongo/__init__.py b/src/querytyper/mongo/__init__.py index d0ae42d..7462212 100644 --- a/src/querytyper/mongo/__init__.py +++ b/src/querytyper/mongo/__init__.py @@ -1,9 +1,10 @@ """querytyper package.""" -from querytyper.mongo.query import MongoModelMetaclass, MongoQuery, exists, regex_query +from querytyper.mongo.meta import MongoFilterMeta +from querytyper.mongo.query import MongoQuery, exists, regex_query __all__ = [ "MongoQuery", - "MongoModelMetaclass", + "MongoFilterMeta", "regex_query", "exists", ] diff --git a/src/querytyper/mongo/meta.py b/src/querytyper/mongo/meta.py new file mode 100644 index 0000000..b81685b --- /dev/null +++ b/src/querytyper/mongo/meta.py @@ -0,0 +1,53 @@ +"""MongoFilterMeta implementation.""" +from typing import Any, Dict, Tuple, Type + +from pydantic import BaseModel +from pydantic.main import ModelMetaclass + +from querytyper.mongo.query import QueryField + +DictStrAny = Dict[str, Any] + + +class MongoFilterMeta(ModelMetaclass): + """ + Metaclass for query models. + + Use this metaclass to define query models. + + Example + ------- + ```python + class TransactionFilter(Transaction, metaclass=MongoFilterMeta): + pass + ``` + """ + + def __new__( + cls, + name: str, + bases: Tuple[Type[Any]], + namespace: DictStrAny, + ) -> Type["MongoFilterMeta"]: + """Override new class creation to set query fields as class attributes.""" + # check exactly one base class + if len(bases) > 1: + raise TypeError("MongoFilterMeta does not support multiple inheritance") + if len(bases) < 1: + raise TypeError( + "The class with metaclass MongoFilterMeta requires a base class that inherits from BaseModel" + ) + # check the base class is a subclass of BaseModel + if not issubclass(bases[0], BaseModel): + raise TypeError(f"The base class must inherit from {BaseModel.__qualname__}") + base_model_cls: Type[BaseModel] = super().__new__(cls, name, bases, namespace) + for field_name, model_field in base_model_cls.__fields__.items(): + setattr( + cls, + field_name, + QueryField[model_field.type_]( # type: ignore[name-defined] + name=field_name, + field_type=model_field.type_, + ), + ) + return cls diff --git a/src/querytyper/mongo/query.py b/src/querytyper/mongo/query.py index 98160f4..1084ada 100644 --- a/src/querytyper/mongo/query.py +++ b/src/querytyper/mongo/query.py @@ -1,49 +1,57 @@ -"""MongoQuery .""" +"""MongoQuery implementation.""" import re from collections.abc import Iterable -from pprint import pformat -from typing import Any, Dict, Generic, Tuple, Type, TypeVar, Union, cast - -from pydantic import BaseModel -from pydantic.main import ModelMetaclass +from typing import Any, Dict, Generic, Type, TypeVar, Union, cast T = TypeVar("T") DictStrAny = Dict[str, Any] -class MongoQuery: - """MongoQuery wrapper.""" +class MongoQuery(DictStrAny): + """ + MongoQuery is the core `querytyper` class to write MongoDB queries. + + It's a special dict subclass that keeps track of the query conditions + and returns a dictionary that is compatible with the `filter` argument of + the find methods of a pymongo collection. + + Example + ------- + ```python + query = MongoQuery(FilterModel.str_field == "a") + collection = MongoClient(...).get_database("db_name").get_collection("collection_name") + found_doc = collection.find_one(query) + ``` + """ _query_dict: DictStrAny = {} - def __init__(self, *args: Any) -> None: - """Initialize a query object.""" + def __init__(self, *args: Any, **kwargs: DictStrAny) -> None: + """ + Initialize a query object. + """ for arg in args: - if not isinstance(arg, (QueryCondition, bool)): + if not isinstance(arg, (QueryCondition, dict, bool)): raise TypeError( - f"MongoQuery argument must be a QueryCondition or a boolean value, {type(arg)} is not supported." + f"MongoQuery argument must be a QueryCondition, dict or a boolean value, {type(arg)} is not supported." ) - # copy over the class _query_dict to the instance - self._query_dict = MongoQuery._query_dict - # clean up MongoQuery _query_dict at each instantiation + if isinstance(arg, QueryCondition): + MongoQuery._query_dict.update(arg) + super().__init__(MongoQuery._query_dict) + # clean up the class query dict after each instantiation MongoQuery._query_dict = {} def __del__(self) -> None: """MongoQuery destructor.""" - self._query_dict = {} MongoQuery._query_dict = {} - def __repr__(self) -> str: - """Overload repr method to pretty format it.""" - return pformat(self._query_dict) - def __or__( self, other: "MongoQuery", ) -> "MongoQuery": """Overload | operator.""" - self._query_dict = {"$or": [self._query_dict, other._query_dict]} - return self + MongoQuery._query_dict = {"$or": [self, other]} + return MongoQuery() class QueryCondition(DictStrAny): @@ -53,10 +61,6 @@ def __init__(self, *args: Any, **kwargs: DictStrAny) -> None: """Overload init typing.""" super().__init__(*args, **kwargs) - def __repr__(self) -> str: - """Overload repr method to pretty format it.""" - return pformat({**self}) - def __and__( self, other: Union["QueryCondition", bool], @@ -164,47 +168,3 @@ def regex_query( if isinstance(field, QueryField): return QueryCondition({field.name: {"$regex": regex.pattern}}) return QueryCondition({field: {"$regex": regex.pattern}}) - - -class MongoModelMetaclass(ModelMetaclass): - """ - Metaclass for query models. - - Use this metaclass to define query models. - - Example - ------- - ```python - class TransactionFilter(Transaction, metaclass=MongoModelMetaclass): - pass - ``` - """ - - def __new__( - cls, - name: str, - bases: Tuple[Type[Any]], - namespace: DictStrAny, - ) -> Type["MongoModelMetaclass"]: - """Override new class creation to set query fields as class attributes.""" - # check exactly one base class - if len(bases) > 1: - raise TypeError("MongoModelMetaclass does not support multiple inheritance") - if len(bases) < 1: - raise TypeError( - "The class with metaclass MongoModelMetaclass requires a base class that inherits from BaseModel" - ) - # check the base class is a subclass of BaseModel - if not issubclass(bases[0], BaseModel): - raise TypeError(f"The base class must inherit from {BaseModel.__qualname__}") - base_model_cls: Type[BaseModel] = super().__new__(cls, name, bases, namespace) - for field_name, model_field in base_model_cls.__fields__.items(): - setattr( - cls, - field_name, - QueryField[model_field.type_]( # type: ignore[name-defined] - name=field_name, - field_type=model_field.type_, - ), - ) - return cls diff --git a/tests/mongo/conftest.py b/tests/mongo/conftest.py new file mode 100644 index 0000000..7d3846b --- /dev/null +++ b/tests/mongo/conftest.py @@ -0,0 +1,21 @@ +"""Tests fixtures and configurations.""" +from typing import Dict, List, Optional +from pydantic import BaseModel + +from querytyper import MongoFilterMeta + + +class Dummy(BaseModel): + """Dummy base model.""" + + str_field: str + int_field: int + float_field: float + bool_field: bool + list_field: List[str] = ["a", "b"] + dict_field: Dict[str, int] = {"a": 1, "b": 2} + optional_str: Optional[str] = None + + +class QueryModel(Dummy, metaclass=MongoFilterMeta): + """Test model.""" diff --git a/tests/mongo/test_pymongo_integration.py b/tests/mongo/test_pymongo_integration.py new file mode 100644 index 0000000..51ce123 --- /dev/null +++ b/tests/mongo/test_pymongo_integration.py @@ -0,0 +1,32 @@ +"""Test querytyper integration with pymongo.""" +import re +from typing import Any, Dict +from mongomock import MongoClient + +from querytyper import MongoQuery, regex_query +from .conftest import Dummy, QueryModel + + +def test_integration_with_pymongo() -> None: + """Test writing and query documents to and from MongoDB.""" + client: MongoClient[Dict[str, Any]] = MongoClient() + collection = client.get_database("test").get_collection("test") + doc_num = 10 + collection.insert_many( + [ + Dummy( + str_field=f"{i} test string", + int_field=i, + float_field=float(i), + bool_field=True, + ).dict() + for i in range(doc_num) + ] + ) + # found_doc = collection.find_one(MongoQuery(QueryModel.int_field == 1)._query_dict) + found_doc = collection.find_one(MongoQuery(QueryModel.int_field == 1)) + assert found_doc is not None + found_dummy = Dummy(**found_doc) + assert found_dummy.int_field == 1 + found_docs = list(collection.find(MongoQuery(regex_query(QueryModel.str_field, re.compile("test"))))) + assert len(found_docs) == doc_num \ No newline at end of file diff --git a/tests/mongo/test_query.py b/tests/mongo/test_query.py index b214a72..07ba4c1 100644 --- a/tests/mongo/test_query.py +++ b/tests/mongo/test_query.py @@ -1,14 +1,15 @@ """Test mongo query implementation.""" import re -from typing import Dict, List, Optional import pytest from pydantic import BaseModel, EmailStr -from querytyper import MongoModelMetaclass, MongoQuery, exists, regex_query +from querytyper import MongoFilterMeta, MongoQuery, exists, regex_query from querytyper.mongo.query import QueryCondition +from .conftest import Dummy, QueryModel + class User(BaseModel): """User database model.""" @@ -29,7 +30,7 @@ class User(BaseModel): age: int email: EmailStr - class UserFilter(User, metaclass=MongoModelMetaclass): + class UserFilter(User, metaclass=MongoFilterMeta): """User query filter.""" query = MongoQuery( @@ -44,7 +45,7 @@ class UserFilter(User, metaclass=MongoModelMetaclass): ) ) assert isinstance(query, MongoQuery) - assert query._query_dict == { + assert query == { "name": "John", "age": {"$gte": 10}, "email": [ @@ -54,65 +55,49 @@ class UserFilter(User, metaclass=MongoModelMetaclass): } -class Dummy(BaseModel): - """Dummy base model.""" - - str_field: str - int_field: int - float_field: float - bool_field: bool - list_field: List[str] = ["a", "b"] - dict_field: Dict[str, int] = {"a": 1, "b": 2} - optional_str: Optional[str] = None - - -class QueryModel(Dummy, metaclass=MongoModelMetaclass): - """Test model.""" - - def test_query_equals() -> None: """Test MongoQuery equals override.""" query = MongoQuery(QueryModel.str_field == "a") assert isinstance(query, MongoQuery) - assert query._query_dict == {"str_field": "a"} + assert query == {"str_field": "a"} def test_query_less_then() -> None: """Test MongoQuery less_then override.""" query = MongoQuery(QueryModel.int_field <= 1) assert isinstance(query, MongoQuery) - assert query._query_dict == {"int_field": {"$lte": 1}} + assert query == {"int_field": {"$lte": 1}} query = MongoQuery(QueryModel.int_field < 1) assert isinstance(query, MongoQuery) - assert query._query_dict == {"int_field": {"$lt": 1}} + assert query == {"int_field": {"$lt": 1}} def test_query_and() -> None: """Test MongoQuery equals override.""" query = MongoQuery((QueryModel.str_field == "a") & (QueryModel.int_field >= 1)) assert isinstance(query, MongoQuery) - assert query._query_dict == {"str_field": "a", "int_field": {"$gte": 1}} + assert query == {"str_field": "a", "int_field": {"$gte": 1}} def test_query_or() -> None: """Test MongoQuery equals override.""" query = MongoQuery(QueryModel.str_field == "a") | MongoQuery(QueryModel.int_field > 1) assert isinstance(query, MongoQuery) - assert query._query_dict == {"$or": [{"str_field": "a"}, {"int_field": {"$gt": 1}}]} + assert query == {"$or": [{"str_field": "a"}, {"int_field": {"$gt": 1}}]} def test_query_in() -> None: """Test MongoQuery equals override.""" query = MongoQuery(QueryModel.str_field in ["a", "b"]) assert isinstance(query, MongoQuery) - assert query._query_dict == {"str_field": ["a", "b"]} + assert query == {"str_field": ["a", "b"]} def test_query_init_error() -> None: """Test MongoQuery equals override.""" with pytest.raises( TypeError, - match="MongoQuery argument must be a QueryCondition or a boolean value, is not supported.", + match="MongoQuery argument must be a QueryCondition, dict or a boolean value, is not supported.", ): MongoQuery("random string") @@ -144,17 +129,16 @@ def test_querycondition_and() -> None: conditions = True & condition assert isinstance(conditions, QueryCondition) assert isinstance(condition, QueryCondition) - assert condition == MongoQuery._query_dict def test_metaclass_type_errors() -> None: - """Test MongoModelMetaclass.""" + """Test MongoFilterMeta.""" with pytest.raises( TypeError, - match="The class with metaclass MongoModelMetaclass requires a base class that inherits from BaseModel", + match="The class with metaclass MongoFilterMeta requires a base class that inherits from BaseModel", ): - class Test(metaclass=MongoModelMetaclass): + class Test(metaclass=MongoFilterMeta): """Test class.""" class AnotherDummy(BaseModel): @@ -162,16 +146,14 @@ class AnotherDummy(BaseModel): another_field: str - with pytest.raises( - TypeError, match="MongoModelMetaclass does not support multiple inheritance" - ): + with pytest.raises(TypeError, match="MongoFilterMeta does not support multiple inheritance"): - class MultipleInheritanceTest(Dummy, AnotherDummy, metaclass=MongoModelMetaclass): + class MultipleInheritanceTest(Dummy, AnotherDummy, metaclass=MongoFilterMeta): """Test class.""" with pytest.raises(TypeError, match="The base class must inherit from BaseModel"): - class TestNoBaseModel(str, metaclass=MongoModelMetaclass): + class TestNoBaseModel(str, metaclass=MongoFilterMeta): """Test class."""