Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Expand Up @@ -10,6 +10,12 @@
logger = logging.getLogger("openfeature.contrib")


class Fraction:
weight: int = 1
variant: str
pass


def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
if not args:
logger.error("No arguments provided to fractional operator.")
Expand All @@ -32,28 +38,50 @@ 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 len(arg) == 0:
logger.error("Fractional variant weights must be (str, int) tuple")
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()
if len(arg) >= 2:
fraction.weight = arg[1]
fraction.variant = arg[0]

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