Skip to content

Commit af40eea

Browse files
authored
feat: add value sharing (#44)
* feat: add value sharing Signed-off-by: develop-cs <[email protected]> * docs: add how to get input and output data in functions Signed-off-by: develop-cs <[email protected]> * fix: enable other kwargs param name Signed-off-by: develop-cs <[email protected]> --------- Signed-off-by: develop-cs <[email protected]>
1 parent 88ebd80 commit af40eea

File tree

17 files changed

+168
-36
lines changed

17 files changed

+168
-36
lines changed

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "github-actions"
4+
directory: "/"
5+
schedule:
6+
interval: "monthly"
7+
commit-message:
8+
prefix: "chore(gh-actions): "
9+
- package-ecosystem: "pip"
10+
directory: "/"
11+
schedule:
12+
interval: "monthly"
13+
commit-message:
14+
prefix: "chore(dependencies): "

.pre-commit-config.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,29 +26,29 @@ repos:
2626
- id: check-added-large-files
2727
args: [--maxkb=500]
2828
- repo: https://github.com/astral-sh/ruff-pre-commit
29-
rev: v0.8.2
29+
rev: v0.9.9
3030
hooks:
3131
- id: ruff
3232
args: [--fix]
3333
- id: ruff-format
3434
- repo: https://github.com/pre-commit/mirrors-mypy
35-
rev: v1.13.0
35+
rev: v1.15.0
3636
hooks:
3737
- id: mypy
3838
args: [--config-file=pyproject.toml]
3939
files: src
4040
additional_dependencies: [pydantic~=2.0]
4141
- repo: https://github.com/gitleaks/gitleaks
42-
rev: v8.21.2
42+
rev: v8.24.0
4343
hooks:
4444
- id: gitleaks
4545
- repo: https://github.com/pypa/pip-audit
46-
rev: v2.7.3
46+
rev: v2.8.0
4747
hooks:
4848
- id: pip-audit
4949
args: [--skip-editable]
5050
- repo: https://github.com/compilerla/conventional-pre-commit
51-
rev: v3.6.0
51+
rev: v4.0.0
5252
hooks:
5353
- id: conventional-pre-commit
5454
stages: [commit-msg]

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

55
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).
66

7+
## [0.10.0] - March, 2025
8+
9+
### Features
10+
11+
* Ability to share values between validation and action functions (#7).
12+
* The `kwargs` parameter of action functions is now optional (only mandatory if you need to access `input_data`).
13+
714
## [0.9.0] - December, 2024
815

916
### Features

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ actions_source_modules:
7878
from typing import Any
7979
8080
81-
def set_admission(value: bool, **kwargs: Any) -> dict[str, bool]:
81+
def set_admission(value: bool) -> dict[str, bool]:
8282
"""Return a dictionary containing the admission result."""
8383
return {"is_admitted": value}
8484
```

docs/mkdocs.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ nav:
8888
- Parameters: parameters.md
8989
- Rule activation mode: rule_activation_mode.md
9090
- Rule sets: rule_sets.md
91+
- Value sharing: value_sharing.md
9192
- Use your business objects: business_objects.md
9293

9394
extra_css:

docs/pages/a_simple_example.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,17 +127,17 @@ And could be for example (intentionally simple) in `actions.py`:
127127
from typing import Any
128128
129129
130-
def set_admission(value: bool, **kwargs: Any) -> dict[str, bool]:
130+
def set_admission(value: bool) -> dict[str, bool]:
131131
"""Return a dictionary containing the admission result."""
132132
return {"is_admitted": value}
133133
134134
135-
def set_course(course_id: str, **kwargs: Any) -> dict[str, str]:
135+
def set_course(course_id: str) -> dict[str, str]:
136136
"""Return the course id as a dictionary."""
137137
return {"course_id": course_id}
138138
139139
140-
def send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> str | None:
140+
def send_email(mail_to: str, mail_content: str, meal: str) -> str | None:
141141
"""Send an email."""
142142
result: str | None = None
143143
@@ -148,10 +148,6 @@ def send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> str
148148
return result
149149
```
150150

151-
!!! warning "\*\*kwargs"
152-
153-
**\*\*kwargs** is mandatory in *action functions*.
154-
155151
### Engine
156152

157153
The *rules engine* is responsible for evaluating the [configured rules](#rules) against some *data* (usually named *"input data"*).

docs/pages/how_to.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,21 @@ def has_authorized_super_power(power):
227227
**actions.py**:
228228

229229
```python
230-
def set_admission(value, **kwargs): # (1)
230+
def set_admission(value):
231231
return {"is_admitted": value}
232232
```
233233

234-
1. `**kwargs` is mandatory here.
235-
236234
!!! warning
237235

238-
Function name and parameters must be the same as the one configured in the YAML file.
236+
**Function name** and **parameters** must be the same as the one configured in the YAML file.
237+
238+
!!! tip
239+
240+
If you need to retrieve some `input_data` or `output` of a previous action in your function, just:
241+
242+
1. Add a ****kwargs** parameter.
243+
1. Access *input_data* with `kwargs["input_data"]["<some-key>"]` where `<some-key>` is a key in the *input data* dictionary (or dict like object).
244+
1. Access data from previous rules group with `kwargs["input_data"]["output"]["<a-rule-group-id>"]`.
239245

240246
### Usage
241247

@@ -395,7 +401,7 @@ Both are made of a *callable object* and some *parameters*:
395401
```python
396402
from arta import RulesEngine
397403
398-
set_admission = lambda value, **kwargs: {"is_admitted": value}
404+
set_admission = lambda value: {"is_admitted": value}
399405
400406
rules = {
401407
"check_admission": {
@@ -441,7 +447,7 @@ Both are made of a *callable object* and some *parameters*:
441447
442448
from arta import RulesEngine
443449
444-
set_admission: Callable = lambda value, **kwargs: {"is_admitted": value}
450+
set_admission: Callable = lambda value: {"is_admitted": value}
445451
446452
rules: dict[str, Any] = {
447453
"check_admission": {

docs/pages/value_sharing.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
!!! note
2+
3+
Only available with `arta>=0.10.0`.
4+
5+
6+
It is possible to share some informations between **condition** and **action** implementations.
7+
8+
It can be usefull when an **action** needs some data that was computed in a **condition** (e.g., sanity check use cases).
9+
10+
In the following example, a **condition** is computing the *median* of some *input values* and checking it. Then, the **action** retrieves this *median* value and uses it.
11+
12+
13+
**Two things have to be done for that:**
14+
15+
1. Add the `**kwargs` parameter in your functions' definition (**validation** and **action** functions) if not already there.
16+
1. Set some new subkeys in the `input_data` key.
17+
18+
19+
**Set the value (in a condition for example):**
20+
21+
```python hl_lines="4 9"
22+
def is_median_above(
23+
values: list[float],
24+
limit: float,
25+
**kwargs: Any, # (1)
26+
) -> bool:
27+
"""Check if the median of some values is above limit."""
28+
median = statistics.median(values)
29+
# Store the value for later use by an action function
30+
kwargs["input_data"]["median"] = median # (2)
31+
kwargs["input_data"]["median_limit"] = limit
32+
return median > limit
33+
```
34+
35+
1. Add the ****kwargs** parameter.
36+
2. Set your value in the `input_data`.
37+
38+
39+
**Get the value (in an action for example):**
40+
41+
```python hl_lines="1 4"
42+
def alert_on_median(**kwargs: Any) -> str: # (1)
43+
"""Alert: "Median is too high: 13, limit is: 10."""
44+
return (
45+
f"Median is too high: {kwargs['input_data']['median']}, " # (2)
46+
f"limit is: {kwargs['input_data']['median_limit']}."
47+
)
48+
```
49+
50+
1. Add the ****kwargs** parameter.
51+
2. Get the value.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "arta"
7-
version = "0.9.0"
7+
version = "0.10.0"
88
requires-python = ">3.8.0"
99
description = "A Python Rules Engine - Make rule handling simple"
1010
readme = "README.md"

src/arta/condition.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
import inspect
89
import re
910
from abc import ABC, abstractmethod
1011
from typing import Any, Callable
@@ -140,6 +141,11 @@ def verify(self, input_data: dict[str, Any], parsing_error_strategy: ParsingErro
140141
parameter=value, input_data=input_data, parsing_error_strategy=parsing_error_strategy
141142
)
142143

144+
# Pass input_data for value sharing if validation function can accept it
145+
arg_spec: inspect.FullArgSpec = inspect.getfullargspec(self._validation_function)
146+
if arg_spec.varkw is not None:
147+
parameters["input_data"] = input_data
148+
143149
# Run validation_function
144150
return self._validation_function(**parameters)
145151

src/arta/rule.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
import inspect
89
import re
910
from typing import Any, Callable
1011

@@ -105,8 +106,13 @@ def apply(
105106
# Track the rule id
106107
rule_results["activated_rule"] = self._rule_id
107108

109+
# Pass input_data for value sharing if action function can accept it
110+
arg_spec: inspect.FullArgSpec = inspect.getfullargspec(self._action)
111+
if arg_spec.varkw is not None:
112+
parameters["input_data"] = input_data
113+
108114
# Run action
109-
rule_results["action_result"] = self._action(**parameters, input_data=input_data)
115+
rule_results["action_result"] = self._action(**parameters)
110116

111117
return rule_results["action_result"], rule_results
112118
except Exception as error:

src/arta/utils.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
class ParsingErrorStrategy(str, Enum):
1212
"""Define authorized error handling strategies when a key is missing in the input data."""
1313

14-
RAISE: str = "raise"
15-
IGNORE: str = "ignore"
16-
DEFAULT_VALUE: str = "default_value"
14+
RAISE = "raise"
15+
IGNORE = "ignore"
16+
DEFAULT_VALUE = "default_value"
1717

1818

1919
class RuleActivationMode(str, Enum):
@@ -22,8 +22,8 @@ class RuleActivationMode(str, Enum):
2222
ONE_BY_GROUP is the default mode.
2323
"""
2424

25-
ONE_BY_GROUP: str = "one_by_group"
26-
MANY_BY_GROUP: str = "many_by_group"
25+
ONE_BY_GROUP = "one_by_group"
26+
MANY_BY_GROUP = "many_by_group"
2727

2828

2929
def get_value_in_nested_dict_from_path(path: str, nested_dict: dict[str, Any]) -> Any:

tests/examples/code/actions.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import Any
1010

1111

12-
def set_admission(value: bool, **kwargs: Any) -> dict[str, bool]:
12+
def set_admission(value: bool) -> dict[str, bool]:
1313
"""Return a dictionary containing the admission result."""
1414
return {"admission": value}
1515

@@ -19,7 +19,7 @@ def set_student_course(course_id: str, **kwargs: Any) -> dict[str, str]:
1919
return {"course_id": course_id}
2020

2121

22-
def send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> bool:
22+
def send_email(mail_to: str, mail_content: str, meal: str) -> bool:
2323
"""Send an email and return True if OK."""
2424
is_ok = False
2525

@@ -30,7 +30,7 @@ def send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> boo
3030
return is_ok
3131

3232

33-
def concatenate_list(list_str: list[Any], **kwargs: Any) -> str:
33+
def concatenate_list(list_str: list[Any], **extra: Any) -> str:
3434
"""Demo function: return the concatenation of a list of string using input_data (two levels max)."""
3535
list_str = [str(element) for element in list_str]
3636
return "".join(list_str)
@@ -46,6 +46,11 @@ def compute_sum(value1: float, value2: float, **kwargs: Any) -> float:
4646
return value1 + value2
4747

4848

49-
def concatenate(value1: str, value2: str, **kwargs: Any) -> str:
49+
def concatenate(value1: str, value2: str) -> str:
5050
"""Demo function: return the concatenation of two strings."""
5151
return value1 + value2
52+
53+
54+
def alert_on_median(**kwargs: Any) -> str:
55+
"""Alert: "Median is too high: 13, limit is: 10."""
56+
return f"Median is too high: {kwargs['input_data']['median']}, limit is: {kwargs['input_data']['median_limit']}."

tests/examples/code/conditions.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""
66

77
from __future__ import annotations
8+
import statistics
9+
from typing import Any
810

911

1012
def has_authorized_super_power(authorized_powers: list[str], candidate_powers: list[str]) -> bool:
@@ -23,6 +25,15 @@ def is_speaking_language(value: str, spoken_language: str) -> bool:
2325
return value == spoken_language
2426

2527

26-
def has_favorite_meal(favorite_meal: str) -> bool:
28+
def has_favorite_meal(favorite_meal: str, **extra: Any) -> bool:
2729
"""Check if candidate has a favorite meal."""
2830
return favorite_meal is not None
31+
32+
33+
def is_median_above(values: list[int | float], limit: float, **kwargs: Any) -> bool:
34+
"""Check if the median of some values is above limit."""
35+
median = statistics.median(values)
36+
# Store the value for later use by an action function
37+
kwargs["input_data"]["median"] = median
38+
kwargs["input_data"]["median_limit"] = limit
39+
return median > limit

tests/examples/good_conf/conditions.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ conditions:
66
validation_function: has_authorized_super_power
77
condition_parameters:
88
authorized_powers:
9-
- "strength"
10-
- "fly"
11-
- "immortality"
9+
- strength
10+
- fly
11+
- immortality
1212
candidate_powers: input.powers
1313
IS_SPEAKING_FRENCH:
1414
description: "Does it speak french?"
1515
validation_function: is_speaking_language
1616
condition_parameters:
17-
value: "french"
17+
value: french
1818
spoken_language: input.language
1919
IS_SPEAKING_ENGLISH:
2020
description: "Does it speak english?"
2121
validation_function: is_speaking_language
2222
condition_parameters:
23-
value: "english"
23+
value: english
2424
spoken_language: input.language
2525
IS_AGE_UNKNOWN:
2626
description: "Do we know his age?"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
rules:
3+
default_rule_set:
4+
median_check:
5+
MEDIAN_KO:
6+
condition: IS_MEDIAN_ABOVE_10
7+
action: alert_on_median
8+
9+
conditions:
10+
IS_MEDIAN_ABOVE_10:
11+
description: "Is the computed median above 10?"
12+
validation_function: is_median_above
13+
condition_parameters:
14+
values: input.values
15+
limit: 10
16+
17+
conditions_source_modules:
18+
- "tests.examples.code.conditions"
19+
actions_source_modules:
20+
- "tests.examples.code.actions"

0 commit comments

Comments
 (0)