Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c657db3

Browse files
authoredNov 18, 2024
chore: context values are now coerced to JSON representable strings before being passed to Yggdrasil (#329)
1 parent b9c25d3 commit c657db3

File tree

3 files changed

+141
-11
lines changed

3 files changed

+141
-11
lines changed
 

‎UnleashClient/__init__.py

+31-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import warnings
66
from dataclasses import asdict
77
from datetime import datetime, timezone
8-
from typing import Callable, Optional
8+
from typing import Any, Callable, Dict, Optional
99

1010
from apscheduler.executors.pool import ThreadPoolExecutor
1111
from apscheduler.job import Job
@@ -348,13 +348,7 @@ def is_enabled(
348348
:param fallback_function: Allows users to provide a custom function to set default value.
349349
:return: Feature flag result
350350
"""
351-
context = context or {}
352-
353-
base_context = self.unleash_static_context.copy()
354-
# Update context with static values and allow context to override environment
355-
base_context.update(context)
356-
context = base_context
357-
351+
context = self._safe_context(context)
358352
feature_enabled = self.engine.is_enabled(feature_name, context)
359353

360354
if feature_enabled is None:
@@ -399,9 +393,7 @@ def get_variant(self, feature_name: str, context: Optional[dict] = None) -> dict
399393
:param context: Dictionary with context (e.g. IPs, email) for feature toggle.
400394
:return: Variant and feature flag status.
401395
"""
402-
context = context or {}
403-
context.update(self.unleash_static_context)
404-
396+
context = self._safe_context(context)
405397
variant = self._resolve_variant(feature_name, context)
406398

407399
if not variant:
@@ -439,6 +431,34 @@ def get_variant(self, feature_name: str, context: Optional[dict] = None) -> dict
439431

440432
return variant
441433

434+
def _safe_context(self, context) -> dict:
435+
new_context: Dict[str, Any] = self.unleash_static_context.copy()
436+
new_context.update(context or {})
437+
438+
if "currentTime" not in new_context:
439+
new_context["currentTime"] = datetime.now(timezone.utc).isoformat()
440+
441+
safe_properties = new_context.get("properties", {})
442+
safe_properties = {
443+
k: self._safe_context_value(v) for k, v in safe_properties.items()
444+
}
445+
safe_context = {
446+
k: self._safe_context_value(v)
447+
for k, v in new_context.items()
448+
if k != "properties"
449+
}
450+
451+
safe_context["properties"] = safe_properties
452+
453+
return safe_context
454+
455+
def _safe_context_value(self, value):
456+
if isinstance(value, datetime):
457+
return value.isoformat()
458+
if isinstance(value, (int, float)):
459+
return str(value)
460+
return value
461+
442462
def _resolve_variant(self, feature_name: str, context: dict) -> dict:
443463
"""
444464
Resolves a feature variant.

‎tests/unit_tests/test_client.py

+56
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
22
import warnings
3+
from datetime import datetime, timezone
34
from pathlib import Path
45

56
import pytest
@@ -13,7 +14,9 @@
1314
MOCK_FEATURE_ENABLED_NO_VARIANTS_RESPONSE,
1415
MOCK_FEATURE_RESPONSE,
1516
MOCK_FEATURE_RESPONSE_PROJECT,
17+
MOCK_FEATURE_WITH_DATE_AFTER_CONSTRAINT,
1618
MOCK_FEATURE_WITH_DEPENDENCIES_RESPONSE,
19+
MOCK_FEATURE_WITH_NUMERIC_CONSTRAINT,
1720
)
1821
from tests.utilities.testing_constants import (
1922
APP_NAME,
@@ -920,3 +923,56 @@ def example_callback(event: UnleashEvent):
920923
assert unleash_client.is_enabled("testFlag")
921924
variant = unleash_client.get_variant("testVariations", context={"userId": "2"})
922925
assert variant["name"] == "VarA"
926+
927+
928+
def test_context_handles_numerics():
929+
cache = FileCache("MOCK_CACHE")
930+
cache.bootstrap_from_dict(MOCK_FEATURE_WITH_NUMERIC_CONSTRAINT)
931+
932+
unleash_client = UnleashClient(
933+
url=URL,
934+
app_name=APP_NAME,
935+
disable_metrics=True,
936+
disable_registration=True,
937+
cache=cache,
938+
environment="default",
939+
)
940+
941+
context = {"userId": 99999}
942+
943+
assert unleash_client.is_enabled("NumericConstraint", context)
944+
945+
946+
def test_context_handles_datetimes():
947+
cache = FileCache("MOCK_CACHE")
948+
cache.bootstrap_from_dict(MOCK_FEATURE_RESPONSE)
949+
950+
unleash_client = UnleashClient(
951+
url=URL,
952+
app_name=APP_NAME,
953+
disable_metrics=True,
954+
disable_registration=True,
955+
cache=cache,
956+
environment="default",
957+
)
958+
959+
current_time = datetime.fromisoformat("1834-02-20").replace(tzinfo=timezone.utc)
960+
context = {"currentTime": current_time}
961+
962+
assert unleash_client.is_enabled("testConstraintFlag", context)
963+
964+
965+
def test_context_adds_current_time_if_not_set():
966+
cache = FileCache("MOCK_CACHE")
967+
cache.bootstrap_from_dict(MOCK_FEATURE_WITH_DATE_AFTER_CONSTRAINT)
968+
969+
unleash_client = UnleashClient(
970+
url=URL,
971+
app_name=APP_NAME,
972+
disable_metrics=True,
973+
disable_registration=True,
974+
cache=cache,
975+
environment="default",
976+
)
977+
978+
assert unleash_client.is_enabled("DateConstraint")

‎tests/utilities/mocks/mock_features.py

+54
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,57 @@
298298
},
299299
],
300300
}
301+
302+
MOCK_FEATURE_WITH_NUMERIC_CONSTRAINT = {
303+
"version": 1,
304+
"features": [
305+
{
306+
"name": "NumericConstraint",
307+
"description": "Feature toggle with numeric constraint",
308+
"enabled": True,
309+
"strategies": [
310+
{
311+
"name": "default",
312+
"parameters": {},
313+
"constraints": [
314+
{
315+
"contextName": "userId",
316+
"operator": "NUM_GT",
317+
"value": "10",
318+
"inverted": False,
319+
}
320+
],
321+
}
322+
],
323+
"createdAt": "2018-10-09T06:04:05.667Z",
324+
"impressionData": False,
325+
},
326+
],
327+
}
328+
329+
MOCK_FEATURE_WITH_DATE_AFTER_CONSTRAINT = {
330+
"version": 1,
331+
"features": [
332+
{
333+
"name": "DateConstraint",
334+
"description": "Feature toggle with numeric constraint",
335+
"enabled": True,
336+
"strategies": [
337+
{
338+
"name": "default",
339+
"parameters": {},
340+
"constraints": [
341+
{
342+
"contextName": "currentTime",
343+
"operator": "DATE_AFTER",
344+
"value": "1988-06-15T06:40:17.766Z",
345+
"inverted": False,
346+
}
347+
],
348+
}
349+
],
350+
"createdAt": "2018-10-09T06:04:05.667Z",
351+
"impressionData": False,
352+
},
353+
],
354+
}

0 commit comments

Comments
 (0)
Failed to load comments.