Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import typing
from dataclasses import dataclass

import mmh3
import semver
Expand All @@ -10,6 +11,12 @@
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.")
Expand All @@ -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]

range_end = 0
for variant, weight in variant_weights:
range_end += weight
fraction = _parse_fraction(arg)
if fraction:
fractions.append(fraction)
total_weight += fraction.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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"flags": {
"basic-flag": {
"state": "ENABLED",
"variants": {
"default": "default",
"true": "true",
"false": "false"
},
"defaultVariant": "default",
"targeting": {
"fractional": [[]]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"flags": {
"basic-flag": {
"state": "ENABLED",
"variants": {
"default": "default",
"true": "true",
"false": "false"
},
"defaultVariant": "default",
"targeting": {
"fractional": [
["a", "one"],
["b", "one"]
]
}
}
}
}
2 changes: 2 additions & 0 deletions providers/openfeature-provider-flagd/tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down