Skip to content

Commit 7b34822

Browse files
authored
feat: Change fractional custom op from percentage-based to relative weighting. (#91)
Signed-off-by: Simon Schrottner <[email protected]>
1 parent 0f5b0ca commit 7b34822

File tree

4 files changed

+82
-15
lines changed

4 files changed

+82
-15
lines changed

providers/openfeature-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import typing
3+
from dataclasses import dataclass
34

45
import mmh3
56
import semver
@@ -10,6 +11,12 @@
1011
logger = logging.getLogger("openfeature.contrib")
1112

1213

14+
@dataclass
15+
class Fraction:
16+
variant: str
17+
weight: int = 1
18+
19+
1320
def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
1421
if not args:
1522
logger.error("No arguments provided to fractional operator.")
@@ -32,28 +39,51 @@ def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
3239
return None
3340

3441
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
35-
bucket = int(hash_ratio * 100)
42+
bucket = hash_ratio * 100
3643

44+
total_weight = 0
45+
fractions = []
3746
for arg in args:
38-
if (
39-
not isinstance(arg, (tuple, list))
40-
or len(arg) != 2
41-
or not isinstance(arg[0], str)
42-
or not isinstance(arg[1], int)
43-
):
44-
logger.error("Fractional variant weights must be (str, int) tuple")
45-
return None
46-
variant_weights: typing.Tuple[typing.Tuple[str, int]] = args # type: ignore[assignment]
47-
48-
range_end = 0
49-
for variant, weight in variant_weights:
50-
range_end += weight
47+
fraction = _parse_fraction(arg)
48+
if fraction:
49+
fractions.append(fraction)
50+
total_weight += fraction.weight
51+
52+
range_end: float = 0
53+
for fraction in fractions:
54+
range_end += fraction.weight * 100 / total_weight
5155
if bucket < range_end:
52-
return variant
56+
return fraction.variant
5357

5458
return None
5559

5660

61+
def _parse_fraction(arg: JsonLogicArg) -> typing.Optional[Fraction]:
62+
if not isinstance(arg, (tuple, list)) or not arg:
63+
logger.error(
64+
"Fractional variant weights must be (str, int) tuple or [str] list"
65+
)
66+
return None
67+
68+
if not isinstance(arg[0], str):
69+
logger.error(
70+
"Fractional variant identifier (first element) isn't of type 'str'"
71+
)
72+
return None
73+
74+
if len(arg) >= 2 and not isinstance(arg[1], int):
75+
logger.error(
76+
"Fractional variant weight value (second element) isn't of type 'int'"
77+
)
78+
return None
79+
80+
fraction = Fraction(variant=arg[0])
81+
if len(arg) >= 2:
82+
fraction.weight = arg[1]
83+
84+
return fraction
85+
86+
5787
def starts_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
5888
def f(s1: str, s2: str) -> bool:
5989
return s1.startswith(s2)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"flags": {
3+
"basic-flag": {
4+
"state": "ENABLED",
5+
"variants": {
6+
"default": "default",
7+
"true": "true",
8+
"false": "false"
9+
},
10+
"defaultVariant": "default",
11+
"targeting": {
12+
"fractional": [[]]
13+
}
14+
}
15+
}
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"flags": {
3+
"basic-flag": {
4+
"state": "ENABLED",
5+
"variants": {
6+
"default": "default",
7+
"true": "true",
8+
"false": "false"
9+
},
10+
"defaultVariant": "default",
11+
"targeting": {
12+
"fractional": [
13+
["a", "one"],
14+
["b", "one"]
15+
]
16+
}
17+
}
18+
}
19+
}

providers/openfeature-provider-flagd/tests/test_errors.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ def test_file_load_errors(file_name: str):
4848
"invalid-semver-args.json",
4949
"invalid-stringcomp-args.json",
5050
"invalid-fractional-args.json",
51+
"invalid-fractional-args-wrong-content.json",
5152
"invalid-fractional-weights.json",
53+
"invalid-fractional-weights-strings.json",
5254
],
5355
)
5456
def test_json_logic_parse_errors(file_name: str):

0 commit comments

Comments
 (0)