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 +177 -0
- mgqpy/operators/all.py +52 -0
- mgqpy/operators/and_or_nor.py +31 -0
- mgqpy/operators/elem_match.py +31 -0
- mgqpy/operators/eq_ne_not.py +45 -0
- mgqpy/operators/gt.py +56 -0
- mgqpy/operators/gte.py +65 -0
- mgqpy/operators/in_nin.py +45 -0
- mgqpy/operators/lt.py +59 -0
- mgqpy/operators/lte.py +65 -0
- mgqpy/operators/mod.py +46 -0
- mgqpy/operators/regex.py +44 -0
- mgqpy/operators/size.py +36 -0
- mgqpy/utils.py +89 -0
- mgqpy-0.8.0.dist-info/METADATA +127 -0
- mgqpy-0.8.0.dist-info/RECORD +17 -0
- mgqpy-0.8.0.dist-info/WHEEL +4 -0
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
|
+
)
|
mgqpy/operators/regex.py
ADDED
|
@@ -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
|
mgqpy/operators/size.py
ADDED
|
@@ -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
|
+
[](https://codecov.io/gh/weiliddat/mgqpy)
|
|
36
|
+

|
|
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,,
|