Skip to content

Commit

Permalink
feat: rule activation mode
Browse files Browse the repository at this point in the history
Signed-off-by: develop-cs <[email protected]>
  • Loading branch information
develop-cs committed Dec 13, 2024
1 parent 81e2498 commit 001d4e9
Show file tree
Hide file tree
Showing 21 changed files with 380 additions and 46 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repos:
- id: check-added-large-files
args: [--maxkb=500]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
rev: v0.8.2
hooks:
- id: ruff
args: [--fix]
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.2] - November, 2024
## [0.9.0] - December, 2024

### Features

* Add a new configuration setting for rules' execution: `rule_activation_mode` (#38).

### Maintenance

Expand Down
6 changes: 4 additions & 2 deletions docs/mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ nav:
- How to: how_to.md
- Glossary: glossary.md
- Advanced User Guide:
- API Reference: api_reference.md
- Custom conditions: custom_conditions.md
- Parameters: parameters.md
- Rule activation mode: rule_activation_mode.md
- Rule sets: rule_sets.md
- Use your business objects: business_objects.md
- Custom conditions: custom_conditions.md
- API Reference: api_reference.md

extra_css:
- assets/css/mkdocs_extra.css
29 changes: 19 additions & 10 deletions docs/pages/home.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,35 @@
<em>An Open Source Rules Engine - Make rule handling simple</em>
</p>
<p align="center">
<img src="https://img.shields.io/pypi/v/arta" alt="Versions">
<a href="https://pypi.org/project/arta/"><img src="https://img.shields.io/pypi/v/arta" alt="Versions"></a>
</p>

# Welcome to the documentation

* Want to discover what is **Arta**? :arrow_right: [Get Started](a_simple_example.md)
* Want to know how to use it? :arrow_right: [User Guide](how_to.md)
Want to discover what is **Arta**? :arrow_right: [Get Started](a_simple_example.md)

!!! info "New feature"

Check out the new and very convenient feature called the [simple condition](how_to.md#simple-condition). A new and lightweight way of configuring your rules' conditions.
Want to know how to use it? :arrow_right: [User Guide](how_to.md)

**Arta** is automatically tested with:

![Alt Python](https://img.shields.io/pypi/pyversions/arta)
!!! info inline "New feature"

Use **Arta** as a *process execution engine* :zap:

Read [this page](rule_activation_mode.md) for more details.

!!! tip "Releases"

Want to see last updates, check the [Release notes](https://github.com/MAIF/arta/releases) :rocket:
Check the [Release notes](https://github.com/MAIF/arta/releases) :rocket:

!!! warning "Pydantic 1 compatibility is deprecated"

**Arta** is working with [Pydantic 2](https://docs.pydantic.dev/latest/) and Pydantic 1 but compatibility with V1 will be removed in the next **major** release.

**Arta** is working and automatically tested with:

![Alt Python](https://img.shields.io/pypi/pyversions/arta)

!!! success "Pydantic 2"
You like **Arta**? Add a :star:

**Arta** is now working with [Pydantic 2](https://docs.pydantic.dev/latest/)! And of course, Pydantic 1 as well.
[![GitHub Repo stars](https://img.shields.io/github/stars/maif/arta)](https://github.com/MAIF/arta)
4 changes: 0 additions & 4 deletions docs/pages/how_to.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ Ensure that you have correctly installed **Arta** before, check the [Installatio

## Simple condition

!!! beta "Beta feature"

**Simple condition** is still a *beta feature*, some cases could not work as designed.

**Simple conditions** are a new and straightforward way of configuring your *conditions*.

It simplifies your rules a lot by:
Expand Down
95 changes: 95 additions & 0 deletions docs/pages/rule_activation_mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
!!! example "Beta feature"

This new feature (i.e., `rule_activation_mode: many_by_group`) is a **beta feature**, some cases could not work as designed. Please report them using [issues](https://github.com/MAIF/arta/issues).

This feature was designed at MAIF when the idea to use **Arta** as a simple *process execution engine* came about.

Our goal was to handle different *rules* of data processing inside an ETL pipeline.

It's actually quite simple :zap:

## Illustration

Traditionaly, **Arta** is evaluating rules (like most rules engines) like this:

```mermaid
---
title: one_by_group
---
flowchart
s((Start))
e((End))
subgraph Group_1
r1(Rule 1, conditions False)-->r2(Rule 2, conditions True)-.not evaluated.->r3(Rule 3, conditions True)
end
r2-.execute.->a2(Action A)
subgraph Group_2
r1b(Rule 1, conditions True)-.not evaluated.->r2b(Rule 2, conditions False)-.not evaluated.->r3b(Rule 3, conditions True)
end
s-->r1
r1b-.execute.->a1b(Action B)
r2-->r1b
r3-.->r1b
r1b-->e
r3b-.->e
```

> **Only one rule is activated (i.e., meaning one action is triggered) by rule group.**
---

But if we need to use **Arta** to execute *simple workflows*, we need a *control flow* like this one:

```mermaid
---
title: many_by_group
---
flowchart
s((Start))
e((End))
subgraph Group_1
r1(Rule 1, conditions False)-->r2(Rule 2, conditions True)-->r3(Rule 3, conditions True)
end
r2-.execute.->a2(Action A)
r3-.execute.->a3(Action B)
subgraph Group_2
r1b(Rule 1, conditions True)-->r2b(Rule 2, conditions False)-->r3b(Rule 3, conditions True)
end
s-->r1
r1b-.execute.->a1b(Action C)
r3b-.execute.->a3b(Action D)
r3-->r1b
r3b-->e
```

> **All rules are evaluated.**
> **Therefore, many rules can be activated (i.e., meaning many actions can be triggered) by rule group.**
---

## Setting

You just need to add somewhere in the YAML configuration file of **Arta** the following setting:

### One by group

This *traditional* flow of control is the **default one**:

```yaml
rule_activation_mode: one_by_group
```
!!! note "Default value"
Because it is the **default value**, it is *useless* to add this line in the configuration.
### Many by group
This is the *flow of control* of a **process execution engine**:
```yaml
rule_activation_mode: many_by_group
```
That's all! You are all set :+1:
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "arta"
version = "0.8.2"
version = "0.9.0"
requires-python = ">3.8.0"
description = "A Python Rules Engine - Make rule handling simple"
readme = "README.md"
Expand Down
18 changes: 13 additions & 5 deletions src/arta/_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from arta.config import load_config
from arta.models import Configuration, RulesDict
from arta.rule import Rule
from arta.utils import ParsingErrorStrategy
from arta.utils import ParsingErrorStrategy, RuleActivationMode


class RulesEngine:
Expand Down Expand Up @@ -81,8 +81,10 @@ def __init__(
"RulesEngine takes one (and only one) parameter: 'rules_dict' or 'config_path' or 'config_dict'."
)

# Init. default parsing_error_strategy (probably not needed because already defined elsewhere)
# Init. default global settings (useful if not set, can't be set in the Pydantic model
# because of the rules dict mode)
self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE
self._rule_activation_mode: RuleActivationMode = RuleActivationMode.ONE_BY_GROUP

# Initialize directly with a rules dict
if rules_dict is not None:
Expand Down Expand Up @@ -112,6 +114,10 @@ def __init__(
# Set parsing error handling strategy from config
self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)

if config.rule_activation_mode is not None:
# Set rule activation mode from config
self._rule_activation_mode = RuleActivationMode(config.rule_activation_mode)

# dict of available action functions (k: function name, v: function object)
action_modules: list[str] = config.actions_source_modules
action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)
Expand Down Expand Up @@ -166,7 +172,8 @@ def apply_rules(
"""Apply the rules and return results.
For each rule group of a given rule set, rules are applied sequentially,
The loop is broken when a rule is applied (an action is triggered).
The loop is broken when a rule is applied (an action is triggered)
or not (depends on the rule activation mode).
Then, the next rule group is evaluated.
And so on...
Expand Down Expand Up @@ -241,8 +248,9 @@ def apply_rules(
# Update input data with current result with key 'output' (can be used in next rules)
input_data_copy["output"][group_id] = copy.deepcopy(results_dict[group_id])

# We can only have one result per group => break when "action_result" in rule_details
break
if self._rule_activation_mode is RuleActivationMode.ONE_BY_GROUP:
# We can only have one result per group => break when "action_result" in rule_details
break

# Handling non-verbose mode
if not verbose:
Expand Down
6 changes: 4 additions & 2 deletions src/arta/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pydantic
from pydantic.version import VERSION

from arta.utils import ParsingErrorStrategy
from arta.utils import ParsingErrorStrategy, RuleActivationMode

PYDANTIC_V1: bool = VERSION.startswith("1.")

Expand Down Expand Up @@ -66,6 +66,7 @@ class Configuration(pydantic.BaseModel):
condition_factory_mapping: Optional[dict[str, str]] = None
rules: dict[str, dict[str, dict[Annotated[str, pydantic.StringConstraints(to_upper=True)], RulesConfig]]]
parsing_error_strategy: Optional[ParsingErrorStrategy] = None
rule_activation_mode: Optional[RuleActivationMode] = None

else:
# Pydantic V1
Expand Down Expand Up @@ -141,4 +142,5 @@ class Configuration(BaseModelV2): # type: ignore[no-redef]
custom_classes_source_modules: Optional[list[str]]
condition_factory_mapping: Optional[dict[str, str]]
rules: dict[str, dict[str, dict[pydantic.constr(to_upper=True), RulesConfig]]] # type: ignore
parsing_error_strategy: Optional[ParsingErrorStrategy]
parsing_error_strategy: Optional[ParsingErrorStrategy] = None
rule_activation_mode: Optional[RuleActivationMode] = None
10 changes: 10 additions & 0 deletions src/arta/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ class ParsingErrorStrategy(str, Enum):
DEFAULT_VALUE: str = "default_value"


class RuleActivationMode(str, Enum):
"""Define how Arta is processing rules.
ONE_BY_GROUP is the default mode.
"""

ONE_BY_GROUP: str = "one_by_group"
MANY_BY_GROUP: str = "many_by_group"


def get_value_in_nested_dict_from_path(path: str, nested_dict: dict[str, Any]) -> Any:
"""From a path, get a value in a nested dict.
Expand Down
7 changes: 6 additions & 1 deletion tests/examples/code/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> boo
return is_ok


def concatenate_str(list_str: list[Any], **kwargs: Any) -> str:
def concatenate_list(list_str: list[Any], **kwargs: Any) -> str:
"""Demo function: return the concatenation of a list of string using input_data (two levels max)."""
list_str = [str(element) for element in list_str]
return "".join(list_str)
Expand All @@ -44,3 +44,8 @@ def do_nothing(**kwargs: Any) -> None:
def compute_sum(value1: float, value2: float, **kwargs: Any) -> float:
"""Demo function: return sum of two values."""
return value1 + value2


def concatenate(value1: str, value2: str, **kwargs: Any) -> str:
"""Demo function: return the concatenation of two strings."""
return value1 + value2
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
rule_activation_mode: dummy-value

actions_source_modules:
- tests.examples.code.actions

rules:
default_rule_set:
rg_1:
RULE_1:
simple_condition: input.power=="strength" or input.power=="fly" or input.power=="time-manipulation"
action: concatenate
action_parameters:
value1: ""
value2: a
RULE_2:
simple_condition: null
action: concatenate
action_parameters:
value1: ""
value2: b
rg_2:
RULE_1:
simple_condition: input.language=="english" and input.age!=None
action: concatenate
action_parameters:
value1: output.rg_1
value2: c
RULE_2:
simple_condition: input.age>=100 or input.age==None
action: concatenate
action_parameters:
value1: output.rg_1
value2: d
RULE_3:
simple_condition: input.language=="french"
action: concatenate
action_parameters:
value1: output.rg_1
value2: e
rg_3:
RULE_1:
simple_condition: input.favorite_meal!=None
action: concatenate
action_parameters:
value1: output.rg_2
value2: f
2 changes: 1 addition & 1 deletion tests/examples/ignore_conf/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ rules:
test_action:
TEST_IGNORE:
condition: null
action: concatenate_str
action: concatenate_list
action_parameters:
list_str:
- "My name is "
Expand Down
Loading

0 comments on commit 001d4e9

Please sign in to comment.