devcycle-python-server-sdk 3.3.6__py3-none-any.whl → 3.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devcycle_python_sdk/VERSION.txt +1 -1
- devcycle_python_sdk/__init__.py +1 -0
- devcycle_python_sdk/api/config_client.py +18 -6
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/cloud_client.py +19 -2
- devcycle_python_sdk/devcycle_client.py +40 -0
- devcycle_python_sdk/local_client.py +16 -2
- devcycle_python_sdk/managers/config_manager.py +7 -2
- devcycle_python_sdk/models/bucketed_config.py +10 -6
- devcycle_python_sdk/models/user.py +86 -3
- devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
- devcycle_python_sdk/open_feature_provider/provider.py +125 -0
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/METADATA +3 -2
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/RECORD +28 -20
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/WHEEL +1 -1
- example/local_bucketing_client_example.py +1 -1
- example/openfeature_example.py +77 -0
- test/api/test_config_client.py +24 -6
- test/api/test_local_bucketing.py +15 -12
- test/managers/test_config_manager.py +32 -10
- test/models/test_user.py +188 -0
- test/openfeature_test/__init__.py +0 -0
- test/openfeature_test/test_provider.py +219 -0
- test/openfeature_test/test_provider_local_sdk.py +136 -0
- test/test_local_client.py +10 -0
- test/util/test_version.py +1 -1
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/LICENSE +0 -0
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
|
|
6
|
+
|
|
7
|
+
from openfeature import api
|
|
8
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
9
|
+
|
|
10
|
+
FLAG_KEY = "test-boolean-variable"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""
|
|
17
|
+
Sample usage of the DevCycle OpenFeature Provider along with the Python Server SDK using Local Bucketing.
|
|
18
|
+
"""
|
|
19
|
+
logging.basicConfig(level="INFO", format="%(levelname)s: %(message)s")
|
|
20
|
+
|
|
21
|
+
# create an instance of the DevCycle Client object
|
|
22
|
+
server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
|
|
23
|
+
devcycle_client = DevCycleLocalClient(server_sdk_key, DevCycleLocalOptions())
|
|
24
|
+
|
|
25
|
+
# Wait for DevCycle to initialize and load the configuration
|
|
26
|
+
for i in range(10):
|
|
27
|
+
if devcycle_client.is_initialized():
|
|
28
|
+
break
|
|
29
|
+
logger.info("Waiting for DevCycle to initialize...")
|
|
30
|
+
time.sleep(0.5)
|
|
31
|
+
else:
|
|
32
|
+
logger.error("DevCycle failed to initialize")
|
|
33
|
+
exit(1)
|
|
34
|
+
|
|
35
|
+
# set the provider for OpenFeature
|
|
36
|
+
api.set_provider(devcycle_client.get_openfeature_provider())
|
|
37
|
+
|
|
38
|
+
# get the OpenFeature client
|
|
39
|
+
open_feature_client = api.get_client()
|
|
40
|
+
|
|
41
|
+
# create the request context for the user
|
|
42
|
+
context = EvaluationContext(
|
|
43
|
+
targeting_key="test-1234",
|
|
44
|
+
attributes={
|
|
45
|
+
"email": "test-user@domain.com",
|
|
46
|
+
"name": "Test User",
|
|
47
|
+
"language": "en",
|
|
48
|
+
"country": "CA",
|
|
49
|
+
"appVersion": "1.0.11",
|
|
50
|
+
"appBuild": 1000,
|
|
51
|
+
"customData": {"custom": "data"},
|
|
52
|
+
"privateCustomData": {"private": "data"},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Look up the value of the flag
|
|
57
|
+
if open_feature_client.get_boolean_value(FLAG_KEY, False, context):
|
|
58
|
+
logger.info(f"Variable {FLAG_KEY} is enabled")
|
|
59
|
+
else:
|
|
60
|
+
logger.info(f"Variable {FLAG_KEY} is not enabled")
|
|
61
|
+
|
|
62
|
+
# Fetch a JSON object variable
|
|
63
|
+
json_object = open_feature_client.get_object_value(
|
|
64
|
+
"test-json-variable", {"default": "value"}, context
|
|
65
|
+
)
|
|
66
|
+
logger.info(f"JSON Object Value: {json_object}")
|
|
67
|
+
|
|
68
|
+
# Retrieve a string variable along with resolution details
|
|
69
|
+
details = open_feature_client.get_string_details("doesnt-exist", "default", context)
|
|
70
|
+
logger.info(f"String Value: {details.value}")
|
|
71
|
+
logger.info(f"Eval Reason: {details.reason}")
|
|
72
|
+
|
|
73
|
+
devcycle_client.close()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
test/api/test_config_client.py
CHANGED
|
@@ -2,6 +2,9 @@ import logging
|
|
|
2
2
|
import unittest
|
|
3
3
|
import uuid
|
|
4
4
|
from http import HTTPStatus
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from email.utils import formatdate
|
|
7
|
+
from time import mktime
|
|
5
8
|
|
|
6
9
|
import requests
|
|
7
10
|
import responses
|
|
@@ -36,6 +39,9 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
36
39
|
|
|
37
40
|
options = DevCycleLocalOptions(config_retry_delay_ms=0)
|
|
38
41
|
self.test_client = ConfigAPIClient(self.sdk_key, options)
|
|
42
|
+
now = datetime.now()
|
|
43
|
+
stamp = mktime(now.timetuple())
|
|
44
|
+
self.test_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
|
|
39
45
|
self.test_etag = str(uuid.uuid4())
|
|
40
46
|
self.test_config_json: dict = small_config_json()
|
|
41
47
|
|
|
@@ -45,14 +51,20 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
45
51
|
@responses.activate
|
|
46
52
|
def test_get_config(self):
|
|
47
53
|
new_etag = str(uuid.uuid4())
|
|
54
|
+
now = datetime.now()
|
|
55
|
+
stamp = mktime(now.timetuple())
|
|
56
|
+
new_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
|
|
48
57
|
responses.add(
|
|
49
58
|
responses.GET,
|
|
50
59
|
self.config_url,
|
|
51
|
-
headers={"ETag": new_etag},
|
|
60
|
+
headers={"ETag": new_etag, "Last-Modified": new_lastmodified},
|
|
52
61
|
json=self.test_config_json,
|
|
53
62
|
)
|
|
54
|
-
result, etag = self.test_client.get_config(
|
|
63
|
+
result, etag, lastmodified = self.test_client.get_config(
|
|
64
|
+
config_etag=self.test_etag, last_modified=self.test_lastmodified
|
|
65
|
+
)
|
|
55
66
|
self.assertDictEqual(result, self.test_config_json)
|
|
67
|
+
self.assertEqual(lastmodified, new_lastmodified)
|
|
56
68
|
self.assertEqual(etag, new_etag)
|
|
57
69
|
|
|
58
70
|
@responses.activate(registry=OrderedRegistry)
|
|
@@ -68,7 +80,9 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
68
80
|
headers={"ETag": self.test_etag},
|
|
69
81
|
json=self.test_config_json,
|
|
70
82
|
)
|
|
71
|
-
result, etag = self.test_client.get_config(
|
|
83
|
+
result, etag, lastmodified = self.test_client.get_config(
|
|
84
|
+
config_etag=self.test_etag
|
|
85
|
+
)
|
|
72
86
|
|
|
73
87
|
self.assertDictEqual(result, self.test_config_json)
|
|
74
88
|
self.assertEqual(etag, self.test_etag)
|
|
@@ -83,10 +97,12 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
83
97
|
responses.add(
|
|
84
98
|
responses.GET,
|
|
85
99
|
self.config_url,
|
|
86
|
-
headers={"ETag": self.test_etag},
|
|
100
|
+
headers={"ETag": self.test_etag, "Last-Modified": self.test_lastmodified},
|
|
87
101
|
json=self.test_config_json,
|
|
88
102
|
)
|
|
89
|
-
result, etag = self.test_client.get_config(
|
|
103
|
+
result, etag, last_modified = self.test_client.get_config(
|
|
104
|
+
config_etag=self.test_etag
|
|
105
|
+
)
|
|
90
106
|
self.assertDictEqual(result, self.test_config_json)
|
|
91
107
|
self.assertEqual(etag, self.test_etag)
|
|
92
108
|
|
|
@@ -131,6 +147,8 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
131
147
|
status=HTTPStatus.NOT_MODIFIED,
|
|
132
148
|
)
|
|
133
149
|
|
|
134
|
-
new_config, new_etag = self.test_client.get_config(
|
|
150
|
+
new_config, new_etag, last_modified = self.test_client.get_config(
|
|
151
|
+
config_etag=self.test_etag
|
|
152
|
+
)
|
|
135
153
|
self.assertIsNone(new_config)
|
|
136
154
|
self.assertEqual(new_etag, self.test_etag)
|
test/api/test_local_bucketing.py
CHANGED
|
@@ -5,10 +5,7 @@ import unittest
|
|
|
5
5
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing, WASMAbortError
|
|
6
6
|
from devcycle_python_sdk.models.bucketed_config import (
|
|
7
7
|
Environment,
|
|
8
|
-
EdgeDBSettings,
|
|
9
8
|
FeatureVariation,
|
|
10
|
-
OptInColors,
|
|
11
|
-
OptInSettings,
|
|
12
9
|
Project,
|
|
13
10
|
ProjectSettings,
|
|
14
11
|
)
|
|
@@ -197,27 +194,29 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
197
194
|
defaultValue=None,
|
|
198
195
|
evalReason=None,
|
|
199
196
|
),
|
|
197
|
+
"float-var": Variable(
|
|
198
|
+
_id="61200363125123fca69d3a7a",
|
|
199
|
+
key="float-var",
|
|
200
|
+
type="Number",
|
|
201
|
+
value=3.14159,
|
|
202
|
+
isDefaulted=None,
|
|
203
|
+
defaultValue=None,
|
|
204
|
+
evalReason=None,
|
|
205
|
+
),
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
self.assertIsNotNone(result)
|
|
203
209
|
self.assertEqual(result.user, user)
|
|
204
210
|
self.assertEqual(
|
|
205
|
-
result.project,
|
|
206
211
|
Project(
|
|
207
212
|
id="61f97628ff4afcb6d057dbf0",
|
|
208
213
|
key="emma-project",
|
|
209
214
|
a0_organization="org_tPyJN5dvNNirKar7",
|
|
210
215
|
settings=ProjectSettings(
|
|
211
|
-
edge_db=
|
|
212
|
-
opt_in=OptInSettings(
|
|
213
|
-
enabled=True,
|
|
214
|
-
title="EarlyAccess",
|
|
215
|
-
description="Getearlyaccesstobetafeaturesbelow!",
|
|
216
|
-
image_url="",
|
|
217
|
-
colors=OptInColors(primary="#531cd9", secondary="#16dec0"),
|
|
218
|
-
),
|
|
216
|
+
edge_db=None, opt_in=None, disable_passthrough_rollouts=False
|
|
219
217
|
),
|
|
220
218
|
),
|
|
219
|
+
result.project,
|
|
221
220
|
)
|
|
222
221
|
self.assertEqual(
|
|
223
222
|
result.environment,
|
|
@@ -250,6 +249,10 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
250
249
|
feature="62fbf6566f1ba302829f9e32",
|
|
251
250
|
variation="62fbf6566f1ba302829f9e39",
|
|
252
251
|
),
|
|
252
|
+
"float-var": FeatureVariation(
|
|
253
|
+
feature="62fbf6566f1ba302829f9e32",
|
|
254
|
+
variation="62fbf6566f1ba302829f9e39",
|
|
255
|
+
),
|
|
253
256
|
},
|
|
254
257
|
)
|
|
255
258
|
self.assertEqual(
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import json
|
|
2
|
+
import logging
|
|
3
3
|
import time
|
|
4
|
-
import uuid
|
|
5
4
|
import unittest
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from email.utils import formatdate
|
|
8
|
+
from time import mktime
|
|
6
9
|
from unittest.mock import patch, MagicMock
|
|
7
10
|
|
|
8
11
|
from devcycle_python_sdk import DevCycleLocalOptions
|
|
@@ -18,20 +21,27 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
18
21
|
self.test_local_bucketing = MagicMock()
|
|
19
22
|
self.test_options = DevCycleLocalOptions(config_polling_interval_ms=500)
|
|
20
23
|
|
|
24
|
+
now = datetime.now()
|
|
25
|
+
stamp = mktime(now.timetuple())
|
|
26
|
+
self.test_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
|
|
21
27
|
self.test_etag = str(uuid.uuid4())
|
|
22
28
|
self.test_config_json = small_config_json()
|
|
23
29
|
self.test_config_string = json.dumps(self.test_config_json)
|
|
24
30
|
|
|
25
31
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
26
32
|
def test_init(self, mock_get_config):
|
|
27
|
-
mock_get_config.return_value = (
|
|
33
|
+
mock_get_config.return_value = (
|
|
34
|
+
self.test_config_json,
|
|
35
|
+
self.test_etag,
|
|
36
|
+
self.test_lastmodified,
|
|
37
|
+
)
|
|
28
38
|
config_manager = EnvironmentConfigManager(
|
|
29
39
|
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
30
40
|
)
|
|
31
41
|
|
|
32
42
|
# sleep to allow polling thread to load the config
|
|
33
43
|
time.sleep(0.1)
|
|
34
|
-
mock_get_config.assert_called_once_with(config_etag=None)
|
|
44
|
+
mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
|
|
35
45
|
|
|
36
46
|
self.assertTrue(config_manager._polling_enabled)
|
|
37
47
|
self.assertTrue(config_manager.is_alive())
|
|
@@ -45,7 +55,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
45
55
|
|
|
46
56
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
47
57
|
def test_init_with_client_callback(self, mock_get_config):
|
|
48
|
-
mock_get_config.return_value = (
|
|
58
|
+
mock_get_config.return_value = (
|
|
59
|
+
self.test_config_json,
|
|
60
|
+
self.test_etag,
|
|
61
|
+
self.test_lastmodified,
|
|
62
|
+
)
|
|
49
63
|
|
|
50
64
|
mock_callback = MagicMock()
|
|
51
65
|
|
|
@@ -55,7 +69,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
55
69
|
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
56
70
|
)
|
|
57
71
|
time.sleep(0.1)
|
|
58
|
-
mock_get_config.assert_called_once_with(config_etag=None)
|
|
72
|
+
mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
|
|
59
73
|
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
60
74
|
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
61
75
|
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
@@ -66,7 +80,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
66
80
|
|
|
67
81
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
68
82
|
def test_init_with_client_callback_with_error(self, mock_get_config):
|
|
69
|
-
mock_get_config.return_value = (
|
|
83
|
+
mock_get_config.return_value = (
|
|
84
|
+
self.test_config_json,
|
|
85
|
+
self.test_etag,
|
|
86
|
+
self.test_lastmodified,
|
|
87
|
+
)
|
|
70
88
|
mock_callback = MagicMock()
|
|
71
89
|
mock_callback.side_effect = Exception(
|
|
72
90
|
"Badly written callback generates an exception"
|
|
@@ -79,7 +97,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
79
97
|
)
|
|
80
98
|
# the callback error should not negatively impact initialization of the config manager
|
|
81
99
|
time.sleep(0.1)
|
|
82
|
-
mock_get_config.assert_called_once_with(config_etag=None)
|
|
100
|
+
mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
|
|
83
101
|
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
84
102
|
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
85
103
|
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
@@ -105,7 +123,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
105
123
|
|
|
106
124
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
107
125
|
def test_get_config_unchanged(self, mock_get_config):
|
|
108
|
-
mock_get_config.return_value = (
|
|
126
|
+
mock_get_config.return_value = (
|
|
127
|
+
self.test_config_json,
|
|
128
|
+
self.test_etag,
|
|
129
|
+
self.test_lastmodified,
|
|
130
|
+
)
|
|
109
131
|
|
|
110
132
|
self.test_options.config_polling_interval_ms = 200
|
|
111
133
|
config_manager = EnvironmentConfigManager(
|
|
@@ -117,7 +139,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
117
139
|
config_manager.close()
|
|
118
140
|
|
|
119
141
|
self.test_local_bucketing.store_config.reset_mock()
|
|
120
|
-
mock_get_config.return_value = (None, config_manager._config_etag)
|
|
142
|
+
mock_get_config.return_value = (None, config_manager._config_etag, None)
|
|
121
143
|
|
|
122
144
|
# trigger refresh of the config directly
|
|
123
145
|
config_manager._get_config()
|
test/models/test_user.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from devcycle_python_sdk.models.user import DevCycleUser
|
|
5
|
+
|
|
6
|
+
from openfeature.provider import EvaluationContext
|
|
7
|
+
from openfeature.exception import TargetingKeyMissingError, InvalidContextError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DevCycleUserTest(unittest.TestCase):
|
|
14
|
+
def test_create_user_from_context_no_context(self):
|
|
15
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
16
|
+
DevCycleUser.create_user_from_context(None)
|
|
17
|
+
|
|
18
|
+
def test_create_user_from_context_no_user_id(self):
|
|
19
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
20
|
+
DevCycleUser.create_user_from_context(
|
|
21
|
+
EvaluationContext(targeting_key=None, attributes={})
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
25
|
+
DevCycleUser.create_user_from_context(
|
|
26
|
+
EvaluationContext(targeting_key=None, attributes=None)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
30
|
+
DevCycleUser.create_user_from_context(
|
|
31
|
+
EvaluationContext(targeting_key=None, attributes={"user_id": None})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def test_create_user_from_context_only_user_id(self):
|
|
35
|
+
test_user_id = "12345"
|
|
36
|
+
context = EvaluationContext(targeting_key=test_user_id, attributes=None)
|
|
37
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
38
|
+
self.assertIsNotNone(user)
|
|
39
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
40
|
+
|
|
41
|
+
context = EvaluationContext(
|
|
42
|
+
targeting_key=None, attributes={"user_id": test_user_id}
|
|
43
|
+
)
|
|
44
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
45
|
+
self.assertIsNotNone(user)
|
|
46
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
47
|
+
|
|
48
|
+
# ensure that targeting_key takes precedence over user_id
|
|
49
|
+
context = EvaluationContext(
|
|
50
|
+
targeting_key=test_user_id, attributes={"user_id": "99999"}
|
|
51
|
+
)
|
|
52
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
53
|
+
self.assertIsNotNone(user)
|
|
54
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
55
|
+
|
|
56
|
+
def test_create_user_from_context_with_attributes(self):
|
|
57
|
+
test_user_id = "12345"
|
|
58
|
+
context = EvaluationContext(
|
|
59
|
+
targeting_key=test_user_id,
|
|
60
|
+
attributes={
|
|
61
|
+
"user_id": "1234",
|
|
62
|
+
"email": "someone@example.com",
|
|
63
|
+
"name": "John Doe",
|
|
64
|
+
"language": "en",
|
|
65
|
+
"country": "US",
|
|
66
|
+
"appVersion": "1.0.0",
|
|
67
|
+
"appBuild": "1",
|
|
68
|
+
"deviceModel": "iPhone X21",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
72
|
+
self.assertIsNotNone(user)
|
|
73
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
74
|
+
self.assertEqual(user.email, context.attributes["email"])
|
|
75
|
+
self.assertEqual(user.name, context.attributes["name"])
|
|
76
|
+
self.assertEqual(user.language, context.attributes["language"])
|
|
77
|
+
self.assertEqual(user.country, context.attributes["country"])
|
|
78
|
+
self.assertEqual(user.appVersion, context.attributes["appVersion"])
|
|
79
|
+
self.assertEqual(user.appBuild, context.attributes["appBuild"])
|
|
80
|
+
self.assertEqual(user.deviceModel, context.attributes["deviceModel"])
|
|
81
|
+
|
|
82
|
+
def test_create_user_from_context_with_custom_data(self):
|
|
83
|
+
test_user_id = "12345"
|
|
84
|
+
context = EvaluationContext(
|
|
85
|
+
targeting_key=test_user_id,
|
|
86
|
+
attributes={
|
|
87
|
+
"customData": {
|
|
88
|
+
"strValue": "hello",
|
|
89
|
+
"intValue": 123,
|
|
90
|
+
"floatValue": 3.1456,
|
|
91
|
+
"boolValue": True,
|
|
92
|
+
},
|
|
93
|
+
"privateCustomData": {
|
|
94
|
+
"strValue": "world",
|
|
95
|
+
"intValue": 789,
|
|
96
|
+
"floatValue": 0.0001,
|
|
97
|
+
"BoolValue": False,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
102
|
+
self.assertIsNotNone(user)
|
|
103
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
104
|
+
|
|
105
|
+
self.assertEqual(
|
|
106
|
+
user.customData,
|
|
107
|
+
{
|
|
108
|
+
"strValue": "hello",
|
|
109
|
+
"intValue": 123,
|
|
110
|
+
"floatValue": 3.1456,
|
|
111
|
+
"boolValue": True,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
self.assertEqual(
|
|
115
|
+
user.privateCustomData,
|
|
116
|
+
{
|
|
117
|
+
"strValue": "world",
|
|
118
|
+
"intValue": 789,
|
|
119
|
+
"floatValue": 0.0001,
|
|
120
|
+
"BoolValue": False,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def test_create_user_from_context_with_unknown_data_fields(self):
|
|
125
|
+
test_user_id = "12345"
|
|
126
|
+
context = EvaluationContext(
|
|
127
|
+
targeting_key=test_user_id,
|
|
128
|
+
attributes={
|
|
129
|
+
"strValue": "hello",
|
|
130
|
+
"intValue": 123,
|
|
131
|
+
"floatValue": 3.1456,
|
|
132
|
+
"boolValue": True,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
136
|
+
self.assertIsNotNone(user)
|
|
137
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
138
|
+
|
|
139
|
+
# the fields should get reassigned to custom_data
|
|
140
|
+
self.assertEqual(
|
|
141
|
+
user.customData,
|
|
142
|
+
{
|
|
143
|
+
"strValue": "hello",
|
|
144
|
+
"intValue": 123,
|
|
145
|
+
"floatValue": 3.1456,
|
|
146
|
+
"boolValue": True,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
self.assertEqual(user.privateCustomData, None)
|
|
150
|
+
|
|
151
|
+
def test_set_custom_value(self):
|
|
152
|
+
custom_data = {}
|
|
153
|
+
with self.assertRaises(InvalidContextError):
|
|
154
|
+
DevCycleUser._set_custom_value(custom_data, None, None)
|
|
155
|
+
|
|
156
|
+
custom_data = {}
|
|
157
|
+
DevCycleUser._set_custom_value(custom_data, "key1", None)
|
|
158
|
+
self.assertDictEqual(custom_data, {"key1": None})
|
|
159
|
+
|
|
160
|
+
custom_data = {}
|
|
161
|
+
DevCycleUser._set_custom_value(custom_data, "key1", "value1")
|
|
162
|
+
self.assertDictEqual(custom_data, {"key1": "value1"})
|
|
163
|
+
|
|
164
|
+
custom_data = {}
|
|
165
|
+
DevCycleUser._set_custom_value(custom_data, "key1", 1)
|
|
166
|
+
self.assertDictEqual(custom_data, {"key1": 1})
|
|
167
|
+
|
|
168
|
+
custom_data = {}
|
|
169
|
+
DevCycleUser._set_custom_value(custom_data, "key1", 3.1456)
|
|
170
|
+
self.assertDictEqual(custom_data, {"key1": 3.1456})
|
|
171
|
+
|
|
172
|
+
custom_data = {}
|
|
173
|
+
DevCycleUser._set_custom_value(custom_data, "key1", True)
|
|
174
|
+
self.assertDictEqual(custom_data, {"key1": True})
|
|
175
|
+
|
|
176
|
+
custom_data = {}
|
|
177
|
+
with self.assertRaises(InvalidContextError):
|
|
178
|
+
DevCycleUser._set_custom_value(custom_data, "map_data", {"hello": "world"})
|
|
179
|
+
|
|
180
|
+
custom_data = {}
|
|
181
|
+
with self.assertRaises(InvalidContextError):
|
|
182
|
+
DevCycleUser._set_custom_value(
|
|
183
|
+
custom_data, "list_data", ["one", "two", "three"]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
unittest.main()
|
|
File without changes
|