├── jqqb_evaluator ├── __init__.py ├── evaluator.py ├── rule_group.py ├── rule.py └── operators.py ├── requirements.txt ├── .gitignore ├── operators └── __pycache__ │ └── operators.cpython-37.pyc ├── setup.py ├── LICENSE └── README.md /jqqb_evaluator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytimeparse~=1.1.8 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | jqqb_evaluator.egg-info/ 4 | venv 5 | .idea -------------------------------------------------------------------------------- /operators/__pycache__/operators.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shunyeka/jQuery-QueryBuilder-Python-Evaluator/HEAD/operators/__pycache__/operators.cpython-37.pyc -------------------------------------------------------------------------------- /jqqb_evaluator/evaluator.py: -------------------------------------------------------------------------------- 1 | import json 2 | from jqqb_evaluator.rule_group import RuleGroup 3 | 4 | 5 | class Evaluator: 6 | 7 | def __init__(self, rule_set): 8 | if isinstance(rule_set, str): 9 | self.parsed_rule_set = json.loads(rule_set) 10 | else: 11 | self.parsed_rule_set = rule_set 12 | 13 | def get_matching_objects(self, objects): 14 | return list(filter(lambda x: self.object_matches_rules(x), objects)) 15 | 16 | def object_matches_rules(self, obj): 17 | return RuleGroup(self.parsed_rule_set).evaluate(obj) 18 | -------------------------------------------------------------------------------- /jqqb_evaluator/rule_group.py: -------------------------------------------------------------------------------- 1 | from jqqb_evaluator.rule import Rule 2 | 3 | 4 | class RuleGroup: 5 | def __init__(self, rule_group_dict): 6 | self.condition = rule_group_dict['condition'] 7 | self.rules = rule_group_dict['rules'] 8 | 9 | def evaluate(self, obj): 10 | if self.condition == 'AND': 11 | return all(map(lambda x: RuleGroup.get_rule_object(x).evaluate(obj), self.rules)) 12 | else: 13 | return any(map(lambda x: RuleGroup.get_rule_object(x).evaluate(obj), self.rules)) 14 | 15 | @staticmethod 16 | def get_rule_object(rule): 17 | if 'rules' in rule: 18 | return RuleGroup(rule) 19 | return Rule(rule) 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="jqqb_evaluator", 8 | version="0.0.1", 9 | author="Amit Chotaliya", 10 | author_email="amit@shunyeka.com", 11 | description="Python evaluator for jQuery-QueryBuilder rules", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/shunyeka/jQuery-QueryBuilder-Python-Evaluator", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | install_requires=[ 22 | 'pytimeparse~=1.1.8' 23 | ], 24 | python_requires='>=3.6', 25 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ShunEka Systems Prvitate Limited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jQuery-QueryBuilder-Python-Evaluator 2 | Python Rule evaluator for jQuery-QueryBuilder. It evaluates rules agains provided objects. 3 | 4 | [Website](http://www.shunyeka.com) • [autobotAI Cloud Governance](https://autobot.live/) 5 | 6 | Inspired from [SixiS/jquery_query_builder-rails](https://github.com/SixiS/jquery_query_builder-rails) 7 | 8 | ## Usage 9 | 10 | Install the package. 11 | 12 | ``` 13 | pip install jqqb-evaluator 14 | ``` 15 | 16 | Usage Example: 17 | 18 | ```py 19 | from jqqb_evaluator.evaluator import Evaluator 20 | rule_json = { 21 | "condition": "AND", 22 | "rules": [{ 23 | "id": "tagname", 24 | "field": "tags.name", 25 | "type": "string", 26 | "input": "text", 27 | "operator": "not_contains", 28 | "value": "production" 29 | }, { 30 | "id": "tagname", 31 | "field": "tags.name", 32 | "type": "string", 33 | "input": "text", 34 | "operator": "begins_with", 35 | "value": "development" 36 | }, { 37 | "condition": "OR", 38 | "rules": [{ 39 | "id": "type", 40 | "field": "type", 41 | "type": "string", 42 | "input": "text", 43 | "operator": "equal", 44 | "value": "ec2" 45 | },{ 46 | "id": "type", 47 | "field": "type", 48 | "type": "string", 49 | "input": "text", 50 | "operator": "equal", 51 | "value": "ami" 52 | }] 53 | }] 54 | } 55 | 56 | 57 | evaluator = Evaluator(rule_json) 58 | object_1 = {'type': "ec2", "tags": [{"name": "hello"}, {"name": "asdfasfproduction_instance"}]} 59 | object_2 = {'type': "ami", "tags": [{"name": "development"}, {"name": "asfdafdroduction_instance"}, {"name": "proction"}]} 60 | objects = [object_1, object_2] 61 | 62 | print(evaluator.get_matching_objects(objects)) 63 | ``` 64 | 65 | Result: 66 | 67 | ```output 68 | [{'type': 'ami', 'tags': [{'name': 'development'}, {'name': 'asfdafdroduction_instance'}, {'name': 'proction'}]}] 69 | ``` -------------------------------------------------------------------------------- /jqqb_evaluator/rule.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pytimeparse.timeparse import timeparse 4 | from jqqb_evaluator.operators import Operators 5 | 6 | 7 | class Rule: 8 | 9 | def __init__(self, rule_dict): 10 | self.id = rule_dict['id'] 11 | self.field = rule_dict['field'] 12 | self.type = rule_dict['type'] 13 | self.input = rule_dict['input'] 14 | self.operator = rule_dict['operator'] 15 | self.value = rule_dict['value'] 16 | 17 | def evaluate(self, obj): 18 | return self.get_operator()(self.get_input(obj), self.get_value()) 19 | 20 | def get_operator(self): 21 | return getattr(Operators, 'eval_' + self.operator) 22 | 23 | def get_input(self, obj): 24 | fields = self.field.split(".") 25 | result = obj 26 | steps = len(fields) 27 | for i in range(steps): 28 | last_step = i == steps - 1 29 | second_last_step = i == steps - 2 30 | result = result.get(fields[i]) 31 | if second_last_step and isinstance(result, list) and isinstance(result[0], dict): 32 | result = [x[fields[steps - 1]] for x in result] 33 | break 34 | result = result[0] if (result is not None and isinstance(result, list) and not last_step) else result 35 | if result is None: 36 | break 37 | if isinstance(result, list): 38 | return list(map(lambda x: self.typecast_value(x), result)) 39 | else: 40 | return self.typecast_value(result) 41 | 42 | def get_value(self): 43 | if isinstance(self.value, list): 44 | return list(map(lambda x: self.typecast_value(x), self.value)) 45 | return self.typecast_value(self.value) 46 | 47 | def typecast_value(self, value_to_cast): 48 | if value_to_cast is None: 49 | return None 50 | 51 | if self.type == 'string': 52 | return str(value_to_cast) 53 | elif self.type == 'integer': 54 | return int(value_to_cast) 55 | elif self.type == 'double': 56 | return float(value_to_cast) 57 | elif self.type == 'date' or self.type == 'datetime': 58 | return datetime.strptime(value_to_cast, "%Y-%m-%dT%H:%M:%S.%fZ") if isinstance(value_to_cast, 59 | str) else value_to_cast 60 | elif self.type == 'time': 61 | return timeparse(value_to_cast) if isinstance(value_to_cast, str) else value_to_cast 62 | elif self.type == 'boolean': 63 | if isinstance(value_to_cast, str): 64 | return value_to_cast.lower() in ['true', '1', 't', 'y', 'yes', 'yeah', 'yup', 'certainly', 'uh-huh'] 65 | return value_to_cast 66 | -------------------------------------------------------------------------------- /jqqb_evaluator/operators.py: -------------------------------------------------------------------------------- 1 | class Operators: 2 | 3 | #left = The property of the object being evaluated. 4 | #right = The value that was entered/selected by the user from the frontend (rule `value` property) 5 | 6 | @staticmethod 7 | def eval_begins_with(left, right): 8 | if isinstance(left, list): 9 | return any(map(lambda x: x.startswith(right), left)) 10 | return left.startswith(right) 11 | 12 | @staticmethod 13 | def eval_between(input, bounds): 14 | return bounds[0] < input < bounds[1] 15 | 16 | @staticmethod 17 | def eval_contains(left, right): 18 | if isinstance(right, list): 19 | if isinstance(left, list): 20 | return any([any(map(lambda x: r in x, left)) for r in right]) 21 | return any([r == left for r in right]) 22 | else: 23 | if isinstance(left, list): 24 | return any(map(lambda x: right in x, left)) 25 | return right in left 26 | 27 | @staticmethod 28 | def eval_ends_with(left, right): 29 | if isinstance(left, list): 30 | return any(map(lambda x: x.endswith(right), left)) 31 | return left.endswith(right) 32 | 33 | @staticmethod 34 | def eval_equal(left, right): 35 | if isinstance(left, list): 36 | return any(map(lambda x: x == right, left)) 37 | return left == right 38 | 39 | @staticmethod 40 | def eval_greater(left, right): 41 | if isinstance(left, list): 42 | return any(map(lambda x: x > right, left)) 43 | return left > right 44 | 45 | @staticmethod 46 | def eval_greater_or_equal(left, right): 47 | if isinstance(left, list): 48 | return any(map(lambda x: x >= right, left)) 49 | return left >= right 50 | 51 | @staticmethod 52 | def eval_in(left, right): 53 | return left in right 54 | 55 | @staticmethod 56 | def eval_is_empty(input, value): 57 | if isinstance(input, list): 58 | return not bool(input) 59 | return not bool(input and input.strip()) 60 | 61 | @staticmethod 62 | def eval_is_not_empty(input, value): 63 | if isinstance(input, list): 64 | return bool(input) 65 | return bool(input and input.strip()) 66 | 67 | @staticmethod 68 | def eval_is_not_null(input, value): 69 | return input is not None 70 | 71 | @staticmethod 72 | def eval_is_null(input, value): 73 | return input is None 74 | 75 | @staticmethod 76 | def eval_less(left, right): 77 | if isinstance(left, list): 78 | return any(map(lambda x: x < right, left)) 79 | return left < right 80 | 81 | @staticmethod 82 | def eval_less_or_equal(left, right): 83 | if isinstance(left, list): 84 | return any(map(lambda x: x <= right, left)) 85 | return left <= right 86 | 87 | @staticmethod 88 | def eval_not_begins_with(left, right): 89 | if isinstance(left, list): 90 | return not any(map(lambda x: x.startswith(right), left)) 91 | return not left.startswith(right) 92 | 93 | @staticmethod 94 | def eval_not_between(input, bounds): 95 | if isinstance(input, list): 96 | return not any(map(lambda x: x <= bounds[0] or x >= bounds[1], input)) 97 | return input <= bounds[0] or input >= bounds[1] 98 | 99 | @staticmethod 100 | def eval_not_contains(left, right): 101 | if isinstance(right, list): 102 | if isinstance(left, list): 103 | return not any([any(map(lambda x: r in x, left)) for r in right]) 104 | return not any([r == left for r in right]) 105 | else: 106 | if isinstance(left, list): 107 | return not any(map(lambda x: right in x, left)) 108 | return right not in left 109 | 110 | @staticmethod 111 | def eval_not_ends_with(left, right): 112 | if isinstance(left, list): 113 | return not any(map(lambda x: x.endswith(right), left)) 114 | return not left.endswith(right) 115 | 116 | @staticmethod 117 | def eval_not_equal(left, right): 118 | if isinstance(left, list): 119 | return not any(map(lambda x: x == right, left)) 120 | return left != right 121 | 122 | @staticmethod 123 | def eval_not_in(left, right): 124 | return left not in right 125 | --------------------------------------------------------------------------------