mgqpy 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mgqpy/__init__.py ADDED
@@ -0,0 +1,177 @@
1
+ # Terminology
2
+ #
3
+ # Query refers to the document that is passed to the compiler / mongodb find that holds
4
+ # the query conditions (can be multiple),
5
+ # e.g. { "fruits.type": { "$eq": "berry", "$ne": "aggregate" }, "fruits": { "$size": 3 }}
6
+ #
7
+ # Cond (conditions) refers to a single path-expression pair,
8
+ # e.g. { "fruits.type": { "$eq": "berry", "$ne": "aggregate" }
9
+ #
10
+ # Path refers to dot-separated fields,
11
+ # e.g. "fruits.type"
12
+ #
13
+ # Exp (expression) refers to the object that holds operator and value pairs (can be multiple),
14
+ # e.g. { "$eq": "berry", "$ne": "aggregate" }
15
+ #
16
+ # Op (operator) refers to the logical operator that is matched against the value,
17
+ # e.g. "$eq"
18
+ #
19
+ # Ov (operator value) refers to the value that you are matching against in
20
+ # the context of a single operator e.g. "berry"
21
+ #
22
+ # Doc refers to the object that is passed to the compiled filter function
23
+
24
+ from typing import List
25
+
26
+ from .operators.and_or_nor import _match_and, _match_or, _match_nor, _validate_query_ops
27
+ from .operators.all import _match_all, _validate_all
28
+ from .operators.elem_match import _match_elem_match
29
+ from .operators.eq_ne_not import _match_eq, _match_ne, _match_not
30
+ from .operators.gt import _match_gt
31
+ from .operators.gte import _match_gte
32
+ from .operators.in_nin import _match_in, _match_nin, _validate_in_nin
33
+ from .operators.lt import _match_lt
34
+ from .operators.lte import _match_lte
35
+ from .operators.mod import _match_mod, _validate_mod
36
+ from .operators.regex import _match_regex
37
+ from .operators.size import _match_size, _validate_size
38
+
39
+ cond_ops = {
40
+ "$eq",
41
+ "$gt",
42
+ "$gte",
43
+ "$in",
44
+ "$lt",
45
+ "$lte",
46
+ "$ne",
47
+ "$nin",
48
+ "$not",
49
+ "$regex",
50
+ "$options",
51
+ "$mod",
52
+ "$all",
53
+ "$elemMatch",
54
+ "$size",
55
+ }
56
+
57
+ query_ops = {
58
+ "$and",
59
+ "$or",
60
+ "$nor",
61
+ }
62
+
63
+
64
+ class Query:
65
+ def __init__(self, query):
66
+ self._query = query
67
+ pass
68
+
69
+ def test(self, doc) -> bool:
70
+ return _match_cond(self._query, doc)
71
+
72
+ def validate(self):
73
+ _validate(self._query)
74
+ return self
75
+
76
+
77
+ def _match_cond(query, doc):
78
+ results: List[bool] = []
79
+
80
+ for path in query:
81
+ if isinstance(path, str) and path in query_ops:
82
+ if path == "$and":
83
+ results.append(_match_and(doc, path, query["$and"]))
84
+ if path == "$or":
85
+ results.append(_match_or(doc, path, query["$or"]))
86
+ if path == "$nor":
87
+ results.append(_match_nor(doc, path, query["$nor"]))
88
+ else:
89
+ exp_or_ov = query[path]
90
+ is_all_exp = _check_all_exp(exp_or_ov)
91
+ path_parts = path.split(".")
92
+
93
+ if is_all_exp:
94
+ exp = exp_or_ov
95
+ if "$eq" in exp:
96
+ results.append(_match_eq(doc, path_parts, exp["$eq"]))
97
+ if "$ne" in exp:
98
+ results.append(_match_ne(doc, path_parts, exp["$ne"]))
99
+ if "$gt" in exp:
100
+ results.append(_match_gt(doc, path_parts, exp["$gt"]))
101
+ if "$gte" in exp:
102
+ results.append(_match_gte(doc, path_parts, exp["$gte"]))
103
+ if "$lt" in exp:
104
+ results.append(_match_lt(doc, path_parts, exp["$lt"]))
105
+ if "$lte" in exp:
106
+ results.append(_match_lte(doc, path_parts, exp["$lte"]))
107
+ if "$in" in exp:
108
+ results.append(_match_in(doc, path_parts, exp["$in"]))
109
+ if "$nin" in exp:
110
+ results.append(_match_nin(doc, path_parts, exp["$nin"]))
111
+ if "$not" in exp:
112
+ results.append(_match_not(doc, path, exp["$not"]))
113
+ if "$regex" in exp:
114
+ ov = {
115
+ "$regex": exp["$regex"],
116
+ "$options": exp.get("$options", ""),
117
+ }
118
+ results.append(_match_regex(doc, path_parts, ov))
119
+ if "$mod" in exp:
120
+ results.append(_match_mod(doc, path_parts, exp["$mod"]))
121
+ if "$all" in exp:
122
+ results.append(_match_all(doc, path_parts, exp["$all"]))
123
+ if "$elemMatch" in exp:
124
+ results.append(
125
+ _match_elem_match(doc, path_parts, exp["$elemMatch"])
126
+ )
127
+ if "$size" in exp:
128
+ results.append(_match_size(doc, path_parts, exp["$size"]))
129
+ else:
130
+ ov = exp_or_ov
131
+ results.append(_match_eq(doc, path_parts, ov))
132
+
133
+ return all(results)
134
+
135
+
136
+ def _validate(query) -> bool:
137
+ if not isinstance(query, dict):
138
+ raise TypeError("query must be a dict")
139
+
140
+ for path in query:
141
+ if isinstance(path, str) and path in query_ops:
142
+ if path == "$and":
143
+ if not _validate_query_ops(query["$and"]):
144
+ raise TypeError("$and operator value must be a list")
145
+ if path == "$or":
146
+ if not _validate_query_ops(query["$or"]):
147
+ raise TypeError("$or operator value must be a list")
148
+ if path == "$nor":
149
+ if not _validate_query_ops(query["$nor"]):
150
+ raise TypeError("$nor operator value must be a list")
151
+ for cond in query[path]:
152
+ _validate(cond)
153
+ else:
154
+ exp_or_ov = query[path]
155
+ is_all_exp = _check_all_exp(exp_or_ov)
156
+ if is_all_exp:
157
+ exp = exp_or_ov
158
+ if "$in" in exp and not _validate_in_nin(exp["$in"]):
159
+ raise TypeError("$in operator value must be a list")
160
+ if "$nin" in exp and not _validate_in_nin(exp["$nin"]):
161
+ raise TypeError("$nin operator value must be a list")
162
+ if "$all" in exp and not _validate_all(exp["$all"]):
163
+ raise TypeError("$all operator value must be a list")
164
+ if "$mod" in exp and not _validate_mod(exp["$mod"]):
165
+ raise TypeError("$mod operator value must be a list of 2 numbers")
166
+ if "$size" in exp and not _validate_size(exp["$size"]):
167
+ raise TypeError("$size operator value must be a number")
168
+
169
+ return True
170
+
171
+
172
+ def _check_all_exp(exp_or_ov):
173
+ return (
174
+ exp_or_ov
175
+ and isinstance(exp_or_ov, dict)
176
+ and all(key in cond_ops for key in exp_or_ov.keys())
177
+ )
mgqpy/operators/all.py ADDED
@@ -0,0 +1,52 @@
1
+ from typing import List
2
+
3
+ import mgqpy
4
+
5
+
6
+ def _match_all(doc, path: List[str], ov) -> bool:
7
+ if not _validate_all(ov):
8
+ return False
9
+
10
+ if len(ov) == 0:
11
+ return False
12
+
13
+ if _is_all_elem_match(ov):
14
+ elem_match_query = {"$and": [{".".join(path): o} for o in ov]}
15
+ return mgqpy._match_cond(elem_match_query, doc)
16
+
17
+ if len(path) == 0:
18
+ if not isinstance(doc, list):
19
+ return False
20
+
21
+ return all([o in doc or o == doc for o in ov])
22
+
23
+ key = path[0]
24
+ rest = path[1:]
25
+
26
+ if isinstance(doc, dict) and key in doc:
27
+ return _match_all(doc[key], rest, ov)
28
+
29
+ if isinstance(doc, list) and key.isdigit():
30
+ idx = int(key)
31
+ if idx < len(doc):
32
+ return _match_all(doc[idx], rest, ov)
33
+
34
+ if isinstance(doc, list):
35
+ return any([_match_all(d, path, ov) for d in doc])
36
+
37
+ return False
38
+
39
+
40
+ # $all can be a list of values, or only $elemMatch if $ expression is in a list of dicts
41
+ def _validate_all(ov) -> bool:
42
+ is_list = isinstance(ov, list)
43
+ if is_list:
44
+ if len(ov) == 0:
45
+ return True
46
+ if isinstance(ov[0], dict) and any(k.startswith("$") for k in ov[0].keys()):
47
+ return _is_all_elem_match(ov)
48
+ return is_list
49
+
50
+
51
+ def _is_all_elem_match(ov) -> bool:
52
+ return all(isinstance(o, dict) and "$elemMatch" in o for o in ov)
@@ -0,0 +1,31 @@
1
+ import mgqpy
2
+
3
+
4
+ def _match_and(doc, path: str, ov) -> bool:
5
+ if not _validate_query_ops(ov):
6
+ return False
7
+
8
+ return all(mgqpy._match_cond(cond, doc) for cond in ov)
9
+
10
+
11
+ def _match_or(doc, path: str, ov) -> bool:
12
+ if not _validate_query_ops(ov):
13
+ return False
14
+
15
+ return any(mgqpy._match_cond(cond, doc) for cond in ov)
16
+
17
+
18
+ def _match_nor(doc, path: str, ov) -> bool:
19
+ if not _validate_query_ops(ov):
20
+ return False
21
+
22
+ return not any(mgqpy._match_cond(cond, doc) for cond in ov)
23
+
24
+
25
+ def _validate_query_ops(ov) -> bool:
26
+ """
27
+ Validate query op is given a list (shallow).
28
+ Recursion is done by _validate and _match_cond.
29
+ """
30
+
31
+ return isinstance(ov, list)
@@ -0,0 +1,31 @@
1
+ import mgqpy
2
+
3
+
4
+ from typing import List
5
+
6
+
7
+ def _match_elem_match(doc, path: List[str], ov) -> bool:
8
+ if len(path) == 0:
9
+ if not isinstance(doc, list):
10
+ return False
11
+
12
+ if any(mgqpy._match_cond(ov, d) for d in doc):
13
+ return True
14
+
15
+ return False
16
+
17
+ key = path[0]
18
+ rest = path[1:]
19
+
20
+ if isinstance(doc, dict) and key in doc:
21
+ return _match_elem_match(doc[key], rest, ov)
22
+
23
+ if isinstance(doc, list) and key.isdigit():
24
+ idx = int(key)
25
+ if idx < len(doc):
26
+ return _match_elem_match(doc[idx], rest, ov)
27
+
28
+ if isinstance(doc, list):
29
+ return any([_match_elem_match(d, path, ov) for d in doc])
30
+
31
+ return False
@@ -0,0 +1,45 @@
1
+ import re
2
+ from typing import List
3
+
4
+ from mgqpy.utils import coerce
5
+
6
+ import mgqpy
7
+
8
+
9
+ def _match_eq(doc, path: List[str], ov) -> bool:
10
+ if len(path) == 0:
11
+ if isinstance(doc, list) and any([_match_eq(d, path, ov) for d in doc]):
12
+ return True
13
+
14
+ if isinstance(ov, re.Pattern) and isinstance(doc, str):
15
+ return bool(ov.search(doc))
16
+
17
+ doc, ov = coerce(doc, ov)
18
+ return doc == ov
19
+
20
+ key = path[0]
21
+ rest = path[1:]
22
+
23
+ if isinstance(doc, dict) and key in doc:
24
+ return _match_eq(doc[key], rest, ov)
25
+
26
+ if isinstance(doc, list) and key.isdigit():
27
+ idx = int(key)
28
+ if idx < len(doc):
29
+ return _match_eq(doc[idx], rest, ov)
30
+
31
+ if isinstance(doc, list):
32
+ return any([_match_eq(d, path, ov) for d in doc])
33
+
34
+ if ov is None:
35
+ return True
36
+
37
+ return False
38
+
39
+
40
+ def _match_ne(doc, path: List[str], ov) -> bool:
41
+ return not _match_eq(doc, path, ov)
42
+
43
+
44
+ def _match_not(doc, path, ov):
45
+ return not mgqpy._match_cond({path: ov}, doc)
mgqpy/operators/gt.py ADDED
@@ -0,0 +1,56 @@
1
+ import operator
2
+ from itertools import zip_longest
3
+ from numbers import Number
4
+ from typing import List
5
+
6
+ from mgqpy.utils import coerce
7
+
8
+
9
+ def _match_gt(doc, path: List[str], ov) -> bool:
10
+ if len(path) == 0:
11
+ if isinstance(doc, list) and any([_match_gt(d, path, ov) for d in doc]):
12
+ return True
13
+
14
+ if isinstance(doc, list) and isinstance(ov, list):
15
+ if doc > ov:
16
+ return True
17
+
18
+ if isinstance(doc, dict) and isinstance(ov, dict):
19
+ keys = zip_longest(doc.keys(), ov.keys())
20
+ for doc_key, ov_key in keys:
21
+ if doc_key is None:
22
+ return False
23
+ if ov_key is None:
24
+ return True
25
+ if doc_key != ov_key:
26
+ return doc_key > ov_key
27
+ if doc_key == ov_key:
28
+ if doc[doc_key] > ov[ov_key]:
29
+ return True
30
+ if doc[doc_key] < ov[ov_key]:
31
+ return False
32
+ return False
33
+
34
+ try:
35
+ doc, ov = coerce(doc, ov)
36
+ return operator.gt(doc, ov)
37
+ except Exception:
38
+ pass
39
+
40
+ return False
41
+
42
+ key = path[0]
43
+ rest = path[1:]
44
+
45
+ if isinstance(doc, dict) and key in doc:
46
+ return _match_gt(doc[key], rest, ov)
47
+
48
+ if isinstance(doc, list) and key.isdigit():
49
+ idx = int(key)
50
+ if idx < len(doc):
51
+ return _match_gt(doc[idx], rest, ov)
52
+
53
+ if isinstance(doc, list):
54
+ return any([_match_gt(d, path, ov) for d in doc])
55
+
56
+ return False
mgqpy/operators/gte.py ADDED
@@ -0,0 +1,65 @@
1
+ import operator
2
+ from itertools import zip_longest
3
+ from numbers import Number
4
+ from typing import List
5
+
6
+ from mgqpy.utils import coerce
7
+
8
+
9
+ def _match_gte(doc, path: List[str], ov) -> bool:
10
+ if len(path) == 0:
11
+ if isinstance(doc, list) and any([_match_gte(d, path, ov) for d in doc]):
12
+ return True
13
+
14
+ if isinstance(doc, list) and isinstance(ov, list):
15
+ if doc >= ov:
16
+ return True
17
+
18
+ if isinstance(doc, dict) and isinstance(ov, dict):
19
+ if not doc and not ov:
20
+ return True
21
+
22
+ keys = zip_longest(doc.keys(), ov.keys())
23
+ for doc_key, ov_key in keys:
24
+ if doc_key is None:
25
+ return False
26
+ if ov_key is None:
27
+ return True
28
+ if doc_key != ov_key:
29
+ return doc_key > ov_key
30
+ if doc_key == ov_key:
31
+ if doc[doc_key] > ov[ov_key]:
32
+ return True
33
+ if doc[doc_key] < ov[ov_key]:
34
+ return False
35
+ return True
36
+
37
+ if doc is None and ov is None:
38
+ return True
39
+
40
+ try:
41
+ doc, ov = coerce(doc, ov)
42
+ return operator.ge(doc, ov)
43
+ except Exception:
44
+ pass
45
+
46
+ return False
47
+
48
+ key = path[0]
49
+ rest = path[1:]
50
+
51
+ if isinstance(doc, dict) and key in doc:
52
+ return _match_gte(doc[key], rest, ov)
53
+
54
+ if isinstance(doc, list) and key.isdigit():
55
+ idx = int(key)
56
+ if idx < len(doc):
57
+ return _match_gte(doc[idx], rest, ov)
58
+
59
+ if isinstance(doc, list):
60
+ return any([_match_gte(d, path, ov) for d in doc])
61
+
62
+ if ov is None:
63
+ return True
64
+
65
+ return False
@@ -0,0 +1,45 @@
1
+ from mgqpy.operators.eq_ne_not import _match_eq
2
+
3
+
4
+ from typing import List
5
+
6
+
7
+ def _match_in(doc, path: List[str], ov) -> bool:
8
+ if not _validate_in_nin(ov):
9
+ return False
10
+
11
+ if len(path) == 0:
12
+ if isinstance(doc, list) and any([_match_in(d, path, ov) for d in doc]):
13
+ return True
14
+
15
+ return any([_match_eq(doc, path, o) for o in ov])
16
+
17
+ key = path[0]
18
+ rest = path[1:]
19
+
20
+ if isinstance(doc, dict) and key in doc:
21
+ return _match_in(doc[key], rest, ov)
22
+
23
+ if isinstance(doc, list) and key.isdigit():
24
+ idx = int(key)
25
+ if idx < len(doc):
26
+ return _match_in(doc[idx], rest, ov)
27
+
28
+ if isinstance(doc, list):
29
+ return any([_match_in(d, path, ov) for d in doc])
30
+
31
+ if None in ov:
32
+ return True
33
+
34
+ return False
35
+
36
+
37
+ def _match_nin(doc, path: List[str], ov) -> bool:
38
+ if not _validate_in_nin(ov):
39
+ return False
40
+
41
+ return not _match_in(doc, path, ov)
42
+
43
+
44
+ def _validate_in_nin(ov):
45
+ return isinstance(ov, list)
mgqpy/operators/lt.py ADDED
@@ -0,0 +1,59 @@
1
+ import operator
2
+ from itertools import zip_longest
3
+ from numbers import Number
4
+ from typing import List
5
+
6
+ from mgqpy.utils import coerce
7
+
8
+
9
+ def _match_lt(doc, path: List[str], ov) -> bool:
10
+ if len(path) == 0:
11
+ if isinstance(doc, list) and any([_match_lt(d, path, ov) for d in doc]):
12
+ return True
13
+
14
+ if isinstance(doc, list) and isinstance(ov, list):
15
+ if doc < ov:
16
+ return True
17
+
18
+ if isinstance(doc, dict) and isinstance(ov, dict):
19
+ if not doc and not ov:
20
+ return False
21
+
22
+ keys = zip_longest(doc.keys(), ov.keys())
23
+ for doc_key, ov_key in keys:
24
+ if doc_key is None:
25
+ return True
26
+ if ov_key is None:
27
+ return False
28
+ if doc_key != ov_key:
29
+ return doc_key < ov_key
30
+ if doc_key == ov_key:
31
+ if doc[doc_key] > ov[ov_key]:
32
+ return False
33
+ if doc[doc_key] < ov[ov_key]:
34
+ return True
35
+ return False
36
+
37
+ try:
38
+ doc, ov = coerce(doc, ov)
39
+ return operator.lt(doc, ov)
40
+ except Exception:
41
+ pass
42
+
43
+ return False
44
+
45
+ key = path[0]
46
+ rest = path[1:]
47
+
48
+ if isinstance(doc, dict) and key in doc:
49
+ return _match_lt(doc[key], rest, ov)
50
+
51
+ if isinstance(doc, list) and key.isdigit():
52
+ idx = int(key)
53
+ if idx < len(doc):
54
+ return _match_lt(doc[idx], rest, ov)
55
+
56
+ if isinstance(doc, list):
57
+ return any([_match_lt(d, path, ov) for d in doc])
58
+
59
+ return False
mgqpy/operators/lte.py ADDED
@@ -0,0 +1,65 @@
1
+ import operator
2
+ from itertools import zip_longest
3
+ from numbers import Number
4
+ from typing import List
5
+
6
+ from mgqpy.utils import coerce
7
+
8
+
9
+ def _match_lte(doc, path: List[str], ov) -> bool:
10
+ if len(path) == 0:
11
+ if isinstance(doc, list) and any([_match_lte(d, path, ov) for d in doc]):
12
+ return True
13
+
14
+ if isinstance(doc, list) and isinstance(ov, list):
15
+ if doc <= ov:
16
+ return True
17
+
18
+ if isinstance(doc, dict) and isinstance(ov, dict):
19
+ if not doc and not ov:
20
+ return True
21
+
22
+ keys = zip_longest(doc.keys(), ov.keys())
23
+ for doc_key, ov_key in keys:
24
+ if doc_key is None:
25
+ return True
26
+ if ov_key is None:
27
+ return False
28
+ if doc_key != ov_key:
29
+ return doc_key < ov_key
30
+ if doc_key == ov_key:
31
+ if doc[doc_key] < ov[ov_key]:
32
+ return True
33
+ if doc[doc_key] > ov[ov_key]:
34
+ return False
35
+ return True
36
+
37
+ if doc is None and ov is None:
38
+ return True
39
+
40
+ try:
41
+ doc, ov = coerce(doc, ov)
42
+ return operator.le(doc, ov)
43
+ except Exception:
44
+ pass
45
+
46
+ return False
47
+
48
+ key = path[0]
49
+ rest = path[1:]
50
+
51
+ if isinstance(doc, dict) and key in doc:
52
+ return _match_lte(doc[key], rest, ov)
53
+
54
+ if isinstance(doc, list) and key.isdigit():
55
+ idx = int(key)
56
+ if idx < len(doc):
57
+ return _match_lte(doc[idx], rest, ov)
58
+
59
+ if isinstance(doc, list):
60
+ return any([_match_lte(d, path, ov) for d in doc])
61
+
62
+ if ov is None:
63
+ return True
64
+
65
+ return False
mgqpy/operators/mod.py ADDED
@@ -0,0 +1,46 @@
1
+ import math
2
+ from numbers import Number
3
+ from typing import List
4
+
5
+
6
+ def _match_mod(doc, path: List[str], ov) -> bool:
7
+ if not _validate_mod(ov):
8
+ return False
9
+
10
+ if len(path) == 0:
11
+ if isinstance(doc, list) and any([_match_mod(d, path, ov) for d in doc]):
12
+ return True
13
+
14
+ if not isinstance(doc, Number):
15
+ return False
16
+
17
+ divisor = math.floor(ov[0])
18
+ expected_remainer = math.floor(ov[1])
19
+ doc_remainer = math.floor(doc % divisor)
20
+
21
+ return doc_remainer == expected_remainer
22
+
23
+ key = path[0]
24
+ rest = path[1:]
25
+
26
+ if isinstance(doc, dict) and key in doc:
27
+ return _match_mod(doc[key], rest, ov)
28
+
29
+ if isinstance(doc, list) and key.isdigit():
30
+ idx = int(key)
31
+ if idx < len(doc):
32
+ return _match_mod(doc[idx], rest, ov)
33
+
34
+ if isinstance(doc, list):
35
+ return any([_match_mod(d, path, ov) for d in doc])
36
+
37
+ return False
38
+
39
+
40
+ def _validate_mod(ov):
41
+ return (
42
+ isinstance(ov, list)
43
+ and len(ov) == 2
44
+ and isinstance(ov[0], Number)
45
+ and isinstance(ov[1], Number)
46
+ )
@@ -0,0 +1,44 @@
1
+ import re
2
+ from typing import List
3
+
4
+
5
+ def _match_regex(doc, path: List[str], ov) -> bool:
6
+ if len(path) == 0:
7
+ if isinstance(doc, list) and any([_match_regex(d, path, ov) for d in doc]):
8
+ return True
9
+
10
+ if not isinstance(doc, str):
11
+ return False
12
+
13
+ flags = re.NOFLAG
14
+ if "i" in ov["$options"]:
15
+ flags = flags | re.IGNORECASE
16
+ if "m" in ov["$options"]:
17
+ flags = flags | re.MULTILINE
18
+ if "s" in ov["$options"]:
19
+ flags = flags | re.DOTALL
20
+ if "x" in ov["$options"]:
21
+ flags = flags | re.VERBOSE
22
+
23
+ matcher = re.compile(ov["$regex"], flags)
24
+
25
+ if matcher.search(doc):
26
+ return True
27
+
28
+ return False
29
+
30
+ key = path[0]
31
+ rest = path[1:]
32
+
33
+ if isinstance(doc, dict) and key in doc:
34
+ return _match_regex(doc[key], rest, ov)
35
+
36
+ if isinstance(doc, list) and key.isdigit():
37
+ idx = int(key)
38
+ if idx < len(doc):
39
+ return _match_regex(doc[idx], rest, ov)
40
+
41
+ if isinstance(doc, list):
42
+ return any([_match_regex(d, path, ov) for d in doc])
43
+
44
+ return False
@@ -0,0 +1,36 @@
1
+ from numbers import Number
2
+ from typing import List
3
+
4
+
5
+ def _match_size(doc, path: List[str], ov) -> bool:
6
+ if not _validate_size(ov):
7
+ return False
8
+
9
+ if len(path) == 0:
10
+ if not isinstance(doc, list):
11
+ return False
12
+
13
+ if len(doc) == int(ov):
14
+ return True
15
+
16
+ return False
17
+
18
+ key = path[0]
19
+ rest = path[1:]
20
+
21
+ if isinstance(doc, dict) and key in doc:
22
+ return _match_size(doc[key], rest, ov)
23
+
24
+ if isinstance(doc, list) and key.isdigit():
25
+ idx = int(key)
26
+ if idx < len(doc):
27
+ return _match_size(doc[idx], rest, ov)
28
+
29
+ if isinstance(doc, list):
30
+ return any([_match_size(d, path, ov) for d in doc])
31
+
32
+ return False
33
+
34
+
35
+ def _validate_size(ov) -> bool:
36
+ return isinstance(ov, Number)
mgqpy/utils.py ADDED
@@ -0,0 +1,89 @@
1
+ """Utility helpers for mgqpy."""
2
+
3
+ import datetime
4
+ import decimal
5
+ import uuid
6
+ from typing import Any, Sequence
7
+
8
+
9
+ def coerce(a: Any, b: Any) -> tuple[Any, Any]:
10
+ """Make *a* and *b* directly comparable by aligning their types.
11
+
12
+ The coercion rules are intentionally conservative - we only convert
13
+ where the *intent* is unambiguous (e.g. ISO-8601 date strings). The
14
+ function never raises: if conversion fails we fall back to the
15
+ original values so the caller can attempt a normal comparison.
16
+ """
17
+
18
+ match (a, b):
19
+ # one is date/datetime, other is str
20
+ case (datetime.datetime() as dt, str()):
21
+ return dt, _try_date(b, datetime.datetime, tzinfo=dt.tzinfo)
22
+ case (datetime.date() as d, str()):
23
+ return d, _try_date(b, datetime.date)
24
+ case (str(), datetime.datetime() as dt):
25
+ return _try_date(a, datetime.datetime, tzinfo=dt.tzinfo), dt
26
+ case (str(), datetime.date() as d):
27
+ return _try_date(a, datetime.date), d
28
+
29
+ # one is decimal
30
+ case (decimal.Decimal(), _):
31
+ return a, _try_decimal(b)
32
+ case (_, decimal.Decimal()):
33
+ return _try_decimal(a), b
34
+
35
+ # one is uuid
36
+ case (uuid.UUID(), _):
37
+ return a, _try_uuid(b)
38
+ case (_, uuid.UUID()):
39
+ return _try_uuid(a), b
40
+
41
+ # default return original values
42
+ case _:
43
+ return a, b
44
+
45
+
46
+ # Datetime ↔︎ ISO 8601 string
47
+ def _try_date(s: str, target, *, tzinfo: datetime.tzinfo | None = None):
48
+ """Attempt to convert *val* to target date or datetime"""
49
+ try:
50
+ if target is datetime.date:
51
+ return datetime.date.fromisoformat(s)
52
+ dt = datetime.datetime.fromisoformat(s)
53
+ if tzinfo is not None and dt.tzinfo is None:
54
+ return dt.replace(tzinfo=tzinfo)
55
+ return dt
56
+ except ValueError:
57
+ # Not a valid ISO format – leave as string so normal
58
+ # comparison semantics apply (and tests expecting a failure
59
+ # still pass).
60
+ return s
61
+
62
+
63
+ # UUID ↔︎ string
64
+ def _try_uuid(val: object):
65
+ """Attempt to convert *val* to UUID"""
66
+ if isinstance(val, uuid.UUID):
67
+ return val
68
+ if isinstance(val, str):
69
+ try:
70
+ return uuid.UUID(val)
71
+ except ValueError:
72
+ return val
73
+ return val
74
+
75
+
76
+ # Decimal ↔︎ number / numeric-string
77
+ def _try_decimal(val: object):
78
+ """Attempt to convert *val* to Decimal"""
79
+ if isinstance(val, decimal.Decimal):
80
+ return val
81
+ if isinstance(val, (int, float, str)) and not isinstance(val, bool):
82
+ try:
83
+ return decimal.Decimal(str(val))
84
+ except (decimal.InvalidOperation, ValueError):
85
+ pass
86
+ return val
87
+
88
+
89
+ __all__: Sequence[str] = ("coerce",)
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.3
2
+ Name: mgqpy
3
+ Version: 0.8.0
4
+ Summary: mongo query as a predicate function
5
+ Author: Chiawei Ong
6
+ Author-email: Chiawei Ong <ongchiawei@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2024 Chiawei Ong
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Classifier: License :: OSI Approved :: MIT License
29
+ Requires-Python: >=3.10
30
+ Project-URL: Home, https://github.com/weiliddat/mgqpy
31
+ Description-Content-Type: text/markdown
32
+
33
+ # mgqpy
34
+
35
+ [![codecov](https://codecov.io/gh/weiliddat/mgqpy/graph/badge.svg?token=CuQS0w5IkL)](https://codecov.io/gh/weiliddat/mgqpy)
36
+ ![PyPI - Version](https://img.shields.io/pypi/v/mgqpy)
37
+
38
+ [MongoDB query](https://www.mongodb.com/docs/manual/reference/operator/query/) as a predicate function
39
+
40
+ This aims to be consistent with how MongoDB's matches documents. This includes traversal across nested dicts and lists, None and field-presence/absence handling.
41
+
42
+ ## Installation
43
+
44
+ ```sh
45
+ pip install mgqpy
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ Test if an input satisfies a query.
51
+
52
+ Invalid queries (e.g. using $and without a list) will automatically return False.
53
+
54
+ Use the `validate` method to raise errors if there's a problem with the query.
55
+
56
+ ```python
57
+ from mgqpy import Query
58
+
59
+ predicate = Query({"foo.bar": {"$gt": 1}})
60
+
61
+ inputs = [
62
+ {"foo": [{"bar": [1, 2]}]},
63
+ {"foo": {"bar": 1}},
64
+ {"foo": {"bar": 2}},
65
+ {"foo": None},
66
+ ]
67
+
68
+ filtered = filter(predicate.test, inputs)
69
+
70
+ assert list(filtered) == [
71
+ {"foo": [{"bar": [1, 2]}]},
72
+ {"foo": {"bar": 2}},
73
+ ]
74
+ ```
75
+
76
+ Use `validate` to validate queries given.
77
+
78
+ ```python
79
+ predicate = Query({"foo": {"$in": 1}})
80
+
81
+ try:
82
+ predicate.validate()
83
+ except TypeError as e:
84
+ # ...
85
+ ```
86
+
87
+ `validate` returns the `Query` instance so you can chain `test` if you wish to validate and test against an input in one go.
88
+
89
+ ```python
90
+ input = {"foo": 1}
91
+
92
+ predicate.validate().test(input)
93
+ ```
94
+
95
+ ## Supported operators
96
+
97
+ Comparison query operators
98
+
99
+ - [x] \$eq
100
+ - [x] \$eq (implicit), e.g. `{"foo": None}`
101
+ - [x] \$ne
102
+ - [x] \$gt
103
+ - [x] \$gte
104
+ - [x] \$lt
105
+ - [x] \$lte
106
+ - [x] \$in
107
+ - [x] \$nin
108
+
109
+ Logical query operators
110
+
111
+ - [x] \$and
112
+ - [x] \$and (implicit), e.g. `{"foo": 1, "bar": "baz"}`
113
+ - [x] \$or
114
+ - [x] \$not
115
+ - [x] \$nor
116
+
117
+ Evaluation query operators
118
+
119
+ - [x] \$regex
120
+ - [x] \$regex (implicit), e.g. `{"foo": re.compile('^bar')}`
121
+ - [x] \$mod
122
+
123
+ Array query operators
124
+
125
+ - [x] \$all
126
+ - [x] \$elemMatch
127
+ - [x] \$size
@@ -0,0 +1,17 @@
1
+ mgqpy/__init__.py,sha256=eouUh0LAX-EMCqwTpb6n-6qP9Qs3XhTQx25Hzog5h1Q,6402
2
+ mgqpy/operators/all.py,sha256=KyllXvvaP30lVL0NwHNPp4IwPytjgVn_nfQEcYLpJ_c,1350
3
+ mgqpy/operators/and_or_nor.py,sha256=jMLG1cxBOHwToKWxD2eROUY1LjuJ4SrQWscVfWiiXfA,697
4
+ mgqpy/operators/elem_match.py,sha256=HjMi4zurci0jp3j4h6XrrGZv_K028PavCQ2j_uvMhSo,695
5
+ mgqpy/operators/eq_ne_not.py,sha256=4EGFnd7_cANOarzXrD_QOFUkURDbj-7ZFz05GWeSrtc,1023
6
+ mgqpy/operators/gt.py,sha256=QXF5dnRzw-6XwgMSr0Nfk8Ll5vxab75bYFu2VJ0f6sA,1576
7
+ mgqpy/operators/gte.py,sha256=HFzHZhlceJnxiOvq_-Aw_I-vvYFdRggtJf11t9gdVfw,1749
8
+ mgqpy/operators/in_nin.py,sha256=mEI1VKW88sWhzKfEsq7WyR5IHfYrWXhEtM1laHZFb1o,999
9
+ mgqpy/operators/lt.py,sha256=zTLq2FAL89UrnUs8tqqlqSxqlCKfXB8HSiqq7YNTgIM,1641
10
+ mgqpy/operators/lte.py,sha256=GqUnmncH6nsF6g_yGODrK5zoU2MY2Rl-h2byJmzcg6U,1749
11
+ mgqpy/operators/mod.py,sha256=ULy9slBK0WtHBLytsGWmJmpYBg0AXZ407Lqdhm4UdV8,1106
12
+ mgqpy/operators/regex.py,sha256=LrtW3g2DNHYVHBX81WyaJwf5EbERGeZzReIfo6Yk278,1129
13
+ mgqpy/operators/size.py,sha256=TinEpCQ5WuVXiYXKMpQjV_3B9S4PTuEiulcvb6qDSf8,781
14
+ mgqpy/utils.py,sha256=C555omp9WRipT1ZSTLoMP87dl6QHtuuUD1H1gDxycDM,2775
15
+ mgqpy-0.8.0.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
16
+ mgqpy-0.8.0.dist-info/METADATA,sha256=KZwMLxSAGYJmxHIXVQwDwI5Cd3p79Jz7bkQ_DXCuq2M,3484
17
+ mgqpy-0.8.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.9.21
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any