Skip to content

Commit 86fcd7c

Browse files
feat: ignore rules
* feat: add config_dict and splitted rules capabilities Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * chore: fix pre-commit warnings Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * docs: add feature explanations Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * docs: typos Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * tests: typos Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * ✨ Add ignore rule feature * ✨ Add ignore rule feature * chore: refactoring Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> * ci: fix pre-commit hooks Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> --------- Signed-off-by: develop-cs <43383361+develop-cs@users.noreply.github.com> Signed-off-by: Charles <43383361+develop-cs@users.noreply.github.com> Co-authored-by: develop-cs <43383361+develop-cs@users.noreply.github.com>
1 parent 3bcf4e6 commit 86fcd7c

File tree

8 files changed

+375
-139
lines changed

8 files changed

+375
-139
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ repos:
2525
exclude: ^(docs/)
2626
- id: check-added-large-files
2727
args: ['--maxkb=500']
28-
- id: no-commit-to-branch
29-
args: ['--branch', 'main']
3028
- repo: https://github.com/astral-sh/ruff-pre-commit
3129
rev: v0.5.4
3230
hooks:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
99
### Features
1010

1111
* Add a new parameter `config_dict` in the `RulesEngine`'s constructor. It can be used when you have already loaded the YAML configuration in a dictionary and want to use it straightforward.
12+
* Add a new parameter `ignored_rules` in the `apply_rules()` method. It can be used to easily disable a rule by its id.
1213
* Split a rule set in two (or more) files (keep the rules organized by their file names [alphabetically sorted]).
1314

1415
### Fixes

src/arta/_engine.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def __init__(
6363
rules_dict: A dictionary containing the rules' definitions.
6464
config_path: Path of a directory containing the YAML files.
6565
config_dict: A dictionary containing the configuration (same as YAML files but already
66-
parsed in a dictionary).
66+
parsed in a dictionary).
6767
6868
Raises:
6969
KeyError: Key not found.
@@ -155,7 +155,13 @@ def __init__(
155155
)
156156

157157
def apply_rules(
158-
self, input_data: dict[str, Any], *, rule_set: str | None = None, verbose: bool = False, **kwargs: Any
158+
self,
159+
input_data: dict[str, Any],
160+
*,
161+
rule_set: str | None = None,
162+
ignored_rules: set[str] | None = None,
163+
verbose: bool = False,
164+
**kwargs: Any,
159165
) -> dict[str, Any]:
160166
"""Apply the rules and return results.
161167
@@ -170,6 +176,7 @@ def apply_rules(
170176
Args:
171177
input_data: Input data to apply rules on.
172178
rule_set: Apply rules associated with the specified rule set.
179+
ignored_rules: A set/list of rule's ids to be ignored/disabled during evaluation.
173180
verbose: If True, add extra ids (group_id, rule_id) for result explicability.
174181
**kwargs: For user extra arguments.
175182
@@ -190,6 +197,7 @@ def apply_rules(
190197

191198
# Var init.
192199
input_data_copy: dict[str, Any] = copy.deepcopy(input_data)
200+
ignored_ids: set[str] = ignored_rules if ignored_rules is not None else set()
193201

194202
# Prepare the result key
195203
input_data_copy["output"] = {}
@@ -215,6 +223,10 @@ def apply_rules(
215223

216224
# Rules' loop (inside a group)
217225
for rule in rules_list:
226+
if rule._rule_id in ignored_ids:
227+
# Ignore that rule
228+
continue
229+
218230
# Apply rules
219231
action_result, rule_details = rule.apply(
220232
input_data_copy, parsing_error_strategy=self._parsing_error_strategy, **kwargs
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
rules:
3+
default_rule_set:
4+
admission:
5+
ADM_OK:
6+
condition: HAS_SCHOOL_AUTHORIZED_POWER
7+
action: set_admission
8+
action_parameters:
9+
value: true
10+
ADM_KO:
11+
condition: null
12+
action: set_admission
13+
action_parameters:
14+
value: false
15+
course:
16+
COURSE_ENGLISH:
17+
condition: IS_SPEAKING_ENGLISH and not(IS_AGE_UNKNOWN)
18+
action: set_student_course
19+
action_parameters:
20+
course_id: "english"
21+
COURSE_SENIOR:
22+
condition: IS_AGE_UNKNOWN
23+
action: set_student_course
24+
action_parameters:
25+
course_id: "senior"
26+
COURSE_INTERNATIONAL:
27+
condition: not(IS_SPEAKING_ENGLISH)
28+
action: set_student_course
29+
action_parameters:
30+
course_id: "international"
31+
email:
32+
EMAIL_COOK:
33+
condition: HAS_SCHOOL_AUTHORIZED_POWER
34+
action: send_email
35+
action_parameters:
36+
mail_to: "cook@super-heroes.test"
37+
mail_content: "Thanks for preparing once a month the following dish:"
38+
meal: input.favorite_meal
39+
40+
conditions:
41+
HAS_SCHOOL_AUTHORIZED_POWER:
42+
description: "Does it have school authorized power?"
43+
validation_function: has_authorized_super_power
44+
condition_parameters:
45+
authorized_powers:
46+
- "strength"
47+
- "fly"
48+
- "immortality"
49+
candidate_powers: input.powers
50+
IS_SPEAKING_FRENCH:
51+
description: "Does it speak french?"
52+
validation_function: is_speaking_language
53+
condition_parameters:
54+
value: "french"
55+
spoken_language: input.language
56+
IS_SPEAKING_ENGLISH:
57+
description: "Does it speak english?"
58+
validation_function: is_speaking_language
59+
condition_parameters:
60+
value: "english"
61+
spoken_language: input.language
62+
IS_AGE_UNKNOWN:
63+
description: "Do we know his age?"
64+
validation_function: is_age_unknown
65+
condition_parameters:
66+
age: input.age
67+
HAS_FAVORITE_MEAL:
68+
description: "Does it have a favorite meal?"
69+
validation_function: has_favorite_meal
70+
condition_parameters:
71+
favorite_meal: input.favorite_meal
72+
73+
74+
conditions_source_modules:
75+
- "tests.examples.code.conditions"
76+
actions_source_modules:
77+
- "tests.examples.code.actions"
78+
79+
parsing_error_strategy: raise
80+
81+
custom_classes_source_modules:
82+
- "tests.examples.code.custom_class"
83+
condition_factory_mapping:
84+
custom_condition: "CustomCondition"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
# Global settings
3+
actions_source_modules:
4+
- "tests.examples.code.actions"
5+
6+
# Rule sets for simple conditions tests
7+
rules:
8+
default_rule_set:
9+
admission:
10+
IGNORED_RULE_1:
11+
simple_condition: input.power=="strength"
12+
action: set_admission
13+
action_parameters:
14+
value: true
15+
IGNORED_RULE_2:
16+
simple_condition: input.dummy>1
17+
action: set_admission
18+
action_parameters:
19+
value: true
20+
ADM_KO:
21+
simple_condition: null
22+
action: set_admission
23+
action_parameters:
24+
value: false
25+
26+
parsing_error_strategy: raise

tests/unit/test_engine.py renamed to tests/unit/test_engine_with_conf.py

Lines changed: 46 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,6 @@ def test_instanciation(base_config_path):
1818
eng_2 = RulesEngine(config_dict=config_dict)
1919
assert isinstance(eng_2, RulesEngine)
2020

21-
# Dummy action function
22-
set_value = lambda value, **kwargs: {"value": value}
23-
24-
raw_rules = {
25-
"type": {
26-
"rule_str": {
27-
"condition": lambda x: isinstance(x, str),
28-
"condition_parameters": {"x": "input.to_check"},
29-
"action": set_value,
30-
"action_parameters": {"value": "String"},
31-
},
32-
"rule_other": {
33-
"condition": None,
34-
"condition_parameters": None,
35-
"action": set_value,
36-
"action_parameters": {"value": "other type"},
37-
},
38-
}
39-
}
40-
# Dictionary instanciation
41-
eng_3 = RulesEngine(rules_dict=raw_rules)
42-
assert isinstance(eng_3, RulesEngine)
43-
4421

4522
@pytest.mark.parametrize(
4623
"input_data, config_dir, rule_set, verbose, good_results",
@@ -280,6 +257,22 @@ def test_instanciation(base_config_path):
280257
"email": True,
281258
},
282259
),
260+
(
261+
{
262+
"age": None,
263+
"language": "french",
264+
"powers": ["strength", "fly"],
265+
"favorite_meal": "Spinach",
266+
},
267+
"ignored_rules",
268+
"default_rule_set",
269+
False,
270+
{
271+
"admission": {"admission": True},
272+
"course": {"course_id": "senior"},
273+
"email": True,
274+
},
275+
),
283276
],
284277
)
285278
def test_conf_apply_rules(input_data, config_dir, rule_set, verbose, good_results, base_config_path):
@@ -295,115 +288,6 @@ def test_conf_apply_rules(input_data, config_dir, rule_set, verbose, good_result
295288
assert res_1 == res_2 == good_results
296289

297290

298-
@pytest.mark.parametrize(
299-
"input_data, verbose, good_results",
300-
[
301-
(
302-
{
303-
"age": 5000,
304-
"language": "english",
305-
"power": "immortality",
306-
"favorite_meal": None,
307-
"weapons": ["Magic lasso", "Bulletproof bracelets", "Sword", "Shield"],
308-
},
309-
True,
310-
{
311-
"verbosity": {
312-
"rule_set": "default_rule_set",
313-
"results": [
314-
{
315-
"rule_group": "power_level",
316-
"verified_conditions": {
317-
"condition": {"expression": "USER_CONDITION", "values": {"USER_CONDITION": True}}
318-
},
319-
"activated_rule": "super_power",
320-
"action_result": "super",
321-
},
322-
{
323-
"rule_group": "admission",
324-
"verified_conditions": {
325-
"condition": {"expression": "USER_CONDITION", "values": {"USER_CONDITION": True}}
326-
},
327-
"activated_rule": "admitted",
328-
"action_result": "Admitted!",
329-
},
330-
],
331-
},
332-
"power_level": "super",
333-
"admission": "Admitted!",
334-
},
335-
),
336-
(
337-
{
338-
"age": 5000,
339-
"language": "english",
340-
"power": "immortality",
341-
"favorite_meal": None,
342-
"weapons": ["Magic lasso", "Bulletproof bracelets", "Sword", "Shield"],
343-
},
344-
False,
345-
{"power_level": "super", "admission": "Admitted!"},
346-
),
347-
],
348-
)
349-
def test_dict_apply_rules(input_data, verbose, good_results):
350-
"""Unit test of the method RulesEngine.apply_rules() when init was done using rule_dict"""
351-
# Dummy action function
352-
353-
def is_a_super_power(level, **kwargs):
354-
"""Dummy validation function."""
355-
return level == "super"
356-
357-
def admit(**kwargs):
358-
"""Dummy action function."""
359-
return "Admitted!"
360-
361-
def set_value(value, **kwargs):
362-
"""Dummy action function."""
363-
return value
364-
365-
rules_raw = {
366-
"power_level": {
367-
"super_power": {
368-
"condition": lambda p: p in ["immortality", "time_travelling", "invisibility"],
369-
"condition_parameters": {"p": "input.power"},
370-
"action": lambda x, **kwargs: x,
371-
"action_parameters": {"x": "super"},
372-
},
373-
"minor_power": {
374-
"condition": lambda p: p in ["juggle", "sing", "sleep"],
375-
"condition_parameters": {"p": "input.power"},
376-
"action": lambda x, **kwargs: x,
377-
"action_parameters": {"x": "minor"},
378-
},
379-
"no_power": {
380-
"condition": None,
381-
"condition_parameters": None,
382-
"action": lambda x, **kwargs: x,
383-
"action_parameters": {"x": "no_power"},
384-
},
385-
},
386-
"admission": {
387-
"admitted": {
388-
"condition": is_a_super_power,
389-
"condition_parameters": {"level": "output.power_level"},
390-
"action": admit,
391-
"action_parameters": None,
392-
},
393-
"not_admitted": {
394-
"condition": None,
395-
"condition_parameters": None,
396-
"action": set_value,
397-
"action_parameters": {"value": "Not admitted :-("},
398-
},
399-
},
400-
}
401-
402-
eng_2 = RulesEngine(rules_dict=rules_raw)
403-
res = eng_2.apply_rules(input_data, verbose=verbose)
404-
assert res == good_results
405-
406-
407291
def test_ignore_global_strategy(base_config_path):
408292
"""Unit test of the class RulesEngine"""
409293
# Config. instanciation
@@ -440,3 +324,33 @@ def test_kwargs_in_apply_rules(input_data, good_results, base_config_path):
440324
res = eng.apply_rules(input_data, rule_set="fourth_rule_set", my_parameter="super@connection")
441325

442326
assert res == good_results
327+
328+
329+
@pytest.mark.parametrize(
330+
"input_data, config_dir, rule_set, ignored_rules, good_results",
331+
[
332+
(
333+
{
334+
"age": None,
335+
"language": "french",
336+
"powers": ["strength", "fly"],
337+
"favorite_meal": "Spinach",
338+
},
339+
"ignored_rules",
340+
"default_rule_set",
341+
{"ADM_OK"},
342+
{
343+
"admission": {"admission": False},
344+
"course": {"course_id": "senior"},
345+
"email": True,
346+
},
347+
),
348+
],
349+
)
350+
def test_conf_ignored_rules(input_data, config_dir, rule_set, ignored_rules, good_results, base_config_path):
351+
"""UT of ignored rules."""
352+
path = os.path.join(base_config_path, config_dir)
353+
eng = RulesEngine(config_path=path)
354+
res = eng.apply_rules(input_data=input_data, rule_set=rule_set, ignored_rules=ignored_rules)
355+
356+
assert res == good_results

0 commit comments

Comments
 (0)