Skip to content

Commit 0f55db9

Browse files
authored
Merge pull request #5 from splitio/FME-10905-async-mode
added support for async mode
2 parents a66f1d3 + e54b2b5 commit 0f55db9

File tree

5 files changed

+492
-46
lines changed

5 files changed

+492
-46
lines changed

split_openfeature/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from split_openfeature.split_provider import SplitProvider
1+
from split_openfeature.split_provider import SplitProvider, SplitProviderAsync
22
from split_openfeature.split_client_wrapper import SplitClientWrapper
33

split_openfeature/split_client_wrapper.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from splitio import get_factory
2-
from splitio.client.client import Client
1+
from splitio import get_factory, get_factory_async
32
from splitio.exceptions import TimeoutException
43
import logging
54

@@ -14,29 +13,50 @@ def __init__(self, initial_context):
1413
if not self._validate_context(initial_context):
1514
raise AttributeError()
1615

16+
self._api_key = initial_context.get("SdkKey")
17+
self._config = {}
18+
if initial_context.get("ConfigOptions") != None:
19+
self._config = initial_context.get("ConfigOptions")
20+
21+
self._ready_block_time = 10
22+
if initial_context.get("ReadyBlockTime") != None:
23+
self._ready_block_time = initial_context.get("ReadyBlockTime")
24+
25+
if initial_context.get("ThreadingMode") != None:
26+
self._threading_mode = initial_context.get("ThreadingMode")
27+
if self._threading_mode == "asyncio":
28+
self._initial_context = initial_context
29+
return
30+
1731
if initial_context.get("SplitClient") != None:
1832
self.split_client = initial_context.get("SplitClient")
1933
self._factory = self.split_client._factory
2034
return
21-
22-
api_key = initial_context.get("SdkKey")
23-
config = {}
24-
if initial_context.get("ConfigOptions") != None:
25-
config = initial_context.get("ConfigOptions")
2635

27-
self._factory = get_factory(api_key, config=config)
28-
ready_block_time = 10
29-
if initial_context.get("ReadyBlockTime") != None:
30-
ready_block_time = initial_context.get("ReadyBlockTime")
36+
try:
37+
self._factory = get_factory(self._api_key, config=self._config)
38+
self._factory.block_until_ready(self._ready_block_time)
39+
self.sdk_ready = True
40+
except TimeoutException:
41+
_LOGGER.debug("Split SDK timed out")
42+
43+
self.split_client = self._factory.client()
44+
45+
async def create(self):
46+
if self._initial_context.get("SplitClient") != None:
47+
self.split_client = self._initial_context.get("SplitClient")
48+
self._factory = self.split_client._factory
49+
return
3150

3251
try:
33-
self._factory.block_until_ready(ready_block_time)
52+
self._factory = await get_factory_async(self._api_key, config=self._config)
53+
await self._factory.block_until_ready(self._ready_block_time)
3454
self.sdk_ready = True
3555
except TimeoutException:
3656
_LOGGER.debug("Split SDK timed out")
3757

3858
self.split_client = self._factory.client()
39-
59+
4060
def is_sdk_ready(self):
4161
if self.sdk_ready:
4262
return True
@@ -48,6 +68,18 @@ def is_sdk_ready(self):
4868
_LOGGER.debug("Split SDK timed out")
4969

5070
return self.sdk_ready
71+
72+
async def is_sdk_ready_async(self):
73+
if self.sdk_ready:
74+
return True
75+
76+
try:
77+
await self._factory.block_until_ready(0.1)
78+
self.sdk_ready = True
79+
except TimeoutException:
80+
_LOGGER.debug("Split SDK timed out")
81+
82+
return self.sdk_ready
5183

5284
def _validate_context(self, initial_context):
5385
if initial_context != None and not isinstance(initial_context, dict):

split_openfeature/split_provider.py

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,14 @@
1111

1212
_LOGGER = logging.getLogger(__name__)
1313

14-
class SplitProvider(AbstractProvider):
15-
16-
def __init__(self, initial_context):
17-
self._split_client_wrapper = SplitClientWrapper(initial_context)
14+
class SplitProviderBase(AbstractProvider):
1815

1916
def get_metadata(self) -> Metadata:
2017
return Metadata("Split")
2118

2219
def get_provider_hooks(self) -> typing.List[Hook]:
2320
return []
2421

25-
def resolve_boolean_details(self, flag_key: str, default_value: bool,
26-
evaluation_context: EvaluationContext = EvaluationContext()):
27-
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
28-
29-
def resolve_string_details(self, flag_key: str, default_value: str,
30-
evaluation_context: EvaluationContext = EvaluationContext()):
31-
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
32-
33-
def resolve_integer_details(self, flag_key: str, default_value: int,
34-
evaluation_context: EvaluationContext = EvaluationContext()):
35-
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
36-
37-
def resolve_float_details(self, flag_key: str, default_value: float,
38-
evaluation_context: EvaluationContext = EvaluationContext()):
39-
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
40-
41-
def resolve_object_details(self, flag_key: str, default_value: dict,
42-
evaluation_context: EvaluationContext = EvaluationContext()):
43-
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
44-
4522
def _evaluate_treatment(self, key: str, evaluation_context: EvaluationContext, default_value):
4623
if evaluation_context is None:
4724
raise GeneralError("Evaluation Context must be provided for the Split Provider")
@@ -54,9 +31,12 @@ def _evaluate_treatment(self, key: str, evaluation_context: EvaluationContext, d
5431
if not targeting_key:
5532
raise TargetingKeyMissingError("Missing targeting key")
5633

34+
attributes = SplitProvider.transform_context(evaluation_context)
35+
evaluated = self._split_client_wrapper.split_client.get_treatment_with_config(targeting_key, key, attributes)
36+
return self._process_treatment(evaluated, default_value)
37+
38+
def _process_treatment(self, evaluated, default_value):
5739
try:
58-
attributes = SplitProvider.transform_context(evaluation_context)
59-
evaluated = self._split_client_wrapper.split_client.get_treatment_with_config(targeting_key, key, attributes)
6040
treatment = None
6141
config = None
6242
if evaluated != None:
@@ -116,3 +96,111 @@ def construct_flag_resolution(value, variant, config, reason: Reason = Reason.TA
11696
error_code: ErrorCode = None):
11797
return FlagResolutionDetails(value=value, error_code=error_code, reason=reason, variant=variant,
11898
flag_metadata={"config": config})
99+
100+
def resolve_boolean_details(self, flag_key: str, default_value: bool,
101+
evaluation_context: EvaluationContext = EvaluationContext()):
102+
pass
103+
104+
def resolve_string_details(self, flag_key: str, default_value: str,
105+
evaluation_context: EvaluationContext = EvaluationContext()):
106+
pass
107+
108+
def resolve_integer_details(self, flag_key: str, default_value: int,
109+
evaluation_context: EvaluationContext = EvaluationContext()):
110+
pass
111+
112+
def resolve_float_details(self, flag_key: str, default_value: float,
113+
evaluation_context: EvaluationContext = EvaluationContext()):
114+
pass
115+
116+
def resolve_object_details(self, flag_key: str, default_value: dict,
117+
evaluation_context: EvaluationContext = EvaluationContext()):
118+
pass
119+
120+
async def resolve_boolean_details_async(self, flag_key: str, default_value: bool,
121+
evaluation_context: EvaluationContext = EvaluationContext()):
122+
pass
123+
124+
async def resolve_string_details_async(self, flag_key: str, default_value: str,
125+
evaluation_context: EvaluationContext = EvaluationContext()):
126+
pass
127+
async def resolve_integer_details_async(self, flag_key: str, default_value: int,
128+
evaluation_context: EvaluationContext = EvaluationContext()):
129+
pass
130+
131+
async def resolve_float_details_async(self, flag_key: str, default_value: float,
132+
evaluation_context: EvaluationContext = EvaluationContext()):
133+
pass
134+
135+
async def resolve_object_details_async(self, flag_key: str, default_value: dict,
136+
evaluation_context: EvaluationContext = EvaluationContext()):
137+
pass
138+
139+
class SplitProvider(SplitProviderBase):
140+
def __init__(self, initial_context):
141+
self._split_client_wrapper = SplitClientWrapper(initial_context)
142+
143+
def resolve_boolean_details(self, flag_key: str, default_value: bool,
144+
evaluation_context: EvaluationContext = EvaluationContext()):
145+
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
146+
147+
def resolve_string_details(self, flag_key: str, default_value: str,
148+
evaluation_context: EvaluationContext = EvaluationContext()):
149+
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
150+
151+
def resolve_integer_details(self, flag_key: str, default_value: int,
152+
evaluation_context: EvaluationContext = EvaluationContext()):
153+
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
154+
155+
def resolve_float_details(self, flag_key: str, default_value: float,
156+
evaluation_context: EvaluationContext = EvaluationContext()):
157+
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
158+
159+
def resolve_object_details(self, flag_key: str, default_value: dict,
160+
evaluation_context: EvaluationContext = EvaluationContext()):
161+
return self._evaluate_treatment(flag_key, evaluation_context, default_value)
162+
163+
class SplitProviderAsync(SplitProviderBase):
164+
def __init__(self, initial_context):
165+
if isinstance(initial_context, dict):
166+
initial_context["ThreadingMode"] = "asyncio"
167+
self._split_client_wrapper = SplitClientWrapper(initial_context)
168+
169+
async def create(self):
170+
await self._split_client_wrapper.create()
171+
172+
async def resolve_boolean_details_async(self, flag_key: str, default_value: bool,
173+
evaluation_context: EvaluationContext = EvaluationContext()):
174+
return await self._evaluate_treatment_async(flag_key, evaluation_context, default_value)
175+
176+
async def resolve_string_details_async(self, flag_key: str, default_value: str,
177+
evaluation_context: EvaluationContext = EvaluationContext()):
178+
return await self._evaluate_treatment_async(flag_key, evaluation_context, default_value)
179+
180+
async def resolve_integer_details_async(self, flag_key: str, default_value: int,
181+
evaluation_context: EvaluationContext = EvaluationContext()):
182+
return await self._evaluate_treatment_async(flag_key, evaluation_context, default_value)
183+
184+
async def resolve_float_details_async(self, flag_key: str, default_value: float,
185+
evaluation_context: EvaluationContext = EvaluationContext()):
186+
return await self._evaluate_treatment_async(flag_key, evaluation_context, default_value)
187+
188+
async def resolve_object_details_async(self, flag_key: str, default_value: dict,
189+
evaluation_context: EvaluationContext = EvaluationContext()):
190+
return await self._evaluate_treatment_async(flag_key, evaluation_context, default_value)
191+
192+
async def _evaluate_treatment_async(self, key: str, evaluation_context: EvaluationContext, default_value):
193+
if evaluation_context is None:
194+
raise GeneralError("Evaluation Context must be provided for the Split Provider")
195+
196+
if not await self._split_client_wrapper.is_sdk_ready_async():
197+
return SplitProvider.construct_flag_resolution(default_value, None, None, Reason.ERROR,
198+
ErrorCode.PROVIDER_NOT_READY)
199+
200+
targeting_key = evaluation_context.targeting_key
201+
if not targeting_key:
202+
raise TargetingKeyMissingError("Missing targeting key")
203+
204+
attributes = SplitProvider.transform_context(evaluation_context)
205+
evaluated = await self._split_client_wrapper.split_client.get_treatment_with_config(targeting_key, key, attributes)
206+
return self._process_treatment(evaluated, default_value)

tests/test_split_client_wrapper.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import pytest
2-
from splitio import get_factory
3-
from split_openfeature import SplitClientWrapper
42
import unittest
53

6-
class TestSplitClientWrapper(unittest.TestCase):
4+
from splitio import get_factory, get_factory_async
5+
from split_openfeature import SplitClientWrapper
76

7+
class TestSplitClientWrapper(unittest.TestCase):
88
def test_using_external_splitclient(self):
99
split_factory = get_factory("localhost", config={"splitFile": "split.yaml"})
1010
split_factory.block_until_ready(5)
@@ -19,7 +19,6 @@ def test_using_internal_splitclient(self):
1919
assert wrapper.is_sdk_ready()
2020
assert wrapper.sdk_ready == 1
2121

22-
2322
def test_sdk_not_ready(self):
2423
wrapper = SplitClientWrapper({"ReadyBlockTime": 0.1, "SdkKey": "api", "ConfigOptions": {}})
2524
assert not wrapper.is_sdk_ready()
@@ -39,3 +38,28 @@ def test_no_params(self):
3938
def test_reqwuired_params(self):
4039
with self.assertRaises(AttributeError) as context:
4140
wrapper = SplitClientWrapper({"ConfigOptions": {}})
41+
42+
class TestSplitClientWrapperAsync(object):
43+
@pytest.mark.asyncio
44+
async def test_using_external_splitclient_async(self):
45+
split_factory = await get_factory_async("localhost", config={"splitFile": "split.yaml"})
46+
await split_factory.block_until_ready(5)
47+
split_client = split_factory.client()
48+
wrapper = SplitClientWrapper({"SplitClient": split_client, "ThreadingMode": "asyncio"})
49+
await wrapper.create()
50+
assert wrapper.split_client != None
51+
assert await wrapper.is_sdk_ready_async()
52+
53+
@pytest.mark.asyncio
54+
async def test_using_internal_splitclient_async(self):
55+
wrapper = SplitClientWrapper({"ReadyBlockTime": 1, "SdkKey": "localhost", "ConfigOptions": {"splitFile": "split.yaml"}, "ThreadingMode": "asyncio"})
56+
await wrapper.create()
57+
assert wrapper.split_client != None
58+
assert await wrapper.is_sdk_ready_async()
59+
assert wrapper.sdk_ready == True
60+
61+
@pytest.mark.asyncio
62+
async def test_sdk_not_ready_async(self):
63+
wrapper = SplitClientWrapper({"ReadyBlockTime": 0.1, "SdkKey": "api", "ConfigOptions": {}, "ThreadingMode": "asyncio"})
64+
await wrapper.create()
65+
assert not await wrapper.is_sdk_ready_async()

0 commit comments

Comments
 (0)