Skip to content

Commit d9c1c4f

Browse files
authored
Merge branch 'main' into flag-metadata
2 parents 7487c29 + c80aee7 commit d9c1c4f

File tree

4 files changed

+234
-2
lines changed

4 files changed

+234
-2
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"hooks/openfeature-hooks-opentelemetry": "0.2.0",
3-
"providers/openfeature-provider-flagd": "0.2.1",
3+
"providers/openfeature-provider-flagd": "0.2.2",
44
"providers/openfeature-provider-ofrep": "0.1.1",
55
"providers/openfeature-provider-flipt": "0.1.3"
66
}

providers/openfeature-provider-flagd/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [0.2.2](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.1...openfeature-provider-flagd/v0.2.2) (2025-03-18)
4+
5+
6+
### 🐛 Bug Fixes
7+
8+
* **flagd:** handle falsy target values correctly ([#214](https://github.com/open-feature/python-sdk-contrib/issues/214)) ([fafd099](https://github.com/open-feature/python-sdk-contrib/commit/fafd099f07365a7d0032e8215477b51bfe90c01a))
9+
10+
11+
### 🧹 Chore
12+
13+
* **deps:** update dependency grpcio-health-checking to v1.71.0 ([#209](https://github.com/open-feature/python-sdk-contrib/issues/209)) ([345e793](https://github.com/open-feature/python-sdk-contrib/commit/345e7934b9de3879d3aff45c8213ece1a98e3711))
14+
* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 ([#212](https://github.com/open-feature/python-sdk-contrib/issues/212)) ([1b9b5f1](https://github.com/open-feature/python-sdk-contrib/commit/1b9b5f128a7fe08ffbf84cbc7de2986f95dc01f5))
15+
* **flagd:** Add sync metadata disabled ([#211](https://github.com/open-feature/python-sdk-contrib/issues/211)) ([2f85057](https://github.com/open-feature/python-sdk-contrib/commit/2f850574943cc92d55d198c8ccd91e80583a2ee6))
16+
317
## [0.2.1](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.0...openfeature-provider-flagd/v0.2.1) (2025-03-10)
418

519

providers/openfeature-provider-flagd/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
55

66
[project]
77
name = "openfeature-provider-flagd"
8-
version = "0.2.1"
8+
version = "0.2.2"
99
description = "OpenFeature provider for the flagd flag evaluation engine"
1010
readme = "README.md"
1111
authors = [{ name = "OpenFeature", email = "[email protected]" }]
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
from unittest.mock import Mock, create_autospec
2+
3+
import pytest
4+
5+
from openfeature.contrib.provider.flagd.config import Config
6+
from openfeature.contrib.provider.flagd.resolvers.in_process import InProcessResolver
7+
from openfeature.contrib.provider.flagd.resolvers.process.flags import Flag, FlagStore
8+
from openfeature.evaluation_context import EvaluationContext
9+
from openfeature.exception import FlagNotFoundError, ParseError
10+
11+
12+
def targeting():
13+
return {
14+
"if": [
15+
{"==": [{"var": "targetingKey"}, "target_variant"]},
16+
"target_variant",
17+
None,
18+
]
19+
}
20+
21+
22+
def context(targeting_key):
23+
return EvaluationContext(targeting_key=targeting_key)
24+
25+
26+
@pytest.fixture
27+
def config():
28+
return create_autospec(Config)
29+
30+
31+
@pytest.fixture
32+
def flag_store():
33+
return create_autospec(FlagStore)
34+
35+
36+
@pytest.fixture
37+
def flag():
38+
return Flag(
39+
key="flag",
40+
state="ENABLED",
41+
variants={"default_variant": False, "target_variant": True},
42+
default_variant="default_variant",
43+
targeting=targeting(),
44+
)
45+
46+
47+
@pytest.fixture
48+
def resolver(config):
49+
config.offline_flag_source_path = "flag.json"
50+
config.deadline_ms = 100
51+
return InProcessResolver(
52+
config=config,
53+
emit_provider_ready=Mock(),
54+
emit_provider_error=Mock(),
55+
emit_provider_stale=Mock(),
56+
emit_provider_configuration_changed=Mock(),
57+
)
58+
59+
60+
def test_resolve_boolean_details_flag_not_found(resolver):
61+
resolver.flag_store.get_flag = Mock(return_value=None)
62+
with pytest.raises(FlagNotFoundError):
63+
resolver.resolve_boolean_details("nonexistent_flag", False)
64+
65+
66+
def test_resolve_boolean_details_disabled_flag(flag, resolver):
67+
flag.state = "DISABLED"
68+
resolver.flag_store.get_flag = Mock(return_value=flag)
69+
70+
result = resolver.resolve_boolean_details("disabled_flag", False)
71+
72+
assert result.reason == "DISABLED"
73+
assert result.variant is None
74+
assert not result.value
75+
76+
77+
def test_resolve_boolean_details_invalid_variant(resolver, flag):
78+
flag.targeting = {"var": ["targetingKey", "invalid_variant"]}
79+
80+
resolver.flag_store.get_flag = Mock(return_value=flag)
81+
82+
with pytest.raises(ParseError):
83+
resolver.resolve_boolean_details("flag", False)
84+
85+
86+
@pytest.mark.parametrize(
87+
"input_config, resolve_config, expected",
88+
[
89+
(
90+
{
91+
"variants": {"default_variant": False, "target_variant": True},
92+
"targeting": None,
93+
},
94+
{
95+
"context": None,
96+
"method": "resolve_boolean_details",
97+
"default_value": False,
98+
},
99+
{"reason": "STATIC", "variant": "default_variant", "value": False},
100+
),
101+
(
102+
{
103+
"variants": {"default_variant": False, "target_variant": True},
104+
"targeting": targeting(),
105+
},
106+
{
107+
"context": context("no_target_variant"),
108+
"method": "resolve_boolean_details",
109+
"default_value": False,
110+
},
111+
{"reason": "DEFAULT", "variant": "default_variant", "value": False},
112+
),
113+
(
114+
{
115+
"variants": {"default_variant": False, "target_variant": True},
116+
"targeting": targeting(),
117+
},
118+
{
119+
"context": context("target_variant"),
120+
"method": "resolve_boolean_details",
121+
"default_value": False,
122+
},
123+
{"reason": "TARGETING_MATCH", "variant": "target_variant", "value": True},
124+
),
125+
(
126+
{
127+
"variants": {"default_variant": "default", "target_variant": "target"},
128+
"targeting": targeting(),
129+
},
130+
{
131+
"context": context("target_variant"),
132+
"method": "resolve_string_details",
133+
"default_value": "placeholder",
134+
},
135+
{
136+
"reason": "TARGETING_MATCH",
137+
"variant": "target_variant",
138+
"value": "target",
139+
},
140+
),
141+
(
142+
{
143+
"variants": {"default_variant": 1.0, "target_variant": 2.0},
144+
"targeting": targeting(),
145+
},
146+
{
147+
"context": context("target_variant"),
148+
"method": "resolve_float_details",
149+
"default_value": 0.0,
150+
},
151+
{"reason": "TARGETING_MATCH", "variant": "target_variant", "value": 2.0},
152+
),
153+
(
154+
{
155+
"variants": {"default_variant": True, "target_variant": False},
156+
"targeting": targeting(),
157+
},
158+
{
159+
"context": context("target_variant"),
160+
"method": "resolve_boolean_details",
161+
"default_value": True,
162+
},
163+
{"reason": "TARGETING_MATCH", "variant": "target_variant", "value": False},
164+
),
165+
(
166+
{
167+
"variants": {"default_variant": 10, "target_variant": 0},
168+
"targeting": targeting(),
169+
},
170+
{
171+
"context": context("target_variant"),
172+
"method": "resolve_integer_details",
173+
"default_value": 1,
174+
},
175+
{"reason": "TARGETING_MATCH", "variant": "target_variant", "value": 0},
176+
),
177+
(
178+
{
179+
"variants": {"default_variant": {}, "target_variant": {}},
180+
"targeting": targeting(),
181+
},
182+
{
183+
"context": context("target_variant"),
184+
"method": "resolve_object_details",
185+
"default_value": {},
186+
},
187+
{"reason": "TARGETING_MATCH", "variant": "target_variant", "value": {}},
188+
),
189+
],
190+
ids=[
191+
"static_flag",
192+
"boolean_default_fallback",
193+
"boolean_targeting_match",
194+
"string_targeting_match",
195+
"float_targeting_match",
196+
"boolean_falsy_target",
197+
"integer_falsy_target",
198+
"object_falsy_target",
199+
],
200+
)
201+
def test_resolver_details(
202+
resolver,
203+
flag,
204+
input_config,
205+
resolve_config,
206+
expected,
207+
):
208+
flag.variants = input_config["variants"]
209+
flag.targeting = input_config["targeting"]
210+
resolver.flag_store.get_flag = Mock(return_value=flag)
211+
212+
result = getattr(resolver, resolve_config["method"])(
213+
"flag", resolve_config["default_value"], resolve_config["context"]
214+
)
215+
216+
assert result.reason == expected["reason"]
217+
assert result.variant == expected["variant"]
218+
assert result.value == expected["value"]

0 commit comments

Comments
 (0)