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
19 changes: 14 additions & 5 deletions flag_engine/context/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# generated by datamodel-codegen:
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/chore/features-contexts-in-eval-context-schema/sdk/evaluation-context.json # noqa: E501
# timestamp: 2025-08-11T18:17:29+00:00
# filename: https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json # noqa: E501
# timestamp: 2025-08-25T11:10:31+00:00

from __future__ import annotations

from typing import Any, Dict, List, Optional, TypedDict, Union
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union

from typing_extensions import NotRequired

Expand All @@ -27,12 +27,21 @@ class IdentityContext(TypedDict):
traits: NotRequired[Dict[str, Optional[Union[str, float, bool]]]]


class SegmentCondition(TypedDict):
property: NotRequired[str]
class StrValueSegmentCondition(TypedDict):
property: str
operator: ConditionOperator
value: str


class InOperatorSegmentCondition(TypedDict):
property: str
operator: Literal["IN"]
value: List[str]


SegmentCondition = Union[StrValueSegmentCondition, InOperatorSegmentCondition]


class SegmentRule(TypedDict):
type: RuleType
conditions: NotRequired[List[SegmentCondition]]
Expand Down
50 changes: 25 additions & 25 deletions flag_engine/segments/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
SegmentCondition,
SegmentContext,
SegmentRule,
StrValueSegmentCondition,
)
from flag_engine.environments.models import EnvironmentModel
from flag_engine.identities.models import IdentityModel
Expand Down Expand Up @@ -235,6 +236,29 @@ def context_matches_condition(
else None
)

if condition["operator"] == constants.IN:
if isinstance(segment_value := condition["value"], list):
in_values = segment_value
else:
try:
in_values = json.loads(segment_value)
# Only accept JSON lists.
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
# but we aim to ditch the pydantic dependency in the future.
if not isinstance(in_values, list):
raise ValueError
except ValueError:
in_values = segment_value.split(",")
in_values = [str(value) for value in in_values]
# Guard against comparing boolean values to numeric strings.
if isinstance(context_value, int) and not (
context_value is True or context_value is False
):
context_value = str(context_value)
return context_value in in_values

condition = typing.cast(StrValueSegmentCondition, condition)

if condition["operator"] == constants.PERCENTAGE_SPLIT:
if context_value is not None:
object_ids = [segment_key, context_value]
Expand Down Expand Up @@ -270,7 +294,7 @@ def get_context_value(


def _matches_context_value(
condition: SegmentCondition,
condition: StrValueSegmentCondition,
context_value: ContextValue,
) -> bool:
if matcher := MATCHERS_BY_OPERATOR.get(condition["operator"]):
Expand Down Expand Up @@ -316,29 +340,6 @@ def _evaluate_modulo(
return context_value % divisor == remainder


def _evaluate_in(
segment_value: typing.Optional[str], context_value: ContextValue
) -> bool:
if segment_value:
try:
in_values = json.loads(segment_value)
# Only accept JSON lists.
# Ideally, we should use something like pydantic.TypeAdapter[list[str]],
# but we aim to ditch the pydantic dependency in the future.
if not isinstance(in_values, list):
raise ValueError
in_values = [str(value) for value in in_values]
except ValueError:
in_values = segment_value.split(",")
# Guard against comparing boolean values to numeric strings.
if isinstance(context_value, int) and not any(
context_value is x for x in (False, True)
):
context_value = str(context_value)
return context_value in in_values
return False


def _context_value_typed(
func: typing.Callable[..., bool],
) -> typing.Callable[[typing.Optional[str], ContextValue], bool]:
Expand All @@ -365,7 +366,6 @@ def inner(
constants.NOT_CONTAINS: _evaluate_not_contains,
constants.REGEX: _evaluate_regex,
constants.MODULO: _evaluate_modulo,
constants.IN: _evaluate_in,
constants.EQUAL: _context_value_typed(operator.eq),
constants.GREATER_THAN: _context_value_typed(operator.gt),
constants.GREATER_THAN_INCLUSIVE: _context_value_typed(operator.ge),
Expand Down
238 changes: 119 additions & 119 deletions tests/unit/segments/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from flag_engine.context.types import SegmentCondition, SegmentContext, SegmentRule
from flag_engine.context.types import SegmentContext
from flag_engine.segments import constants

trait_key_1 = "email"
Expand All @@ -10,141 +10,141 @@
trait_key_3 = "date_joined"
trait_value_3 = "2021-01-01"

empty_segment = SegmentContext(key=str(1), name="empty_segment", rules=[])
empty_segment = {"key": "1", "name": "empty_segment", "rules": []}

segment_single_condition = SegmentContext(
key=str(2),
name="segment_one_condition",
rules=[
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_1,
value=trait_value_1,
)
segment_single_condition: SegmentContext = {
"key": "2",
"name": "segment_one_condition",
"rules": [
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_1,
"value": trait_value_1,
}
],
)
}
],
)
}

segment_multiple_conditions_all = SegmentContext(
key=str(3),
name="segment_multiple_conditions_all",
rules=[
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_1,
value=trait_value_1,
),
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_2,
value=trait_value_2,
),
segment_multiple_conditions_all: SegmentContext = {
"key": "3",
"name": "segment_multiple_conditions_all",
"rules": [
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_1,
"value": trait_value_1,
},
{
"operator": constants.EQUAL,
"property": trait_key_2,
"value": trait_value_2,
},
],
)
}
],
)
}

segment_multiple_conditions_any = SegmentContext(
key=str(4),
name="segment_multiple_conditions_all",
rules=[
SegmentRule(
type=constants.ANY_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_1,
value=trait_value_1,
),
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_2,
value=trait_value_2,
),
segment_multiple_conditions_any: SegmentContext = {
"key": "4",
"name": "segment_multiple_conditions_all",
"rules": [
{
"type": constants.ANY_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_1,
"value": trait_value_1,
},
{
"operator": constants.EQUAL,
"property": trait_key_2,
"value": trait_value_2,
},
],
)
}
],
)
}

segment_nested_rules = SegmentContext(
key=str(5),
name="segment_nested_rules_all",
rules=[
SegmentRule(
type=constants.ALL_RULE,
rules=[
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_1,
value=trait_value_1,
),
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_2,
value=trait_value_2,
),
segment_nested_rules: SegmentContext = {
"key": "5",
"name": "segment_nested_rules_all",
"rules": [
{
"type": constants.ALL_RULE,
"rules": [
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_1,
"value": trait_value_1,
},
{
"operator": constants.EQUAL,
"property": trait_key_2,
"value": trait_value_2,
},
],
),
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_3,
value=trait_value_3,
)
},
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_3,
"value": trait_value_3,
}
],
),
},
],
)
}
],
)
}

segment_conditions_and_nested_rules = SegmentContext(
key=str(6),
name="segment_multiple_conditions_all_and_nested_rules",
rules=[
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_1,
value=trait_value_1,
)
segment_conditions_and_nested_rules: SegmentContext = {
"key": "6",
"name": "segment_multiple_conditions_all_and_nested_rules",
"rules": [
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_1,
"value": trait_value_1,
}
],
rules=[
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_2,
value=trait_value_2,
),
"rules": [
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_2,
"value": trait_value_2,
},
],
),
SegmentRule(
type=constants.ALL_RULE,
conditions=[
SegmentCondition(
operator=constants.EQUAL,
property=trait_key_3,
value=trait_value_3,
)
},
{
"type": constants.ALL_RULE,
"conditions": [
{
"operator": constants.EQUAL,
"property": trait_key_3,
"value": trait_value_3,
}
],
),
},
],
)
}
],
)
}
Loading