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
6 changes: 5 additions & 1 deletion confidence/confidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ def primitive_matches(value: FieldType, value_type: Type[Primitive]) -> bool:
return (
value_type is None
or (value_type is int and isinstance(value, int))
or (value_type is float and isinstance(value, float))
or (value_type is int and isinstance(value, float) and value == int(value))
or (value_type is float and isinstance(value, (float, int)))
or (value_type is str and isinstance(value, str))
or (value_type is bool and isinstance(value, bool))
)
Expand Down Expand Up @@ -562,6 +563,9 @@ def _select(
)
raise TypeMismatchError("type of value did not match excepted type")

if value_type is int and isinstance(value, float):
value = int(value)

return value

@staticmethod
Expand Down
145 changes: 145 additions & 0 deletions tests/test_confidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,119 @@ async def test_resolve_with_default_timeout_async(self):
_, kwargs = mock_post.call_args
self.assertEqual(kwargs["timeout"], DEFAULT_TIMEOUT_MS / 1000.0)

def test_resolve_integer_from_float_value(self):
"""Test that an integer flag value serialized as 400.0 (float in JSON)
with intSchema can be correctly resolved as an integer."""
with requests_mock.Mocker() as mock:
mock.post(
"https://resolver.confidence.dev/v1/flags:resolve",
json=INTEGER_AS_FLOAT_FLAG_RESOLVE,
)
result = self.confidence.resolve_integer_details(
flag_key="test-flag.myinteger",
default_value=-1,
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.flag_metadata["flag_key"], "test-flag.myinteger")
self.assertEqual(result.value, 400)
self.assertIsInstance(result.value, int)

async def test_resolve_integer_from_float_value_async(self):
mock_response = httpx.Response(
status_code=200,
json=INTEGER_AS_FLOAT_FLAG_RESOLVE,
request=httpx.Request(
"POST", "https://resolver.confidence.dev/v1/flags:resolve"
),
)

with patch("httpx.AsyncClient.post", return_value=mock_response):
result = await self.confidence.resolve_integer_details_async(
flag_key="test-flag.myinteger", default_value=-1
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.flag_metadata["flag_key"], "test-flag.myinteger")
self.assertEqual(result.value, 400)
self.assertIsInstance(result.value, int)

def test_resolve_integer_from_negative_float_value(self):
with requests_mock.Mocker() as mock:
mock.post(
"https://resolver.confidence.dev/v1/flags:resolve",
json=_make_int_flag_resolve(-5.0),
)
result = self.confidence.resolve_integer_details(
flag_key="test-flag.myinteger",
default_value=-1,
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.value, -5)
self.assertIsInstance(result.value, int)

def test_resolve_integer_from_zero_float_value(self):
with requests_mock.Mocker() as mock:
mock.post(
"https://resolver.confidence.dev/v1/flags:resolve",
json=_make_int_flag_resolve(0.0),
)
result = self.confidence.resolve_integer_details(
flag_key="test-flag.myinteger",
default_value=-1,
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.value, 0)
self.assertIsInstance(result.value, int)

def test_resolve_integer_rejects_non_whole_float(self):
with requests_mock.Mocker() as mock:
mock.post(
"https://resolver.confidence.dev/v1/flags:resolve",
json=_make_int_flag_resolve(400.5),
)
result = self.confidence.resolve_integer_details(
flag_key="test-flag.myinteger",
default_value=-1,
)

self.assertEqual(result.reason, Reason.ERROR)
self.assertEqual(result.value, -1)
self.assertEqual(result.error_code, ErrorCode.GENERAL)

def test_resolve_float_from_integer_value(self):
with requests_mock.Mocker() as mock:
mock.post(
"https://resolver.confidence.dev/v1/flags:resolve",
json=FLOAT_AS_INTEGER_FLAG_RESOLVE,
)
result = self.confidence.resolve_float_details(
flag_key="test-flag.mydouble",
default_value=-1.0,
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.value, 42)

async def test_resolve_float_from_integer_value_async(self):
mock_response = httpx.Response(
status_code=200,
json=FLOAT_AS_INTEGER_FLAG_RESOLVE,
request=httpx.Request(
"POST", "https://resolver.confidence.dev/v1/flags:resolve"
),
)

with patch("httpx.AsyncClient.post", return_value=mock_response):
result = await self.confidence.resolve_float_details_async(
flag_key="test-flag.mydouble", default_value=-1.0
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.value, 42)

def test_handle_actual_timeout(self):
with patch("requests.post") as mock_post:
# Simulate a timeout by raising the Timeout exception
Expand Down Expand Up @@ -514,6 +627,38 @@ async def test_handle_actual_timeout_async(self):
}"""
)

def _make_int_flag_resolve(value):
return {
"resolvedFlags": [
{
"flag": "flags/test-flag",
"variant": "flags/test-flag/variants/variant-1",
"value": {"myinteger": value},
"flagSchema": {"schema": {"myinteger": {"intSchema": {}}}},
"reason": "RESOLVE_REASON_MATCH",
"shouldApply": True,
}
],
"resolveToken": "token1",
}


INTEGER_AS_FLOAT_FLAG_RESOLVE = _make_int_flag_resolve(400.0)

FLOAT_AS_INTEGER_FLAG_RESOLVE = {
"resolvedFlags": [
{
"flag": "flags/test-flag",
"variant": "flags/test-flag/variants/variant-1",
"value": {"mydouble": 42},
"flagSchema": {"schema": {"mydouble": {"doubleSchema": {}}}},
"reason": "RESOLVE_REASON_MATCH",
"shouldApply": True,
}
],
"resolveToken": "token1",
}

SUCCESSFUL_FLAG_RESOLVE = json.loads(
"""{
"resolvedFlags": [
Expand Down
32 changes: 32 additions & 0 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,24 @@ def test_resolve_without_targeting_key(self):
last_request.json()["evaluationContext"].get("connection"), "wifi"
)

def test_resolve_integer_from_float_value(self):
ctx = EvaluationContext(targeting_key="boop")
with requests_mock.Mocker() as mock:
mock.post(
"https://resolver.confidence.dev/v1/flags:resolve",
json=INTEGER_AS_FLOAT_FLAG_RESOLVE,
)
result = self.provider.resolve_integer_details(
flag_key="test-flag.myinteger",
default_value=-1,
evaluation_context=ctx,
)

self.assertEqual(result.reason, Reason.TARGETING_MATCH)
self.assertEqual(result.flag_metadata["flag_key"], "test-flag.myinteger")
self.assertEqual(result.value, 400)
self.assertIsInstance(result.value, int)

def test_no_segment_match(self):
ctx = EvaluationContext(attributes={"connection": "wifi"})
with requests_mock.Mocker() as mock:
Expand Down Expand Up @@ -296,6 +314,20 @@ def test_no_segment_match(self):
}"""
)

INTEGER_AS_FLOAT_FLAG_RESOLVE = {
"resolvedFlags": [
{
"flag": "flags/test-flag",
"variant": "flags/test-flag/variants/variant-1",
"value": {"myinteger": 400.0},
"flagSchema": {"schema": {"myinteger": {"intSchema": {}}}},
"reason": "RESOLVE_REASON_MATCH",
"shouldApply": True,
}
],
"resolveToken": "token1",
}

SUCCESSFUL_FLAG_RESOLVE = json.loads(
"""{
"resolvedFlags": [
Expand Down