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