mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Implement condition negation
This commit is contained in:
@ -6,17 +6,15 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
LOGIC_TYPES = (
|
||||
'and',
|
||||
'or'
|
||||
)
|
||||
AND = 'and'
|
||||
OR = 'or'
|
||||
|
||||
|
||||
def is_ruleset(data):
|
||||
"""
|
||||
Determine whether the given dictionary looks like a rule set.
|
||||
"""
|
||||
return type(data) is dict and len(data) == 1 and list(data.keys())[0] in LOGIC_TYPES
|
||||
return type(data) is dict and len(data) == 1 and list(data.keys())[0] in (AND, OR)
|
||||
|
||||
|
||||
class Condition:
|
||||
@ -28,7 +26,6 @@ class Condition:
|
||||
:param op: The logical operation to use when evaluating the value (default: 'eq')
|
||||
"""
|
||||
EQ = 'eq'
|
||||
NEQ = 'neq'
|
||||
GT = 'gt'
|
||||
GTE = 'gte'
|
||||
LT = 'lt'
|
||||
@ -37,18 +34,18 @@ class Condition:
|
||||
CONTAINS = 'contains'
|
||||
|
||||
OPERATORS = (
|
||||
EQ, NEQ, GT, GTE, LT, LTE, IN, CONTAINS
|
||||
EQ, GT, GTE, LT, LTE, IN, CONTAINS
|
||||
)
|
||||
|
||||
TYPES = {
|
||||
str: (EQ, NEQ, CONTAINS),
|
||||
bool: (EQ, NEQ, CONTAINS),
|
||||
int: (EQ, NEQ, GT, GTE, LT, LTE, CONTAINS),
|
||||
float: (EQ, NEQ, GT, GTE, LT, LTE, CONTAINS),
|
||||
list: (EQ, NEQ, IN, CONTAINS)
|
||||
str: (EQ, CONTAINS),
|
||||
bool: (EQ, CONTAINS),
|
||||
int: (EQ, GT, GTE, LT, LTE, CONTAINS),
|
||||
float: (EQ, GT, GTE, LT, LTE, CONTAINS),
|
||||
list: (EQ, IN, CONTAINS)
|
||||
}
|
||||
|
||||
def __init__(self, attr, value, op=EQ):
|
||||
def __init__(self, attr, value, op=EQ, negate=False):
|
||||
if op not in self.OPERATORS:
|
||||
raise ValueError(f"Unknown operator: {op}. Must be one of: {', '.join(self.OPERATORS)}")
|
||||
if type(value) not in self.TYPES:
|
||||
@ -59,13 +56,18 @@ class Condition:
|
||||
self.attr = attr
|
||||
self.value = value
|
||||
self.eval_func = getattr(self, f'eval_{op}')
|
||||
self.negate = negate
|
||||
|
||||
def eval(self, data):
|
||||
"""
|
||||
Evaluate the provided data to determine whether it matches the condition.
|
||||
"""
|
||||
value = functools.reduce(dict.get, self.attr.split('.'), data)
|
||||
return self.eval_func(value)
|
||||
result = self.eval_func(value)
|
||||
|
||||
if self.negate:
|
||||
return not result
|
||||
return result
|
||||
|
||||
# Equivalency
|
||||
|
||||
@ -104,7 +106,7 @@ class ConditionSet:
|
||||
|
||||
{"and": [
|
||||
{"attr": "foo", "op": "eq", "value": 1},
|
||||
{"attr": "bar", "op": "neq", "value": 2}
|
||||
{"attr": "bar", "op": "eq", "value": 2, "negate": true}
|
||||
]}
|
||||
|
||||
:param ruleset: A dictionary mapping a logical operator to a list of conditional rules
|
||||
@ -117,8 +119,8 @@ class ConditionSet:
|
||||
|
||||
# Determine the logic type
|
||||
logic = list(ruleset.keys())[0]
|
||||
if type(logic) is not str or logic.lower() not in LOGIC_TYPES:
|
||||
raise ValueError(f"Invalid logic type: {logic} (must be 'and' or 'or')")
|
||||
if type(logic) is not str or logic.lower() not in (AND, OR):
|
||||
raise ValueError(f"Invalid logic type: {logic} (must be '{AND}' or '{OR}')")
|
||||
self.logic = logic.lower()
|
||||
|
||||
# Compile the set of Conditions
|
||||
|
@ -48,8 +48,8 @@ class ConditionTestCase(TestCase):
|
||||
self.assertTrue(c.eval({'x': 1}))
|
||||
self.assertFalse(c.eval({'x': 2}))
|
||||
|
||||
def test_neq(self):
|
||||
c = Condition('x', 1, 'neq')
|
||||
def test_eq_negated(self):
|
||||
c = Condition('x', 1, 'eq', negate=True)
|
||||
self.assertFalse(c.eval({'x': 1}))
|
||||
self.assertTrue(c.eval({'x': 2}))
|
||||
|
||||
@ -80,11 +80,21 @@ class ConditionTestCase(TestCase):
|
||||
self.assertTrue(c.eval({'x': 1}))
|
||||
self.assertFalse(c.eval({'x': 9}))
|
||||
|
||||
def test_in_negated(self):
|
||||
c = Condition('x', [1, 2, 3], 'in', negate=True)
|
||||
self.assertFalse(c.eval({'x': 1}))
|
||||
self.assertTrue(c.eval({'x': 9}))
|
||||
|
||||
def test_contains(self):
|
||||
c = Condition('x', 1, 'contains')
|
||||
self.assertTrue(c.eval({'x': [1, 2, 3]}))
|
||||
self.assertFalse(c.eval({'x': [2, 3, 4]}))
|
||||
|
||||
def test_contains_negated(self):
|
||||
c = Condition('x', 1, 'contains', negate=True)
|
||||
self.assertFalse(c.eval({'x': [1, 2, 3]}))
|
||||
self.assertTrue(c.eval({'x': [2, 3, 4]}))
|
||||
|
||||
|
||||
class ConditionSetTest(TestCase):
|
||||
|
||||
@ -100,11 +110,11 @@ class ConditionSetTest(TestCase):
|
||||
cs = ConditionSet({
|
||||
'and': [
|
||||
{'attr': 'a', 'value': 1, 'op': 'eq'},
|
||||
{'attr': 'b', 'value': 2, 'op': 'eq'},
|
||||
{'attr': 'b', 'value': 1, 'op': 'eq', 'negate': True},
|
||||
]
|
||||
})
|
||||
self.assertTrue(cs.eval({'a': 1, 'b': 2}))
|
||||
self.assertFalse(cs.eval({'a': 1, 'b': 3}))
|
||||
self.assertFalse(cs.eval({'a': 1, 'b': 1}))
|
||||
|
||||
def test_or_single_depth(self):
|
||||
cs = ConditionSet({
|
||||
|
Reference in New Issue
Block a user