Skip to content

Commit b76a829

Browse files
authored
fix: Support flat attributes in the evaluation context (#26)
Signed-off-by: Danju Visvanathan <danju.visvanathan@gmail.com>
1 parent 47147ec commit b76a829

File tree

3 files changed

+136
-2
lines changed

3 files changed

+136
-2
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ provider = FlagsmithProvider(
5757
)
5858
```
5959

60-
The provider can then be used with the OpenFeature client as per
60+
The provider can then be used with the OpenFeature client as per
6161
[the documentation](https://openfeature.dev/docs/reference/concepts/evaluation-api#setting-a-provider).
6262

63+
### Evaluation Context
64+
65+
The evaluation context supports traits in two ways:
66+
1. Flat top-level attributes
67+
2. A nested traits object
68+
69+
The two forms are merged and sent to Flagsmith, with the traits object taking precedence if keys conflict.
70+
71+
```python
72+
context = EvaluationContext( # Traits are: {"abc":"def", "foo": "bar2"}
73+
targeting_key="user",
74+
attributes={
75+
"foo": "bar",
76+
"abc": "def",
77+
"traits": {"foo": "bar2"}
78+
},
79+
)
80+
81+
```
82+

openfeature_flagsmith/provider.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,10 @@ def _resolve(
134134

135135
def _get_flags(self, evaluation_context: EvaluationContext = EvaluationContext()):
136136
if targeting_key := evaluation_context.targeting_key:
137+
nested_traits = evaluation_context.attributes.pop("traits", {})
138+
flattened_traits = {**evaluation_context.attributes, **nested_traits}
137139
return self._client.get_identity_flags(
138140
identifier=targeting_key,
139-
traits=evaluation_context.attributes.get("traits", {}),
141+
traits=flattened_traits,
140142
)
141143
return self._client.get_environment_flags()

tests/test_provider.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,118 @@ def test_identity_flags_are_used_if_targeting_key_provided(
319319
)
320320

321321

322+
def test_identity_flags_are_used_with_flat_attributes(
323+
mock_flagsmith_client: MagicMock,
324+
) -> None:
325+
# Given
326+
key = "key"
327+
targeting_key = "targeting_key"
328+
traits = {"foo": "bar", "age": 25}
329+
value = "foo"
330+
default_value = "default"
331+
332+
provider = FlagsmithProvider(mock_flagsmith_client)
333+
334+
mock_flagsmith_client.get_environment_flags.side_effect = NotImplementedError()
335+
mock_flagsmith_client.get_identity_flags.return_value = Flags(
336+
{key: Flag(feature_id=1, feature_name=key, enabled=True, value=value)}
337+
)
338+
339+
# When
340+
result = provider.resolve_string_details(
341+
flag_key=key,
342+
default_value=default_value,
343+
evaluation_context=EvaluationContext(
344+
targeting_key=targeting_key, attributes=traits
345+
),
346+
)
347+
348+
# Then
349+
assert result.value == value
350+
assert result.error_code is None
351+
assert result.reason is None
352+
353+
mock_flagsmith_client.get_identity_flags.assert_called_once_with(
354+
identifier=targeting_key, traits=traits
355+
)
356+
357+
358+
def test_identity_flags_flat_attributes_and_nested_traits_are_merged(
359+
mock_flagsmith_client: MagicMock,
360+
) -> None:
361+
# Given
362+
key = "key"
363+
targeting_key = "targeting_key"
364+
value = "foo"
365+
default_value = "default"
366+
367+
provider = FlagsmithProvider(mock_flagsmith_client)
368+
369+
mock_flagsmith_client.get_environment_flags.side_effect = NotImplementedError()
370+
mock_flagsmith_client.get_identity_flags.return_value = Flags(
371+
{key: Flag(feature_id=1, feature_name=key, enabled=True, value=value)}
372+
)
373+
374+
# When
375+
result = provider.resolve_string_details(
376+
flag_key=key,
377+
default_value=default_value,
378+
evaluation_context=EvaluationContext(
379+
targeting_key=targeting_key,
380+
attributes={
381+
"flat_trait": "flat_value",
382+
"traits": {"nested_trait": "nested_value"},
383+
},
384+
),
385+
)
386+
387+
# Then
388+
assert result.value == value
389+
assert result.error_code is None
390+
assert result.reason is None
391+
392+
mock_flagsmith_client.get_identity_flags.assert_called_once_with(
393+
identifier=targeting_key,
394+
traits={"flat_trait": "flat_value", "nested_trait": "nested_value"},
395+
)
396+
397+
398+
def test_identity_flags_nested_traits_take_precedence_over_flat_attributes(
399+
mock_flagsmith_client: MagicMock,
400+
) -> None:
401+
# Given
402+
key = "key"
403+
targeting_key = "targeting_key"
404+
value = "foo"
405+
default_value = "default"
406+
407+
provider = FlagsmithProvider(mock_flagsmith_client)
408+
409+
mock_flagsmith_client.get_environment_flags.side_effect = NotImplementedError()
410+
mock_flagsmith_client.get_identity_flags.return_value = Flags(
411+
{key: Flag(feature_id=1, feature_name=key, enabled=True, value=value)}
412+
)
413+
414+
# When
415+
provider.resolve_string_details(
416+
flag_key=key,
417+
default_value=default_value,
418+
evaluation_context=EvaluationContext(
419+
targeting_key=targeting_key,
420+
attributes={
421+
"shared_key": "flat_value",
422+
"traits": {"shared_key": "nested_value"},
423+
},
424+
),
425+
)
426+
427+
# Then
428+
mock_flagsmith_client.get_identity_flags.assert_called_once_with(
429+
identifier=targeting_key,
430+
traits={"shared_key": "nested_value"},
431+
)
432+
433+
322434
def test_resolve_boolean_details_uses_enabled_when_use_boolean_config_value_is_false(
323435
mock_flagsmith_client: MagicMock,
324436
) -> None:

0 commit comments

Comments
 (0)