Skip to content

Commit a66f1d3

Browse files
authored
Merge pull request #3 from splitio/FME-10793-splitclient-wrapper
added splitclient wrapper
2 parents 9d759e6 + c49823c commit a66f1d3

File tree

7 files changed

+136
-24
lines changed

7 files changed

+136
-24
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="split_openfeature",
8-
version="0.1.0",
8+
version="1.0.0",
99
author="Robert Grassian",
1010
author_email="robert.grassian@split.io",
1111
description="The official Python Split Provider for OpenFeature",

split_openfeature/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
from split_openfeature.split_provider import SplitProvider
2+
from split_openfeature.split_client_wrapper import SplitClientWrapper
3+
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from splitio import get_factory
2+
from splitio.client.client import Client
3+
from splitio.exceptions import TimeoutException
4+
import logging
5+
6+
_LOGGER = logging.getLogger(__name__)
7+
8+
class SplitClientWrapper():
9+
10+
def __init__(self, initial_context):
11+
self.sdk_ready = False
12+
self.split_client = None
13+
14+
if not self._validate_context(initial_context):
15+
raise AttributeError()
16+
17+
if initial_context.get("SplitClient") != None:
18+
self.split_client = initial_context.get("SplitClient")
19+
self._factory = self.split_client._factory
20+
return
21+
22+
api_key = initial_context.get("SdkKey")
23+
config = {}
24+
if initial_context.get("ConfigOptions") != None:
25+
config = initial_context.get("ConfigOptions")
26+
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")
31+
32+
try:
33+
self._factory.block_until_ready(ready_block_time)
34+
self.sdk_ready = True
35+
except TimeoutException:
36+
_LOGGER.debug("Split SDK timed out")
37+
38+
self.split_client = self._factory.client()
39+
40+
def is_sdk_ready(self):
41+
if self.sdk_ready:
42+
return True
43+
44+
try:
45+
self._factory.block_until_ready(0.1)
46+
self.sdk_ready = True
47+
except TimeoutException:
48+
_LOGGER.debug("Split SDK timed out")
49+
50+
return self.sdk_ready
51+
52+
def _validate_context(self, initial_context):
53+
if initial_context != None and not isinstance(initial_context, dict):
54+
_LOGGER.error("SplitClientWrapper: initial_context must be of type `dict`")
55+
return False
56+
57+
if initial_context.get("SplitClient") == None and initial_context.get("SdkKey") == None:
58+
_LOGGER.error("SplitClientWrapper: initial_context must contain keys `SplitClient` or `SdkKey`")
59+
return False
60+
61+
if initial_context.get("SdkKey") != None and not isinstance(initial_context.get("SdkKey"), str):
62+
_LOGGER.error("SplitClientWrapper: key `SdkKey` must be of type `str`")
63+
return False
64+
65+
if initial_context.get("ConfigOptions") != None and not isinstance(initial_context.get("ConfigOptions"), dict):
66+
_LOGGER.error("SplitClientWrapper: key `ConfigOptions` must be of type `dict`")
67+
return False
68+
69+
return True

split_openfeature/split_provider.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,15 @@
66
from openfeature.exception import ErrorCode, GeneralError, ParseError, OpenFeatureError, TargetingKeyMissingError
77
from openfeature.flag_evaluation import Reason, FlagResolutionDetails
88
from openfeature.provider import AbstractProvider, Metadata
9-
from splitio import get_factory
10-
from splitio.exceptions import TimeoutException
9+
from split_openfeature.split_client_wrapper import SplitClientWrapper
1110
import json
1211

1312
_LOGGER = logging.getLogger(__name__)
1413

1514
class SplitProvider(AbstractProvider):
1615

17-
def __init__(self, api_key="", client=None):
18-
if api_key == "" and client is None:
19-
raise Exception("Must provide apiKey or Split Client")
20-
21-
if api_key != "":
22-
factory = get_factory(api_key)
23-
try:
24-
factory.block_until_ready(1)
25-
except TimeoutException:
26-
raise GeneralError("Error occurred initializing the client.")
27-
28-
self.split_client = factory.client()
29-
else:
30-
self.split_client = client
16+
def __init__(self, initial_context):
17+
self._split_client_wrapper = SplitClientWrapper(initial_context)
3118

3219
def get_metadata(self) -> Metadata:
3320
return Metadata("Split")
@@ -59,13 +46,17 @@ def _evaluate_treatment(self, key: str, evaluation_context: EvaluationContext, d
5946
if evaluation_context is None:
6047
raise GeneralError("Evaluation Context must be provided for the Split Provider")
6148

49+
if not self._split_client_wrapper.is_sdk_ready():
50+
return SplitProvider.construct_flag_resolution(default_value, None, None, Reason.ERROR,
51+
ErrorCode.PROVIDER_NOT_READY)
52+
6253
targeting_key = evaluation_context.targeting_key
6354
if not targeting_key:
6455
raise TargetingKeyMissingError("Missing targeting key")
6556

6657
try:
6758
attributes = SplitProvider.transform_context(evaluation_context)
68-
evaluated = self.split_client.get_treatment_with_config(targeting_key, key, attributes)
59+
evaluated = self._split_client_wrapper.split_client.get_treatment_with_config(targeting_key, key, attributes)
6960
treatment = None
7061
config = None
7162
if evaluated != None:

tests/test_client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
from splitio import get_factory
77
from split_openfeature import SplitProvider
88

9-
109
class TestClient(object):
11-
1210
# The following are splits with treatments defined in the split.yaml file
1311
my_feature = "my_feature" # 'on' when targeting_key='key', else 'off'
1412
some_other_feature = "some_other_feature" # 'off'
@@ -21,7 +19,7 @@ def provider(self):
2119
split_factory = get_factory("localhost", config={"splitFile": "split.yaml"})
2220
split_factory.block_until_ready(5)
2321
split_client = split_factory.client()
24-
return SplitProvider(client=split_client)
22+
return SplitProvider({"SplitClient": split_client})
2523

2624
@pytest.fixture
2725
def set_provider(self, provider):
@@ -197,4 +195,9 @@ def test_obj_fail(self, client):
197195
assert details.error_code == ErrorCode.PARSE_ERROR
198196
assert details.reason == Reason.ERROR
199197
assert details.variant is None
200-
'''
198+
'''
199+
200+
class TestClientInternal(TestClient):
201+
@pytest.fixture
202+
def provider(self):
203+
return SplitProvider({"SdkKey": "localhost", "ConfigOptions": {"splitFile": "split.yaml"}})

tests/test_split_client_wrapper.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import pytest
2+
from splitio import get_factory
3+
from split_openfeature import SplitClientWrapper
4+
import unittest
5+
6+
class TestSplitClientWrapper(unittest.TestCase):
7+
8+
def test_using_external_splitclient(self):
9+
split_factory = get_factory("localhost", config={"splitFile": "split.yaml"})
10+
split_factory.block_until_ready(5)
11+
split_client = split_factory.client()
12+
wrapper = SplitClientWrapper({"SplitClient": split_client})
13+
assert wrapper.split_client != None
14+
assert wrapper.is_sdk_ready()
15+
16+
def test_using_internal_splitclient(self):
17+
wrapper = SplitClientWrapper({"ReadyBlockTime": 1, "SdkKey": "localhost", "ConfigOptions": {"splitFile": "split.yaml"}})
18+
assert wrapper.split_client != None
19+
assert wrapper.is_sdk_ready()
20+
assert wrapper.sdk_ready == 1
21+
22+
23+
def test_sdk_not_ready(self):
24+
wrapper = SplitClientWrapper({"ReadyBlockTime": 0.1, "SdkKey": "api", "ConfigOptions": {}})
25+
assert not wrapper.is_sdk_ready()
26+
27+
def test_invalid_apikey(self):
28+
with self.assertRaises(AttributeError) as context:
29+
wrapper = SplitClientWrapper({"SdkKey": 123})
30+
31+
def test_invalid_config(self):
32+
with self.assertRaises(AttributeError) as context:
33+
wrapper = SplitClientWrapper({"SdkKey": "123", "ConfigOptions": "234"})
34+
35+
def test_no_params(self):
36+
with self.assertRaises(AttributeError) as context:
37+
wrapper = SplitClientWrapper({})
38+
39+
def test_reqwuired_params(self):
40+
with self.assertRaises(AttributeError) as context:
41+
wrapper = SplitClientWrapper({"ConfigOptions": {}})

tests/test_split_provider.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class TestProvider(object):
1212

1313
def reset_client(self):
1414
self.client = MagicMock()
15-
self.provider = SplitProvider(client=self.client)
15+
self.provider = SplitProvider({"SplitClient": self.client})
1616

1717
def mock_client_return(self, val):
1818
self.client.get_treatment_with_config.return_value = (val, "{'prop':'val'}")
@@ -267,4 +267,10 @@ def test_obj_error(self):
267267
except OpenFeatureError as e:
268268
assert e.error_code == ErrorCode.PARSE_ERROR
269269
except Exception:
270-
fail("Unexpected exception occurred")
270+
fail("Unexpected exception occurred")
271+
272+
def test_sdk_not_ready(self):
273+
provider = SplitProvider({"ReadyBlockTime": 0.1,"SdkKey": "api"})
274+
details = provider.resolve_boolean_details(self.flag_name, False, self.eval_context)
275+
assert details.error_code == ErrorCode.PROVIDER_NOT_READY
276+
assert details.value == False

0 commit comments

Comments
 (0)