Skip to content

Commit 3beeac1

Browse files
leakonvalinkatoddbaert
authored andcommitted
feat: nested and more flexible fractional enhancements
- support nested properties in fractional targeting rules - support non-string variant keys in fractional operator - bump grpcio and grpcio-health-checking to >=1.80.0 - update test harness and add comprehensive fractional tests - adjust test assertions for invalid-fractional-weights behavior change
1 parent 7e18bc1 commit 3beeac1

File tree

9 files changed

+335
-111
lines changed

9 files changed

+335
-111
lines changed

providers/openfeature-provider-flagd/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ classifiers = [
1818
keywords = []
1919
dependencies = [
2020
"openfeature-sdk>=0.8.2",
21-
"grpcio>=1.78.0",
21+
"grpcio>=1.80.0",
2222
"protobuf>=6.30.0,<7.0.0",
2323
"mmh3>=5.0.0,<6.0.0",
2424
"panzi-json-logic>=1.0.1",
@@ -35,7 +35,7 @@ Homepage = "https://github.com/open-feature/python-sdk-contrib"
3535
dev = [
3636
"asserts>=0.13.0,<0.14.0",
3737
"coverage[toml]>=7.10.0,<8.0.0",
38-
"grpcio-health-checking>=1.74.0,<2.0.0",
38+
"grpcio-health-checking>=1.80.0,<2.0.0",
3939
"mypy>=1.18.0,<2.0.0",
4040
"poethepoet>=0.37.0",
4141
"pytest>=9.0.0,<10.0.0",

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

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import mmh3
77
import semver
88

9+
MAX_WEIGHT_SUM = 2_147_483_647 # MaxInt32
10+
911
JsonPrimitive: typing.TypeAlias = str | bool | float | int
1012
JsonLogicArg: typing.TypeAlias = JsonPrimitive | Sequence[JsonPrimitive]
1113

@@ -14,50 +16,56 @@
1416

1517
@dataclass
1618
class Fraction:
17-
variant: str
19+
variant: str | float | int | bool | None
1820
weight: int = 1
1921

2022

21-
def fractional(data: dict, *args: JsonLogicArg) -> str | None:
23+
def _resolve_bucket_by(data: dict, args: tuple) -> tuple[str | None, tuple]:
24+
if isinstance(args[0], str):
25+
return args[0], args[1:]
26+
27+
seed = data.get("$flagd", {}).get("flagKey", "")
28+
targeting_key = data.get("targetingKey")
29+
if not targeting_key:
30+
logger.error("No targetingKey provided for fractional shorthand syntax.")
31+
return None, args
32+
return seed + targeting_key, args
33+
34+
35+
def fractional(data: dict, *args: JsonLogicArg) -> str | float | int | bool | None:
2236
if not args:
2337
logger.error("No arguments provided to fractional operator.")
2438
return None
2539

26-
bucket_by = None
27-
if isinstance(args[0], str):
28-
bucket_by = args[0]
29-
args = args[1:]
30-
else:
31-
seed = data.get("$flagd", {}).get("flagKey", "")
32-
targeting_key = data.get("targetingKey")
33-
if not targeting_key:
34-
logger.error("No targetingKey provided for fractional shorthand syntax.")
35-
return None
36-
bucket_by = seed + targeting_key
40+
bucket_by, args = _resolve_bucket_by(data, args)
3741

3842
if not bucket_by:
3943
logger.error("No hashKey value resolved")
4044
return None
4145

42-
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
43-
bucket = hash_ratio * 100
46+
hash_value = mmh3.hash(bucket_by, signed=False)
4447

4548
total_weight = 0
4649
fractions = []
4750
try:
4851
for arg in args:
4952
fraction = _parse_fraction(arg)
50-
if fraction:
51-
fractions.append(fraction)
52-
total_weight += fraction.weight
53+
fractions.append(fraction)
54+
total_weight += fraction.weight
5355

5456
except ValueError:
5557
logger.debug(f"Invalid {args} configuration")
5658
return None
5759

58-
range_end: float = 0
60+
if total_weight > MAX_WEIGHT_SUM:
61+
logger.error(f"Total fractional weight exceeds MaxInt32 ({MAX_WEIGHT_SUM:,}).")
62+
return None
63+
64+
bucket = (hash_value * total_weight) >> 32
65+
66+
range_end = 0
5967
for fraction in fractions:
60-
range_end += fraction.weight * 100 / total_weight
68+
range_end += fraction.weight
6169
if bucket < range_end:
6270
return fraction.variant
6371
return None
@@ -69,19 +77,21 @@ def _parse_fraction(arg: JsonLogicArg) -> Fraction:
6977
"Fractional variant weights must be (str, int) tuple or [str] list"
7078
)
7179

72-
if not isinstance(arg[0], str):
73-
raise ValueError(
74-
"Fractional variant identifier (first element) isn't of type 'str'"
75-
)
76-
77-
if len(arg) >= 2 and not isinstance(arg[1], int):
78-
raise ValueError(
79-
"Fractional variant weight value (second element) isn't of type 'int'"
80-
)
81-
82-
fraction = Fraction(variant=arg[0])
83-
if len(arg) >= 2:
84-
fraction.weight = arg[1]
80+
variant = arg[0]
81+
82+
weight = None
83+
if len(arg) == 2:
84+
w = arg[1]
85+
if isinstance(w, bool):
86+
raise ValueError("Fractional weight value isn't of type 'int'")
87+
elif isinstance(w, int):
88+
weight = w
89+
else:
90+
raise ValueError("Fractional weight value isn't of type 'int'")
91+
92+
fraction = Fraction(variant=variant)
93+
if weight is not None:
94+
fraction.weight = weight
8595

8696
return fraction
8797

providers/openfeature-provider-flagd/tests/e2e/file/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"~grace",
1515
"~contextEnrichment",
1616
"~deprecated",
17+
"~fractional-v1",
1718
}
1819

1920

providers/openfeature-provider-flagd/tests/e2e/inprocess/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from tests.e2e.testfilter import TestFilter
55

66
resolver = ResolverType.IN_PROCESS
7-
feature_list = ["~targetURI", "~unixsocket", "~deprecated"]
7+
feature_list = ["~targetURI", "~unixsocket", "~deprecated", "~fractional-v1"]
88

99

1010
def pytest_collection_modifyitems(config, items):

providers/openfeature-provider-flagd/tests/e2e/rpc/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
"~sync",
1111
"~metadata",
1212
"~deprecated",
13+
"~fractional-v1",
14+
"~fractional-v2",
15+
"~fractional-nested",
1316
]
1417

1518

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ def test_broken_targeting():
8484
"invalid-stringcomp-args.json",
8585
"invalid-fractional-args.json",
8686
"invalid-fractional-args-wrong-content.json",
87-
"invalid-fractional-weights.json",
8887
"invalid-fractional-weights-strings.json",
8988
],
9089
)

0 commit comments

Comments
 (0)