feat: Change fractional custom op from percentage-based to relative weighting. (#91)
Signed-off-by: Simon Schrottner <simon.schrottner@dynatrace.com>
This commit is contained in:
parent
0f5b0ca501
commit
7b34822afd
|
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
import mmh3
|
||||
import semver
|
||||
|
|
@ -10,6 +11,12 @@ JsonLogicArg = typing.Union[JsonPrimitive, typing.Sequence[JsonPrimitive]]
|
|||
logger = logging.getLogger("openfeature.contrib")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Fraction:
|
||||
variant: str
|
||||
weight: int = 1
|
||||
|
||||
|
||||
def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
|
||||
if not args:
|
||||
logger.error("No arguments provided to fractional operator.")
|
||||
|
|
@ -32,28 +39,51 @@ def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
|
|||
return None
|
||||
|
||||
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
|
||||
bucket = int(hash_ratio * 100)
|
||||
bucket = hash_ratio * 100
|
||||
|
||||
total_weight = 0
|
||||
fractions = []
|
||||
for arg in args:
|
||||
if (
|
||||
not isinstance(arg, (tuple, list))
|
||||
or len(arg) != 2
|
||||
or not isinstance(arg[0], str)
|
||||
or not isinstance(arg[1], int)
|
||||
):
|
||||
logger.error("Fractional variant weights must be (str, int) tuple")
|
||||
return None
|
||||
variant_weights: typing.Tuple[typing.Tuple[str, int]] = args # type: ignore[assignment]
|
||||
fraction = _parse_fraction(arg)
|
||||
if fraction:
|
||||
fractions.append(fraction)
|
||||
total_weight += fraction.weight
|
||||
|
||||
range_end = 0
|
||||
for variant, weight in variant_weights:
|
||||
range_end += weight
|
||||
range_end: float = 0
|
||||
for fraction in fractions:
|
||||
range_end += fraction.weight * 100 / total_weight
|
||||
if bucket < range_end:
|
||||
return variant
|
||||
return fraction.variant
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _parse_fraction(arg: JsonLogicArg) -> typing.Optional[Fraction]:
|
||||
if not isinstance(arg, (tuple, list)) or not arg:
|
||||
logger.error(
|
||||
"Fractional variant weights must be (str, int) tuple or [str] list"
|
||||
)
|
||||
return None
|
||||
|
||||
if not isinstance(arg[0], str):
|
||||
logger.error(
|
||||
"Fractional variant identifier (first element) isn't of type 'str'"
|
||||
)
|
||||
return None
|
||||
|
||||
if len(arg) >= 2 and not isinstance(arg[1], int):
|
||||
logger.error(
|
||||
"Fractional variant weight value (second element) isn't of type 'int'"
|
||||
)
|
||||
return None
|
||||
|
||||
fraction = Fraction(variant=arg[0])
|
||||
if len(arg) >= 2:
|
||||
fraction.weight = arg[1]
|
||||
|
||||
return fraction
|
||||
|
||||
|
||||
def starts_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
|
||||
def f(s1: str, s2: str) -> bool:
|
||||
return s1.startswith(s2)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"flags": {
|
||||
"basic-flag": {
|
||||
"state": "ENABLED",
|
||||
"variants": {
|
||||
"default": "default",
|
||||
"true": "true",
|
||||
"false": "false"
|
||||
},
|
||||
"defaultVariant": "default",
|
||||
"targeting": {
|
||||
"fractional": [[]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"flags": {
|
||||
"basic-flag": {
|
||||
"state": "ENABLED",
|
||||
"variants": {
|
||||
"default": "default",
|
||||
"true": "true",
|
||||
"false": "false"
|
||||
},
|
||||
"defaultVariant": "default",
|
||||
"targeting": {
|
||||
"fractional": [
|
||||
["a", "one"],
|
||||
["b", "one"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,7 +48,9 @@ def test_file_load_errors(file_name: str):
|
|||
"invalid-semver-args.json",
|
||||
"invalid-stringcomp-args.json",
|
||||
"invalid-fractional-args.json",
|
||||
"invalid-fractional-args-wrong-content.json",
|
||||
"invalid-fractional-weights.json",
|
||||
"invalid-fractional-weights-strings.json",
|
||||
],
|
||||
)
|
||||
def test_json_logic_parse_errors(file_name: str):
|
||||
|
|
|
|||
Loading…
Reference in New Issue