import json
import re
import hashlib
from typing import Any, Dict, List
def evaluate_condition(user: Dict[str, Any], condition: Dict[str, Any]) -> bool:
"""
Evaluate one condition against one user.
Supported operators:
- eq
- in
- regex
- ~=
Missing/None values fail closed rather than throwing.
"""
attribute = condition.get("attribute")
operator = condition.get("operator")
expected = condition.get("value")
actual = user.get(attribute)
if actual is None:
return False
if operator == "eq":
return actual == expected
if operator == "in":
if not isinstance(expected, (list, tuple, set)):
return False
return actual in expected
if operator == "regex":
try:
return re.match(str(expected), str(actual)) is not None
except re.error:
return False
if operator == "~=":
return approximate_match(actual, expected)
return False
def approximate_match(actual: Any, expected: Any) -> bool:
"""
Example interpretation of ~=.
Adjust this if the provided fixtures demonstrate different semantics.
Here:
- strings: case-insensitive substring match
- numbers: numeric equality within a tiny tolerance
"""
if isinstance(actual, (int, float)) and isinstance(expected, (int, float)):
return abs(actual - expected) < 1e-9
return str(expected).lower() in str(actual).lower()
def rule_conditions_match(
user: Dict[str, Any],
rule: Dict[str, Any]
) -> bool:
"""
All conditions inside a rule use logical AND.
"""
conditions = rule.get("conditions", [])
return all(
evaluate_condition(user, condition)
for condition in conditions
)
def stable_bucket(flag_key: str, user_id: Any) -> int:
"""
Deterministically map a user + flag to bucket [0, 99].
Keeping flag_key in the hash prevents every flag from assigning
the same users to its rollout cohort.
IMPORTANT:
Replace this hashing scheme if expected_decisions.json demonstrates
another percentage-rollout algorithm.
"""
value = f"{flag_key}:{user_id}".encode("utf-8")
digest = hashlib.sha256(value).hexdigest()
return int(digest[:8], 16) % 100
def passes_percentage_rollout(
user: Dict[str, Any],
flag: Dict[str, Any],
rule: Dict[str, Any]
) -> bool:
"""
Apply percentage rollout if the rule contains a `percentage` field.
Percentage:
0 -> nobody
30 -> buckets 0..29
100 -> everybody
"""
percentage = rule.get("percentage")
if percentage is None:
return True
try:
percentage = float(percentage)
except (TypeError, ValueError):
return False
if percentage <= 0:
return False
if percentage >= 100:
return True
user_id = user.get("user_id")
if user_id is None:
return False
bucket = stable_bucket(
flag_key=flag.get("key", ""),
user_id=user_id,
)
return bucket < percentage
def rule_matches(
user: Dict[str, Any],
flag: Dict[str, Any],
rule: Dict[str, Any]
) -> bool:
"""
A rule matches when:
1. every condition matches
2. its percentage rollout includes the user
"""
if not rule_conditions_match(user, rule):
return False
if not passes_percentage_rollout(user, flag, rule):
return False
return True
def evaluate_flag(
user: Dict[str, Any],
flag: Dict[str, Any]
) -> bool:
"""
Evaluate one feature flag for one user.
Semantics:
1. Disabled flag -> default immediately
2. All matching rules are considered
3. If no rules match -> default
4. If multiple rules match -> true wins
"""
default = bool(flag.get("default", False))
if not flag.get("enabled", False):
return default
matched_values = []
for rule in flag.get("rules", []):
if rule_matches(user, flag, rule):
matched_values.append(bool(rule.get("value", False)))
if not matched_values:
return default
return any(matched_values)
def evaluate_all_flags(
user: Dict[str, Any],
flags: List[Dict[str, Any]]
) -> Dict[str, bool]:
"""
Evaluate every flag for one user.
"""
return {
flag["key"]: evaluate_flag(user, flag)
for flag in flags
}
def evaluate_all_users(
users: List[Dict[str, Any]],
flags: List[Dict[str, Any]]
) -> Dict[str, Dict[str, bool]]:
"""
Evaluate all flags for all users.
"""
results = {}
for user in users:
user_id = str(user["user_id"])
results[user_id] = evaluate_all_flags(
user=user,
flags=flags,
)
return results
if __name__ == "__main__":
with open("users.json", "r") as f:
users = json.load(f)
with open("flags.json", "r") as f:
flags = json.load(f)
results = evaluate_all_users(users, flags)
print(json.dumps(results, indent=2))