Skip to content

Commit 7e267c8

Browse files
msiebertclaude
andcommitted
Add OpenFeature provider package
Implement an OpenFeature provider that wraps the Mixpanel Python SDK's local or remote feature flags provider, enabling standardized feature flag evaluation via the OpenFeature API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b0fc5e5 commit 7e267c8

4 files changed

Lines changed: 454 additions & 0 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "mixpanel-openfeature"
7+
version = "0.1.0"
8+
description = "OpenFeature provider for the Mixpanel Python SDK"
9+
license = "Apache-2.0"
10+
authors = [
11+
{name = "Mixpanel, Inc.", email = "dev@mixpanel.com"},
12+
]
13+
requires-python = ">=3.9"
14+
dependencies = [
15+
"mixpanel",
16+
"openfeature-sdk>=0.7.0",
17+
]
18+
19+
[project.optional-dependencies]
20+
test = [
21+
"pytest>=8.4.1",
22+
"pytest-asyncio>=0.23.0",
23+
]
24+
25+
[tool.setuptools.packages.find]
26+
where = ["src"]
27+
28+
[tool.pytest.ini_options]
29+
asyncio_mode = "auto"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .provider import MixpanelProvider
2+
3+
__all__ = ["MixpanelProvider"]
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import math
2+
import typing
3+
from typing import Mapping, Sequence, Union
4+
5+
from openfeature.evaluation_context import EvaluationContext
6+
from openfeature.exception import ErrorCode
7+
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
8+
from openfeature.provider import AbstractProvider, Metadata
9+
10+
from mixpanel.flags.types import SelectedVariant
11+
12+
FlagValueType = Union[bool, str, int, float, list, dict, None]
13+
14+
15+
class MixpanelProvider(AbstractProvider):
16+
"""An OpenFeature provider backed by a Mixpanel feature flags provider."""
17+
18+
def __init__(self, flags_provider: typing.Any) -> None:
19+
super().__init__()
20+
self._flags_provider = flags_provider
21+
22+
def get_metadata(self) -> Metadata:
23+
return Metadata(name="mixpanel-provider")
24+
25+
def shutdown(self) -> None:
26+
pass
27+
28+
def resolve_boolean_details(
29+
self,
30+
flag_key: str,
31+
default_value: bool,
32+
evaluation_context: typing.Optional[EvaluationContext] = None,
33+
) -> FlagResolutionDetails[bool]:
34+
return self._resolve(flag_key, default_value, bool)
35+
36+
def resolve_string_details(
37+
self,
38+
flag_key: str,
39+
default_value: str,
40+
evaluation_context: typing.Optional[EvaluationContext] = None,
41+
) -> FlagResolutionDetails[str]:
42+
return self._resolve(flag_key, default_value, str)
43+
44+
def resolve_integer_details(
45+
self,
46+
flag_key: str,
47+
default_value: int,
48+
evaluation_context: typing.Optional[EvaluationContext] = None,
49+
) -> FlagResolutionDetails[int]:
50+
return self._resolve(flag_key, default_value, int)
51+
52+
def resolve_float_details(
53+
self,
54+
flag_key: str,
55+
default_value: float,
56+
evaluation_context: typing.Optional[EvaluationContext] = None,
57+
) -> FlagResolutionDetails[float]:
58+
return self._resolve(flag_key, default_value, float)
59+
60+
def resolve_object_details(
61+
self,
62+
flag_key: str,
63+
default_value: Union[Sequence[FlagValueType], Mapping[str, FlagValueType]],
64+
evaluation_context: typing.Optional[EvaluationContext] = None,
65+
) -> FlagResolutionDetails[
66+
Union[Sequence[FlagValueType], Mapping[str, FlagValueType]]
67+
]:
68+
return self._resolve(flag_key, default_value, None)
69+
70+
def _resolve(
71+
self,
72+
flag_key: str,
73+
default_value: typing.Any,
74+
expected_type: typing.Optional[type],
75+
) -> FlagResolutionDetails:
76+
if not self._are_flags_ready():
77+
return FlagResolutionDetails(
78+
value=default_value,
79+
error_code=ErrorCode.PROVIDER_NOT_READY,
80+
reason=Reason.ERROR,
81+
)
82+
83+
fallback = SelectedVariant(variant_value=default_value)
84+
result = self._flags_provider.get_variant(flag_key, fallback, {})
85+
86+
if result is fallback:
87+
return FlagResolutionDetails(
88+
value=default_value,
89+
error_code=ErrorCode.FLAG_NOT_FOUND,
90+
reason=Reason.ERROR,
91+
)
92+
93+
value = result.variant_value
94+
95+
if expected_type is None:
96+
return FlagResolutionDetails(value=value, reason=Reason.STATIC)
97+
98+
if expected_type is int and isinstance(value, float):
99+
if math.isfinite(value) and value == math.floor(value):
100+
return FlagResolutionDetails(
101+
value=int(value), reason=Reason.STATIC
102+
)
103+
return FlagResolutionDetails(
104+
value=default_value,
105+
error_code=ErrorCode.TYPE_MISMATCH,
106+
reason=Reason.ERROR,
107+
)
108+
109+
if expected_type is float and isinstance(value, (int, float)):
110+
return FlagResolutionDetails(
111+
value=float(value), reason=Reason.STATIC
112+
)
113+
114+
if not isinstance(value, expected_type):
115+
return FlagResolutionDetails(
116+
value=default_value,
117+
error_code=ErrorCode.TYPE_MISMATCH,
118+
reason=Reason.ERROR,
119+
)
120+
121+
return FlagResolutionDetails(value=value, reason=Reason.STATIC)
122+
123+
def _are_flags_ready(self) -> bool:
124+
if hasattr(self._flags_provider, "are_flags_ready"):
125+
return self._flags_provider.are_flags_ready()
126+
return True

0 commit comments

Comments
 (0)