devcycle-python-server-sdk 2.0.1__py3-none-any.whl → 3.0.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 +3 -3
- devcycle_python_sdk/api/backoff.py +11 -0
- devcycle_python_sdk/api/bucketing_client.py +17 -25
- devcycle_python_sdk/api/config_client.py +116 -0
- devcycle_python_sdk/api/event_client.py +95 -0
- devcycle_python_sdk/api/local_bucketing.py +429 -2
- devcycle_python_sdk/{dvc_cloud_client.py → cloud_client.py} +69 -41
- devcycle_python_sdk/exceptions.py +25 -3
- devcycle_python_sdk/local_client.py +220 -0
- devcycle_python_sdk/managers/config_manager.py +83 -15
- devcycle_python_sdk/managers/event_queue_manager.py +213 -8
- devcycle_python_sdk/models/bucketed_config.py +150 -0
- devcycle_python_sdk/models/event.py +111 -8
- devcycle_python_sdk/models/feature.py +12 -0
- devcycle_python_sdk/models/platform_data.py +33 -0
- devcycle_python_sdk/models/user.py +52 -7
- devcycle_python_sdk/models/variable.py +17 -5
- devcycle_python_sdk/options.py +106 -0
- devcycle_python_sdk/protobuf/__init__.py +0 -0
- devcycle_python_sdk/protobuf/utils.py +129 -0
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +46 -0
- devcycle_python_sdk/util/version.py +1 -1
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/METADATA +3 -1
- devcycle_python_server_sdk-3.0.0.dist-info/RECORD +55 -0
- example/{example.py → cloud_client_example.py} +24 -20
- example/local_bucketing_client_example.py +74 -0
- test/api/test_bucketing_client.py +10 -5
- test/api/test_config_client.py +133 -0
- test/api/test_event_client.py +97 -0
- test/api/test_local_bucketing.py +379 -4
- test/fixture/__init__.py +0 -0
- test/fixture/data.py +57 -0
- test/managers/test_config_manager.py +120 -5
- test/managers/test_event_queue_manager.py +259 -5
- test/models/__init__.py +0 -0
- test/models/test_bucketed_config.py +26 -0
- test/{test_dvc_cloud_client.py → test_cloud_client.py} +11 -17
- test/test_local_client.py +397 -0
- test/util/test_utils.py +183 -0
- test/util/test_version.py +2 -2
- devcycle_python_sdk/dvc_local_client.py +0 -152
- devcycle_python_sdk/dvc_options.py +0 -45
- devcycle_python_server_sdk-2.0.1.dist-info/RECORD +0 -41
- test/test_bucketing_client.py +0 -228
- test/test_dvc_local_client.py +0 -143
- test/test_version.py +0 -18
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/LICENSE +0 -0
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/WHEEL +0 -0
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/top_level.txt +0 -0
test/api/test_local_bucketing.py
CHANGED
|
@@ -1,17 +1,392 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import logging
|
|
2
|
-
|
|
3
3
|
import unittest
|
|
4
4
|
|
|
5
|
-
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
5
|
+
from devcycle_python_sdk.api.local_bucketing import LocalBucketing, WASMAbortError
|
|
6
|
+
from devcycle_python_sdk.models.bucketed_config import (
|
|
7
|
+
Environment,
|
|
8
|
+
EdgeDBSettings,
|
|
9
|
+
FeatureVariation,
|
|
10
|
+
OptInColors,
|
|
11
|
+
OptInSettings,
|
|
12
|
+
Project,
|
|
13
|
+
ProjectSettings,
|
|
14
|
+
)
|
|
15
|
+
from devcycle_python_sdk.models.feature import Feature
|
|
16
|
+
from devcycle_python_sdk.models.variable import Variable
|
|
17
|
+
from devcycle_python_sdk.models.platform_data import default_platform_data
|
|
18
|
+
from devcycle_python_sdk.models.user import DevCycleUser
|
|
19
|
+
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
|
|
20
|
+
from devcycle_python_sdk.models.variable import TypeEnum
|
|
21
|
+
from test.fixture.data import small_config, large_config, special_character_config
|
|
6
22
|
|
|
7
23
|
logger = logging.getLogger(__name__)
|
|
8
24
|
|
|
9
25
|
|
|
10
26
|
class LocalBucketingTest(unittest.TestCase):
|
|
27
|
+
def setUp(self) -> None:
|
|
28
|
+
self.test_sdk_key = "dvc_server_testkey"
|
|
29
|
+
self.local_bucketing = LocalBucketing(self.test_sdk_key)
|
|
30
|
+
|
|
11
31
|
def test_init(self):
|
|
12
|
-
local_bucketing = LocalBucketing()
|
|
32
|
+
local_bucketing = LocalBucketing(self.test_sdk_key)
|
|
13
33
|
self.assertIsNotNone(local_bucketing)
|
|
14
34
|
|
|
35
|
+
def test_write_read_string(self) -> None:
|
|
36
|
+
# Test that we can safely pass ASCII strings to the WASM module and read them back
|
|
37
|
+
test_string = "test_bytes"
|
|
38
|
+
arg_pointer = self.local_bucketing._new_assembly_script_string(test_string)
|
|
39
|
+
|
|
40
|
+
echo_func = self.local_bucketing._get_export("echoString")
|
|
41
|
+
|
|
42
|
+
result_pointer = echo_func(self.local_bucketing.wasm_store, arg_pointer)
|
|
43
|
+
|
|
44
|
+
result_bytes = self.local_bucketing._read_assembly_script_string(result_pointer)
|
|
45
|
+
self.assertEqual(result_bytes, test_string)
|
|
46
|
+
|
|
47
|
+
def test_write_read_bytes_array(self) -> None:
|
|
48
|
+
# Test that we can safely pass byte arrays to the WASM module and read them back
|
|
49
|
+
test_bytes = "test_bytes".encode("utf-8")
|
|
50
|
+
arg_pointer = self.local_bucketing._new_assembly_script_byte_array(test_bytes)
|
|
51
|
+
|
|
52
|
+
echo_func = self.local_bucketing._get_export("echoUint8Array")
|
|
53
|
+
|
|
54
|
+
result_pointer = echo_func(self.local_bucketing.wasm_store, arg_pointer)
|
|
55
|
+
|
|
56
|
+
result_bytes = self.local_bucketing._read_assembly_script_byte_array(
|
|
57
|
+
result_pointer
|
|
58
|
+
)
|
|
59
|
+
self.assertEqual(result_bytes, test_bytes)
|
|
60
|
+
|
|
61
|
+
def test_abort(self) -> None:
|
|
62
|
+
abort_func = self.local_bucketing._get_export("triggerAbort")
|
|
63
|
+
|
|
64
|
+
with self.assertRaises(WASMAbortError) as context:
|
|
65
|
+
abort_func(self.local_bucketing.wasm_store)
|
|
66
|
+
|
|
67
|
+
self.assertRegex(
|
|
68
|
+
context.exception.args[0],
|
|
69
|
+
r"Abort in '[^']+':[0-9]+:[0-9]+ -- 'Manual abort triggered'",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def test_store_config(self) -> None:
|
|
73
|
+
# should store each config with any errors
|
|
74
|
+
self.local_bucketing.store_config(small_config())
|
|
75
|
+
self.local_bucketing.store_config(large_config())
|
|
76
|
+
self.local_bucketing.store_config(special_character_config())
|
|
77
|
+
|
|
78
|
+
def test_set_platform_data(self):
|
|
79
|
+
# should set the data without any errors
|
|
80
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
81
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
82
|
+
|
|
83
|
+
def test_set_client_custom_data(self):
|
|
84
|
+
# should set the data without any errors
|
|
85
|
+
client_custom_data = {
|
|
86
|
+
"strProp": "strVal",
|
|
87
|
+
"intProp": 1,
|
|
88
|
+
"floatProp": 1.1,
|
|
89
|
+
"boolProp": True,
|
|
90
|
+
"nullProp": None,
|
|
91
|
+
}
|
|
92
|
+
data_str = json.dumps(client_custom_data)
|
|
93
|
+
self.local_bucketing.set_client_custom_data(data_str)
|
|
94
|
+
|
|
95
|
+
def test_get_variable_for_user_protobuf(self):
|
|
96
|
+
self.local_bucketing.store_config(small_config())
|
|
97
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
98
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
99
|
+
self.local_bucketing.init_event_queue("{}")
|
|
100
|
+
user = DevCycleUser(user_id="test_user_id")
|
|
101
|
+
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
102
|
+
user=user, key="string-var", default_value="default value"
|
|
103
|
+
)
|
|
104
|
+
self.assertIsNotNone(result)
|
|
105
|
+
self.assertEqual(result.key, "string-var")
|
|
106
|
+
self.assertEqual(result.type, TypeEnum.STRING)
|
|
107
|
+
self.assertEqual(result.value, "variationOn")
|
|
108
|
+
self.assertFalse(result.isDefaulted)
|
|
109
|
+
|
|
110
|
+
def test_get_variable_for_user_protobuf_special_characters(self):
|
|
111
|
+
self.local_bucketing.store_config(special_character_config())
|
|
112
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
113
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
114
|
+
self.local_bucketing.init_event_queue("{}")
|
|
115
|
+
user = DevCycleUser(user_id="test_user_id")
|
|
116
|
+
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
117
|
+
user=user, key="string-var", default_value="default value"
|
|
118
|
+
)
|
|
119
|
+
self.assertIsNotNone(result)
|
|
120
|
+
self.assertEqual(result.key, "string-var")
|
|
121
|
+
self.assertEqual(result.type, TypeEnum.STRING)
|
|
122
|
+
self.assertEqual(result.value, "öé 🐍 ¥ variationOn")
|
|
123
|
+
self.assertFalse(result.isDefaulted)
|
|
124
|
+
|
|
125
|
+
def test_get_variable_for_user_protobuf_type_mismatch(self):
|
|
126
|
+
self.local_bucketing.store_config(small_config())
|
|
127
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
128
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
129
|
+
self.local_bucketing.init_event_queue("{}")
|
|
130
|
+
user = DevCycleUser(user_id="test_user_id")
|
|
131
|
+
|
|
132
|
+
# type mismatch is handled inside the WASM and will return
|
|
133
|
+
# no data if the type is not correct
|
|
134
|
+
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
135
|
+
user=user, key="string-var", default_value=9999
|
|
136
|
+
)
|
|
137
|
+
self.assertIsNone(result)
|
|
138
|
+
|
|
139
|
+
def test_generate_bucketed_config(self):
|
|
140
|
+
self.local_bucketing.store_config(small_config())
|
|
141
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
142
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
143
|
+
self.local_bucketing.init_event_queue("{}")
|
|
144
|
+
user = DevCycleUser(user_id="test_user_id")
|
|
145
|
+
|
|
146
|
+
result = self.local_bucketing.generate_bucketed_config(user=user)
|
|
147
|
+
|
|
148
|
+
expected_features = {
|
|
149
|
+
"a-cool-new-feature": Feature(
|
|
150
|
+
_id="62fbf6566f1ba302829f9e32",
|
|
151
|
+
key="a-cool-new-feature",
|
|
152
|
+
type="release",
|
|
153
|
+
_variation="62fbf6566f1ba302829f9e39",
|
|
154
|
+
variationName="VariationOn",
|
|
155
|
+
variationKey="variation-on",
|
|
156
|
+
evalReason=None,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
expected_variables = {
|
|
160
|
+
"a-cool-new-feature": Variable(
|
|
161
|
+
_id="62fbf6566f1ba302829f9e34",
|
|
162
|
+
key="a-cool-new-feature",
|
|
163
|
+
type="Boolean",
|
|
164
|
+
value=True,
|
|
165
|
+
isDefaulted=None,
|
|
166
|
+
defaultValue=None,
|
|
167
|
+
evalReason=None,
|
|
168
|
+
),
|
|
169
|
+
"string-var": Variable(
|
|
170
|
+
_id="63125320a4719939fd57cb2b",
|
|
171
|
+
key="string-var",
|
|
172
|
+
type="String",
|
|
173
|
+
value="variationOn",
|
|
174
|
+
isDefaulted=None,
|
|
175
|
+
defaultValue=None,
|
|
176
|
+
evalReason=None,
|
|
177
|
+
),
|
|
178
|
+
"json-var": Variable(
|
|
179
|
+
_id="64372363125123fca69d3f7b",
|
|
180
|
+
key="json-var",
|
|
181
|
+
type="JSON",
|
|
182
|
+
value={
|
|
183
|
+
"displayText": "This variation is on",
|
|
184
|
+
"showDialog": True,
|
|
185
|
+
"maxUsers": 100,
|
|
186
|
+
},
|
|
187
|
+
isDefaulted=None,
|
|
188
|
+
defaultValue=None,
|
|
189
|
+
evalReason=None,
|
|
190
|
+
),
|
|
191
|
+
"num-var": Variable(
|
|
192
|
+
_id="65272363125123fca69d3a7d",
|
|
193
|
+
key="num-var",
|
|
194
|
+
type="Number",
|
|
195
|
+
value=12345,
|
|
196
|
+
isDefaulted=None,
|
|
197
|
+
defaultValue=None,
|
|
198
|
+
evalReason=None,
|
|
199
|
+
),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
self.assertIsNotNone(result)
|
|
203
|
+
self.assertEqual(result.user, user)
|
|
204
|
+
self.assertEqual(
|
|
205
|
+
result.project,
|
|
206
|
+
Project(
|
|
207
|
+
id="61f97628ff4afcb6d057dbf0",
|
|
208
|
+
key="emma-project",
|
|
209
|
+
a0_organization="org_tPyJN5dvNNirKar7",
|
|
210
|
+
settings=ProjectSettings(
|
|
211
|
+
edge_db=EdgeDBSettings(enabled=False),
|
|
212
|
+
opt_in=OptInSettings(
|
|
213
|
+
enabled=True,
|
|
214
|
+
title="EarlyAccess",
|
|
215
|
+
description="Getearlyaccesstobetafeaturesbelow!",
|
|
216
|
+
image_url="",
|
|
217
|
+
colors=OptInColors(primary="#531cd9", secondary="#16dec0"),
|
|
218
|
+
),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
)
|
|
222
|
+
self.assertEqual(
|
|
223
|
+
result.environment,
|
|
224
|
+
Environment(id="61f97628ff4afcb6d057dbf2", key="development"),
|
|
225
|
+
)
|
|
226
|
+
self.assertEqual(
|
|
227
|
+
result.features,
|
|
228
|
+
expected_features,
|
|
229
|
+
)
|
|
230
|
+
self.assertEqual(
|
|
231
|
+
result.feature_variation_map,
|
|
232
|
+
{"62fbf6566f1ba302829f9e32": "62fbf6566f1ba302829f9e39"},
|
|
233
|
+
)
|
|
234
|
+
self.assertEqual(
|
|
235
|
+
result.variable_variation_map,
|
|
236
|
+
{
|
|
237
|
+
"a-cool-new-feature": FeatureVariation(
|
|
238
|
+
feature="62fbf6566f1ba302829f9e32",
|
|
239
|
+
variation="62fbf6566f1ba302829f9e39",
|
|
240
|
+
),
|
|
241
|
+
"string-var": FeatureVariation(
|
|
242
|
+
feature="62fbf6566f1ba302829f9e32",
|
|
243
|
+
variation="62fbf6566f1ba302829f9e39",
|
|
244
|
+
),
|
|
245
|
+
"json-var": FeatureVariation(
|
|
246
|
+
feature="62fbf6566f1ba302829f9e32",
|
|
247
|
+
variation="62fbf6566f1ba302829f9e39",
|
|
248
|
+
),
|
|
249
|
+
"num-var": FeatureVariation(
|
|
250
|
+
feature="62fbf6566f1ba302829f9e32",
|
|
251
|
+
variation="62fbf6566f1ba302829f9e39",
|
|
252
|
+
),
|
|
253
|
+
},
|
|
254
|
+
)
|
|
255
|
+
self.assertEqual(
|
|
256
|
+
result.variables,
|
|
257
|
+
expected_variables,
|
|
258
|
+
)
|
|
259
|
+
self.assertEqual(result.known_variable_keys, [])
|
|
260
|
+
|
|
261
|
+
def test_get_event_queue_size(self):
|
|
262
|
+
self.local_bucketing.store_config(small_config())
|
|
263
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
264
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
265
|
+
self.local_bucketing.init_event_queue("{}")
|
|
266
|
+
|
|
267
|
+
result = self.local_bucketing.get_event_queue_size()
|
|
268
|
+
self.assertEqual(result, 0)
|
|
269
|
+
|
|
270
|
+
def test_flush_event_queue_empty(self):
|
|
271
|
+
self.local_bucketing.store_config(small_config())
|
|
272
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
273
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
274
|
+
|
|
275
|
+
self.local_bucketing.init_event_queue(
|
|
276
|
+
json.dumps(
|
|
277
|
+
{
|
|
278
|
+
"disableAutomaticEventLogging": False,
|
|
279
|
+
"disableCustomEventLogging": False,
|
|
280
|
+
"minEventsPerFlush": 1,
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
results = self.local_bucketing.flush_event_queue()
|
|
285
|
+
self.assertIsNotNone(results)
|
|
286
|
+
self.assertListEqual(results, [])
|
|
287
|
+
|
|
288
|
+
def test_flush_event_queue(self):
|
|
289
|
+
self.local_bucketing.store_config(small_config())
|
|
290
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
291
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
292
|
+
|
|
293
|
+
self.local_bucketing.init_event_queue(
|
|
294
|
+
json.dumps(
|
|
295
|
+
{
|
|
296
|
+
"disableAutomaticEventLogging": False,
|
|
297
|
+
"disableCustomEventLogging": False,
|
|
298
|
+
"minEventsPerFlush": 1,
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# trigger two events for a single user
|
|
304
|
+
user = DevCycleUser(user_id="test_user_id")
|
|
305
|
+
self.local_bucketing.get_variable_for_user_protobuf(
|
|
306
|
+
user=user, key="string-var", default_value="default value"
|
|
307
|
+
)
|
|
308
|
+
self.local_bucketing.get_variable_for_user_protobuf(
|
|
309
|
+
user=user, key="bool-var", default_value=False
|
|
310
|
+
)
|
|
311
|
+
results = self.local_bucketing.flush_event_queue()
|
|
312
|
+
self.assertIsNotNone(results)
|
|
313
|
+
self.assertEqual(len(results), 1)
|
|
314
|
+
self.assertEqual(results[0].eventCount, 2)
|
|
315
|
+
self.assertIsNotNone(results[0].payloadId)
|
|
316
|
+
self.assertTrue(len(results[0].payloadId) > 0)
|
|
317
|
+
self.assertEqual(len(results[0].records), 1)
|
|
318
|
+
self.assertEqual(len(results[0].records[0].events), 2)
|
|
319
|
+
|
|
320
|
+
def test_on_event_payload_failure_unknown_payload_id(self):
|
|
321
|
+
self.local_bucketing.store_config(small_config())
|
|
322
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
323
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
324
|
+
|
|
325
|
+
self.local_bucketing.init_event_queue(
|
|
326
|
+
json.dumps(
|
|
327
|
+
{
|
|
328
|
+
"disableAutomaticEventLogging": False,
|
|
329
|
+
"disableCustomEventLogging": False,
|
|
330
|
+
"minEventsPerFlush": 1,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
with self.assertRaises(WASMAbortError):
|
|
336
|
+
self.local_bucketing.on_event_payload_failure("test_payload_id", True)
|
|
337
|
+
|
|
338
|
+
def test_on_event_payload_success_unknown_payload_id(self):
|
|
339
|
+
self.local_bucketing.store_config(small_config())
|
|
340
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
341
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
342
|
+
|
|
343
|
+
self.local_bucketing.init_event_queue(
|
|
344
|
+
json.dumps(
|
|
345
|
+
{
|
|
346
|
+
"disableAutomaticEventLogging": False,
|
|
347
|
+
"disableCustomEventLogging": False,
|
|
348
|
+
"minEventsPerFlush": 1,
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
with self.assertRaises(WASMAbortError):
|
|
354
|
+
self.local_bucketing.on_event_payload_success("test_payload_id")
|
|
355
|
+
|
|
356
|
+
def test_queue_event(self):
|
|
357
|
+
self.local_bucketing.store_config(small_config())
|
|
358
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
359
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
360
|
+
self.local_bucketing.init_event_queue("{}")
|
|
361
|
+
user = DevCycleUser(user_id="test_user_id")
|
|
362
|
+
event = DevCycleEvent(
|
|
363
|
+
type=EventType.CustomEvent,
|
|
364
|
+
target="string-var",
|
|
365
|
+
value=1,
|
|
366
|
+
metaData={"test": "test"},
|
|
367
|
+
)
|
|
368
|
+
user_json = json.dumps(user.to_json())
|
|
369
|
+
event_json = json.dumps(event.to_json())
|
|
370
|
+
self.local_bucketing.queue_event(user_json, event_json)
|
|
371
|
+
|
|
372
|
+
def test_queue_aggregate_event(self):
|
|
373
|
+
self.local_bucketing.store_config(small_config())
|
|
374
|
+
platform_json = json.dumps(default_platform_data().to_json())
|
|
375
|
+
self.local_bucketing.set_platform_data(platform_json)
|
|
376
|
+
self.local_bucketing.init_event_queue("{}")
|
|
377
|
+
|
|
378
|
+
event = DevCycleEvent(
|
|
379
|
+
type=EventType.AggVariableDefaulted,
|
|
380
|
+
target="string-var",
|
|
381
|
+
value=1,
|
|
382
|
+
metaData={"test": "test"},
|
|
383
|
+
)
|
|
384
|
+
event_json = json.dumps(event.to_json())
|
|
385
|
+
variable_variation_map_json = "{}"
|
|
386
|
+
self.local_bucketing.queue_aggregate_event(
|
|
387
|
+
event_json, variable_variation_map_json
|
|
388
|
+
)
|
|
389
|
+
|
|
15
390
|
|
|
16
|
-
if __name__ ==
|
|
391
|
+
if __name__ == "__main__":
|
|
17
392
|
unittest.main()
|
test/fixture/__init__.py
ADDED
|
File without changes
|
test/fixture/data.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def small_config() -> str:
|
|
6
|
+
config_filename = os.path.join(
|
|
7
|
+
os.path.dirname(__file__), "fixture_small_config.json"
|
|
8
|
+
)
|
|
9
|
+
with open(config_filename, "r") as f:
|
|
10
|
+
return f.read()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def small_config_json() -> dict:
|
|
14
|
+
data = small_config()
|
|
15
|
+
return json.loads(data)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def special_character_config() -> str:
|
|
19
|
+
config_filename = os.path.join(
|
|
20
|
+
os.path.dirname(__file__), "fixture_small_config_special_characters.json"
|
|
21
|
+
)
|
|
22
|
+
with open(config_filename, "r") as f:
|
|
23
|
+
return f.read()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def special_character_config_json() -> dict:
|
|
27
|
+
data = special_character_config()
|
|
28
|
+
return json.loads(data)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def large_config() -> str:
|
|
32
|
+
config_filename = os.path.join(
|
|
33
|
+
os.path.dirname(__file__), "fixture_large_config.json"
|
|
34
|
+
)
|
|
35
|
+
with open(config_filename, "r") as f:
|
|
36
|
+
return f.read()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def large_config_json() -> dict:
|
|
40
|
+
data = large_config()
|
|
41
|
+
return json.loads(data)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def bucketed_config() -> str:
|
|
45
|
+
config_filename = os.path.join(
|
|
46
|
+
os.path.dirname(__file__), "fixture_bucketed_config.json"
|
|
47
|
+
)
|
|
48
|
+
with open(config_filename, "r") as f:
|
|
49
|
+
return f.read()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def bucketed_config_minimal() -> str:
|
|
53
|
+
config_filename = os.path.join(
|
|
54
|
+
os.path.dirname(__file__), "fixture_bucketed_config_minimal.json"
|
|
55
|
+
)
|
|
56
|
+
with open(config_filename, "r") as f:
|
|
57
|
+
return f.read()
|
|
@@ -1,17 +1,132 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
3
5
|
import unittest
|
|
6
|
+
from unittest.mock import patch, MagicMock
|
|
4
7
|
|
|
8
|
+
from devcycle_python_sdk import DevCycleLocalOptions
|
|
5
9
|
from devcycle_python_sdk.managers.config_manager import EnvironmentConfigManager
|
|
10
|
+
from test.fixture.data import small_config_json
|
|
6
11
|
|
|
7
12
|
logger = logging.getLogger(__name__)
|
|
8
13
|
|
|
9
14
|
|
|
10
15
|
class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
11
|
-
def
|
|
12
|
-
|
|
13
|
-
self.
|
|
16
|
+
def setUp(self) -> None:
|
|
17
|
+
self.sdk_key = "dvc_server_" + str(uuid.uuid4())
|
|
18
|
+
self.test_local_bucketing = MagicMock()
|
|
19
|
+
self.test_options = DevCycleLocalOptions(config_polling_interval_ms=500)
|
|
20
|
+
|
|
21
|
+
self.test_etag = str(uuid.uuid4())
|
|
22
|
+
self.test_config_json = small_config_json()
|
|
23
|
+
self.test_config_string = json.dumps(self.test_config_json)
|
|
24
|
+
|
|
25
|
+
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
26
|
+
def test_init(self, mock_get_config):
|
|
27
|
+
mock_get_config.return_value = (self.test_config_json, self.test_etag)
|
|
28
|
+
config_manager = EnvironmentConfigManager(
|
|
29
|
+
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# sleep to allow polling thread to load the config
|
|
33
|
+
time.sleep(0.1)
|
|
34
|
+
mock_get_config.assert_called_once_with(config_etag=None)
|
|
35
|
+
|
|
36
|
+
self.assertTrue(config_manager._polling_enabled)
|
|
37
|
+
self.assertTrue(config_manager.is_alive())
|
|
38
|
+
self.assertTrue(config_manager.daemon)
|
|
39
|
+
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
40
|
+
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
41
|
+
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
42
|
+
self.test_config_string
|
|
43
|
+
)
|
|
44
|
+
self.assertTrue(config_manager.is_initialized())
|
|
45
|
+
|
|
46
|
+
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
47
|
+
def test_init_with_client_callback(self, mock_get_config):
|
|
48
|
+
mock_get_config.return_value = (self.test_config_json, self.test_etag)
|
|
49
|
+
|
|
50
|
+
mock_callback = MagicMock()
|
|
51
|
+
|
|
52
|
+
self.test_options.on_client_initialized = mock_callback
|
|
53
|
+
|
|
54
|
+
config_manager = EnvironmentConfigManager(
|
|
55
|
+
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
56
|
+
)
|
|
57
|
+
time.sleep(0.1)
|
|
58
|
+
mock_get_config.assert_called_once_with(config_etag=None)
|
|
59
|
+
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
60
|
+
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
61
|
+
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
62
|
+
self.test_config_string
|
|
63
|
+
)
|
|
64
|
+
self.assertTrue(config_manager.is_initialized())
|
|
65
|
+
mock_callback.assert_called_once()
|
|
66
|
+
|
|
67
|
+
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
68
|
+
def test_init_with_client_callback_with_error(self, mock_get_config):
|
|
69
|
+
mock_get_config.return_value = (self.test_config_json, self.test_etag)
|
|
70
|
+
mock_callback = MagicMock()
|
|
71
|
+
mock_callback.side_effect = Exception(
|
|
72
|
+
"Badly written callback generates an exception"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
self.test_options.on_client_initialized = mock_callback
|
|
76
|
+
|
|
77
|
+
config_manager = EnvironmentConfigManager(
|
|
78
|
+
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
79
|
+
)
|
|
80
|
+
# the callback error should not negatively impact initialization of the config manager
|
|
81
|
+
time.sleep(0.1)
|
|
82
|
+
mock_get_config.assert_called_once_with(config_etag=None)
|
|
83
|
+
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
84
|
+
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
85
|
+
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
86
|
+
self.test_config_string
|
|
87
|
+
)
|
|
88
|
+
self.assertTrue(config_manager.is_initialized())
|
|
89
|
+
mock_callback.assert_called_once()
|
|
90
|
+
|
|
91
|
+
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
92
|
+
def test_close(self, mock_get_config):
|
|
93
|
+
mock_get_config.return_value = (self.test_config_json, self.test_etag)
|
|
94
|
+
self.test_options.config_polling_interval_ms = 500
|
|
95
|
+
|
|
96
|
+
config_manager = EnvironmentConfigManager(
|
|
97
|
+
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# sleep to allow polling thread to load the config
|
|
101
|
+
time.sleep(0.1)
|
|
102
|
+
|
|
103
|
+
config_manager.close()
|
|
104
|
+
self.assertFalse(config_manager._polling_enabled)
|
|
105
|
+
|
|
106
|
+
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
107
|
+
def test_get_config_unchanged(self, mock_get_config):
|
|
108
|
+
mock_get_config.return_value = (self.test_config_json, self.test_etag)
|
|
109
|
+
|
|
110
|
+
self.test_options.config_polling_interval_ms = 200
|
|
111
|
+
config_manager = EnvironmentConfigManager(
|
|
112
|
+
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
time.sleep(0.1)
|
|
116
|
+
# stop the polling
|
|
117
|
+
config_manager.close()
|
|
118
|
+
|
|
119
|
+
self.test_local_bucketing.store_config.reset_mock()
|
|
120
|
+
mock_get_config.return_value = (None, config_manager._config_etag)
|
|
121
|
+
|
|
122
|
+
# trigger refresh of the config directly
|
|
123
|
+
config_manager._get_config()
|
|
124
|
+
|
|
125
|
+
# verify that the config was not updated
|
|
126
|
+
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
127
|
+
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
128
|
+
self.test_local_bucketing.store_config.assert_not_called()
|
|
14
129
|
|
|
15
130
|
|
|
16
|
-
if __name__ ==
|
|
131
|
+
if __name__ == "__main__":
|
|
17
132
|
unittest.main()
|