devcycle-python-server-sdk 2.0.0__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.
Files changed (51) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/__init__.py +3 -2
  3. devcycle_python_sdk/api/backoff.py +11 -0
  4. devcycle_python_sdk/api/bucketing_client.py +17 -25
  5. devcycle_python_sdk/api/config_client.py +116 -0
  6. devcycle_python_sdk/api/event_client.py +95 -0
  7. devcycle_python_sdk/api/local_bucketing.py +436 -0
  8. devcycle_python_sdk/{dvc_cloud_client.py → cloud_client.py} +69 -41
  9. devcycle_python_sdk/exceptions.py +25 -3
  10. devcycle_python_sdk/local_client.py +220 -0
  11. devcycle_python_sdk/managers/__init__.py +0 -0
  12. devcycle_python_sdk/managers/config_manager.py +95 -0
  13. devcycle_python_sdk/managers/event_queue_manager.py +223 -0
  14. devcycle_python_sdk/models/bucketed_config.py +150 -0
  15. devcycle_python_sdk/models/event.py +111 -8
  16. devcycle_python_sdk/models/feature.py +12 -0
  17. devcycle_python_sdk/models/platform_data.py +33 -0
  18. devcycle_python_sdk/models/user.py +52 -7
  19. devcycle_python_sdk/models/variable.py +17 -5
  20. devcycle_python_sdk/options.py +106 -0
  21. devcycle_python_sdk/protobuf/__init__.py +0 -0
  22. devcycle_python_sdk/protobuf/utils.py +129 -0
  23. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +46 -0
  24. devcycle_python_sdk/py.typed +0 -0
  25. devcycle_python_sdk/util/version.py +1 -1
  26. {devcycle_python_server_sdk-2.0.0.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/METADATA +3 -2
  27. devcycle_python_server_sdk-3.0.0.dist-info/RECORD +55 -0
  28. example/{example.py → cloud_client_example.py} +24 -20
  29. example/local_bucketing_client_example.py +74 -0
  30. test/api/__init__.py +0 -0
  31. test/{test_bucketing_client.py → api/test_bucketing_client.py} +10 -5
  32. test/api/test_config_client.py +133 -0
  33. test/api/test_event_client.py +97 -0
  34. test/api/test_local_bucketing.py +392 -0
  35. test/fixture/__init__.py +0 -0
  36. test/fixture/data.py +57 -0
  37. test/managers/__init__.py +0 -0
  38. test/managers/test_config_manager.py +132 -0
  39. test/managers/test_event_queue_manager.py +271 -0
  40. test/models/__init__.py +0 -0
  41. test/models/test_bucketed_config.py +26 -0
  42. test/{test_dvc_cloud_client.py → test_cloud_client.py} +11 -17
  43. test/test_local_client.py +397 -0
  44. test/util/__init__.py +0 -0
  45. test/util/test_utils.py +183 -0
  46. test/{test_version.py → util/test_version.py} +2 -2
  47. devcycle_python_sdk/dvc_options.py +0 -18
  48. devcycle_python_server_sdk-2.0.0.dist-info/RECORD +0 -26
  49. {devcycle_python_server_sdk-2.0.0.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/LICENSE +0 -0
  50. {devcycle_python_server_sdk-2.0.0.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/WHEEL +0 -0
  51. {devcycle_python_server_sdk-2.0.0.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,436 @@
1
+ import logging
2
+ import random
3
+ import time
4
+ import json
5
+
6
+ from pathlib import Path
7
+ from threading import Lock
8
+ from typing import Any, cast, Optional, List
9
+
10
+ import wasmtime
11
+ from wasmtime import (
12
+ Engine,
13
+ Func,
14
+ FuncType,
15
+ Linker,
16
+ Memory,
17
+ Module,
18
+ Store,
19
+ ValType,
20
+ )
21
+
22
+ import devcycle_python_sdk.protobuf.utils as pb_utils
23
+ import devcycle_python_sdk.protobuf.variableForUserParams_pb2 as pb2
24
+ from devcycle_python_sdk.exceptions import (
25
+ VariableTypeMismatchError,
26
+ MalformedConfigError,
27
+ )
28
+ from devcycle_python_sdk.models.bucketed_config import BucketedConfig
29
+ from devcycle_python_sdk.models.user import DevCycleUser
30
+ from devcycle_python_sdk.models.variable import Variable, determine_variable_type
31
+ from devcycle_python_sdk.models.event import FlushPayload
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ wasm_path = Path(__file__).parent.parent / "bucketing-lib.release.wasm"
36
+
37
+
38
+ class WASMError(Exception):
39
+ pass
40
+
41
+
42
+ class WASMAbortError(WASMError):
43
+ pass
44
+
45
+
46
+ class LocalBucketing:
47
+ def __init__(self, sdk_key: str) -> None:
48
+ self.random = random.random()
49
+ self.wasm_lock = Lock()
50
+
51
+ wasi_cfg = wasmtime.WasiConfig()
52
+ wasi_cfg.inherit_env()
53
+ wasi_cfg.inherit_stderr()
54
+ wasi_cfg.inherit_stdout()
55
+
56
+ wasm_engine = Engine()
57
+ wasm_module = Module.from_file(
58
+ wasm_engine,
59
+ str(wasm_path),
60
+ )
61
+ wasm_linker = Linker(wasm_engine)
62
+ wasm_store = Store(wasm_engine)
63
+ wasm_store.set_wasi(wasi_cfg)
64
+ wasm_linker.define_wasi()
65
+
66
+ # Needs to return the current time since Epoch in milliseconds
67
+ def __date_now_func():
68
+ # convert from seconds to milliseconds
69
+ return time.time() * 1000
70
+
71
+ wasm_linker.define_func(
72
+ "env", "Date.now", FuncType([], [ValType.f64()]), __date_now_func
73
+ )
74
+
75
+ def __abort_func(
76
+ message_ptr=None,
77
+ filename_ptr=None,
78
+ line=0,
79
+ column=0,
80
+ ) -> None:
81
+ message = None
82
+ filename = None
83
+ if message_ptr is not None:
84
+ message = self._read_assembly_script_string(message_ptr)
85
+ if filename_ptr is not None:
86
+ filename = self._read_assembly_script_string(filename_ptr)
87
+
88
+ raise WASMAbortError(
89
+ f"Abort in {filename!r}:{line!r}:{column!r} -- {message!r}"
90
+ )
91
+
92
+ wasm_linker.define_func(
93
+ "env",
94
+ "abort",
95
+ FuncType([ValType.i32(), ValType.i32(), ValType.i32(), ValType.i32()], []),
96
+ __abort_func,
97
+ )
98
+
99
+ def __seed_func():
100
+ return time.time() * random.random()
101
+
102
+ wasm_linker.define_func(
103
+ "env", "seed", FuncType([], [ValType.f64()]), __seed_func
104
+ )
105
+
106
+ def __console_log_func(message_ptr) -> None:
107
+ message: str = self._read_assembly_script_string(message_ptr)
108
+ logger.warning(f"WASM console: {message!r}")
109
+
110
+ wasm_linker.define_func(
111
+ "env", "console.log", FuncType([ValType.i32()], []), __console_log_func
112
+ )
113
+
114
+ wasm_instance = wasm_linker.instantiate(wasm_store, wasm_module)
115
+ self.wasm_instance = wasm_instance
116
+ self.wasm_store = wasm_store
117
+ self.wasm_linker = wasm_linker
118
+ self.wasm_module = wasm_module
119
+ self.wasm_engine = wasm_engine
120
+
121
+ wasm_memory: Memory = self._get_export("memory")
122
+ self.wasm_memory = wasm_memory
123
+
124
+ # Bind exported internal AssemblyScript functions
125
+ self.__new: Func = self._get_export("__new")
126
+ self.__pin: Func = self._get_export("__pin")
127
+ self.__unpin: Func = self._get_export("__unpin")
128
+
129
+ # Bind exported WASM functions
130
+ self.initEventQueue = self._get_export("initEventQueue")
131
+ self.flushEventQueue = self._get_export("flushEventQueue")
132
+ self.eventQueueSize = self._get_export("eventQueueSize")
133
+ self.onPayloadSuccess = self._get_export("onPayloadSuccess")
134
+ self.onPayloadFailure = self._get_export("onPayloadFailure")
135
+ self.queueEvent = self._get_export("queueEvent")
136
+ self.queueAggregateEvent = self._get_export("queueAggregateEvent")
137
+ self.setConfigDataUTF8 = self._get_export("setConfigDataUTF8")
138
+ self.setPlatformDataUTF8 = self._get_export("setPlatformDataUTF8")
139
+ self.setClientCustomDataUTF8 = self._get_export("setClientCustomDataUTF8")
140
+ self.generateBucketedConfigForUserUTF8 = self._get_export(
141
+ "generateBucketedConfigForUserUTF8"
142
+ )
143
+ self.VariableForUserProtobuf = self._get_export("variableForUser_PB")
144
+
145
+ # Extract variable type enum values from WASM
146
+ self.variable_type_map = {
147
+ variable_type_key: self._get_export(
148
+ f"VariableType.{variable_type_key}"
149
+ ).value(wasm_store)
150
+ for variable_type_key in [
151
+ "Boolean",
152
+ "String",
153
+ "Number",
154
+ "JSON",
155
+ ]
156
+ }
157
+
158
+ # Set and pin the SDK key so it can be reused multiple times
159
+ self.sdk_key = sdk_key
160
+ self.sdk_key_addr = self._new_assembly_script_string(sdk_key)
161
+ self.__pin(self.wasm_store, self.sdk_key_addr)
162
+
163
+ # Allocate memory for header.
164
+ object_id_uint8_array = 9
165
+ header_pointer = cast(
166
+ int, self.__new(self.wasm_store, 12, object_id_uint8_array)
167
+ )
168
+
169
+ # An external object that is not referenced from within WebAssembly
170
+ # must be pinned whenever an allocation might happen in between
171
+ # allocating it and passing it to WebAssembly.
172
+ self.__pin(self.wasm_store, header_pointer)
173
+
174
+ self._header_pointer = header_pointer
175
+
176
+ def _get_export(self, export_name: str):
177
+ return self.wasm_instance.exports(self.wasm_store)[export_name]
178
+
179
+ def _new_assembly_script_string(self, param: str) -> int:
180
+ """
181
+ Allocate memory for a string in AssemblyScript and write the string
182
+ into memory, then return a pointer to the string.
183
+ Only safe for use with ASCII strings.
184
+ """
185
+ object_id_string = 2
186
+ encoded = param.encode("utf-8")
187
+ try:
188
+ # Create pointer to string buffer in WASM memory.
189
+ pointer = cast(
190
+ int, self.__new(self.wasm_store, len(encoded) * 2, object_id_string)
191
+ )
192
+ except Exception as err:
193
+ raise WASMError(f"Error allocating string in WASM: {err}")
194
+ addr = pointer
195
+ data = self.wasm_memory.data_ptr(self.wasm_store)
196
+
197
+ # Write encoded data into buffer.
198
+ for i, c in enumerate(encoded):
199
+ data[addr + i * 2] = c
200
+
201
+ return pointer
202
+
203
+ def _read_assembly_script_string(self, pointer: int) -> str:
204
+ """
205
+ Read a string from AssemblyScript memory.
206
+ Only safe for use with ASCII strings.
207
+
208
+
209
+ """
210
+ if pointer == 0:
211
+ raise ValueError(
212
+ "Null pointer passed to _read_assembly_script_string - cannot write string"
213
+ )
214
+
215
+ # Get a direct reference to the WASM memory
216
+ data = self.wasm_memory.data_ptr(self.wasm_store)
217
+
218
+ # Parse the string length from the header.
219
+ string_length = int.from_bytes(data[pointer - 4 : pointer], byteorder="little")
220
+ raw_data = data[pointer : pointer + string_length]
221
+
222
+ # This skips every other index in the resulting array because there
223
+ # isn't a great way to parse UTF-16 cleanly that matches the WTF-16
224
+ # format that ASC uses.
225
+ ret = bytearray(len(raw_data) // 2)
226
+ for i in range(0, len(raw_data), 2):
227
+ ret[i // 2] += raw_data[i]
228
+
229
+ return ret.decode("utf-8")
230
+
231
+ def _new_assembly_script_byte_array(self, param: bytes) -> int:
232
+ """
233
+ Allocates memory for a byte array in AssemblyScript and writes the byte
234
+ array into memory, then returns a pointer to the byte array.
235
+ """
236
+ object_id_byte_array = 1
237
+ data_length = len(param)
238
+ try:
239
+ # Allocate memory for buffer.
240
+ buffer_pointer = cast(
241
+ int, self.__new(self.wasm_store, data_length, object_id_byte_array)
242
+ )
243
+
244
+ # Get a direct reference to the WASM memory
245
+ data = self.wasm_memory.data_ptr(self.wasm_store)
246
+
247
+ # Write the buffer pointer value into the first 4 bytes of the header, little endian.
248
+ for i, b in enumerate(buffer_pointer.to_bytes(4, byteorder="little")):
249
+ data[self._header_pointer + i] = b
250
+ data[self._header_pointer + i + 4] = b
251
+
252
+ little_endian_buffer_len = data_length.to_bytes(4, byteorder="little")
253
+
254
+ # Write the buffer length into bytes 8-12 of the header, little endian.
255
+ for i, b in enumerate(little_endian_buffer_len):
256
+ data[self._header_pointer + 8 + i] = b
257
+
258
+ # Write the byte array data into the WASM buffer.
259
+ for i, b in enumerate(param):
260
+ data[buffer_pointer + i] = b
261
+
262
+ return self._header_pointer
263
+ except Exception as err:
264
+ raise WASMError(f"Error writing byte array to WASM: {err}")
265
+
266
+ def _read_assembly_script_byte_array(self, pointer: int) -> bytes:
267
+ """
268
+ Read a byte array from AssemblyScript memory.
269
+ """
270
+ if pointer == 0:
271
+ raise ValueError(
272
+ "Null pointer passed to _read_assembly_script_byte_array - cannot write string"
273
+ )
274
+
275
+ # Get a direct reference to the WASM memory
276
+ data = self.wasm_memory.data_ptr(self.wasm_store)
277
+
278
+ # Parse the data length and data pointer from the header.
279
+ data_length = int.from_bytes(
280
+ data[pointer + 8 : pointer + 12], byteorder="little"
281
+ )
282
+ data_pointer = int.from_bytes(data[pointer : pointer + 4], byteorder="little")
283
+
284
+ ret = bytearray(data_length)
285
+
286
+ # Copy the data from the WASM buffer into the return value.
287
+ for i in range(data_length):
288
+ ret[i] = data[data_pointer + i]
289
+
290
+ return bytes(ret)
291
+
292
+ def init_event_queue(self, options_json: str) -> None:
293
+ with self.wasm_lock:
294
+ options_addr = self._new_assembly_script_string(options_json)
295
+ self.initEventQueue(self.wasm_store, self.sdk_key_addr, options_addr)
296
+
297
+ def get_variable_for_user_protobuf(
298
+ self, user: DevCycleUser, key: str, default_value: Any
299
+ ) -> Optional[Variable]:
300
+ var_type = determine_variable_type(default_value)
301
+ pb_variable_type = pb_utils.convert_type_enum_to_variable_type(var_type)
302
+
303
+ params_pb = pb2.VariableForUserParams_PB(
304
+ sdkKey=self.sdk_key,
305
+ variableKey=key,
306
+ variableType=pb_variable_type,
307
+ user=pb_utils.create_dvcuser_pb(user),
308
+ shouldTrackEvent=True,
309
+ )
310
+
311
+ params_str = params_pb.SerializeToString()
312
+
313
+ with self.wasm_lock:
314
+ params_addr = self._new_assembly_script_byte_array(params_str)
315
+ variable_addr = self.VariableForUserProtobuf(self.wasm_store, params_addr)
316
+
317
+ if variable_addr == 0:
318
+ return None
319
+ else:
320
+ var_bytes = self._read_assembly_script_byte_array(variable_addr)
321
+ sdk_variable = pb2.SDKVariable_PB()
322
+ sdk_variable.ParseFromString(var_bytes)
323
+
324
+ if sdk_variable.type != pb_variable_type:
325
+ # this situation should never actually happen because the WASM handles
326
+ # it internally and returns a null value from the WASM function
327
+ # This check is here just in case that logic changes in the future
328
+ raise VariableTypeMismatchError(
329
+ f"Variable returned does not match requested type: {pb_variable_type}"
330
+ )
331
+ return pb_utils.create_variable(sdk_variable, default_value)
332
+
333
+ def generate_bucketed_config(self, user: DevCycleUser) -> BucketedConfig:
334
+ user_json = json.dumps(user.to_json())
335
+
336
+ with self.wasm_lock:
337
+ user_json_addr = self._new_assembly_script_byte_array(
338
+ user_json.encode("utf-8")
339
+ )
340
+ config_addr = self.generateBucketedConfigForUserUTF8(
341
+ self.wasm_store, self.sdk_key_addr, user_json_addr
342
+ )
343
+
344
+ config_bytes = self._read_assembly_script_byte_array(config_addr)
345
+
346
+ config_data = json.loads(config_bytes.decode("utf-8"))
347
+
348
+ try:
349
+ config = BucketedConfig.from_json(config_data)
350
+ except KeyError as e:
351
+ raise MalformedConfigError(
352
+ f"Failed to parse bucketed config: missing key {e}"
353
+ ) from e
354
+
355
+ config.user = user
356
+
357
+ return config
358
+
359
+ def store_config(self, config_json: str) -> None:
360
+ with self.wasm_lock:
361
+ data = config_json.encode("utf-8")
362
+ config_addr = self._new_assembly_script_byte_array(data)
363
+ self.setConfigDataUTF8(self.wasm_store, self.sdk_key_addr, config_addr)
364
+
365
+ def set_platform_data(self, platform_json: str) -> None:
366
+ with self.wasm_lock:
367
+ data = platform_json.encode("utf-8")
368
+ data_addr = self._new_assembly_script_byte_array(data)
369
+ self.setPlatformDataUTF8(self.wasm_store, data_addr)
370
+
371
+ def set_client_custom_data(self, client_data_json: str) -> None:
372
+ with self.wasm_lock:
373
+ data = client_data_json.encode("utf-8")
374
+ data_addr = self._new_assembly_script_byte_array(data)
375
+ self.setClientCustomDataUTF8(self.wasm_store, self.sdk_key_addr, data_addr)
376
+
377
+ def flush_event_queue(self) -> List[FlushPayload]:
378
+ """
379
+ Collects the events that are ready to send to the server. Events are group into separate payloads
380
+
381
+ Returns: a list of FlushPayload objects, or an empty list if there are no events to send
382
+ """
383
+ with self.wasm_lock:
384
+ result_addr = self.flushEventQueue(self.wasm_store, self.sdk_key_addr)
385
+ result_str = self._read_assembly_script_string(result_addr)
386
+ result_json = json.loads(result_str)
387
+ return [FlushPayload.from_json(element) for element in result_json]
388
+
389
+ def on_event_payload_success(self, payload_id: str) -> None:
390
+ """
391
+ Notifies the WASM that the events associated with the payload_id have been sent successfully and
392
+ can be purged from the queue
393
+ """
394
+ with self.wasm_lock:
395
+ id_addr = self._new_assembly_script_string(payload_id)
396
+ self.onPayloadSuccess(self.wasm_store, self.sdk_key_addr, id_addr)
397
+
398
+ def on_event_payload_failure(self, payload_id: str, retryable: bool) -> None:
399
+ """
400
+ Notifies the WASM that the events associated with the payload_id failed to be sent and should
401
+ be re-queued.
402
+ """
403
+ with self.wasm_lock:
404
+ id_addr = self._new_assembly_script_string(payload_id)
405
+ self.onPayloadFailure(
406
+ self.wasm_store, self.sdk_key_addr, id_addr, 1 if retryable else 0
407
+ )
408
+
409
+ def get_event_queue_size(self) -> int:
410
+ """
411
+ Returns the number of events currently in the queue
412
+ """
413
+ with self.wasm_lock:
414
+ val = self.eventQueueSize(self.wasm_store, self.sdk_key_addr)
415
+ return int(val)
416
+
417
+ def queue_event(self, user_json: str, event_json: str) -> None:
418
+ with self.wasm_lock:
419
+ user_addr = self._new_assembly_script_string(user_json)
420
+ event_addr = self._new_assembly_script_string(event_json)
421
+ self.queueEvent(self.wasm_store, self.sdk_key_addr, user_addr, event_addr)
422
+
423
+ def queue_aggregate_event(
424
+ self, event_json: str, variable_variation_map_json: str
425
+ ) -> None:
426
+ with self.wasm_lock:
427
+ event_addr = self._new_assembly_script_string(event_json)
428
+ variable_variation_map_addr = self._new_assembly_script_string(
429
+ variable_variation_map_json
430
+ )
431
+ self.queueAggregateEvent(
432
+ self.wasm_store,
433
+ self.sdk_key_addr,
434
+ event_addr,
435
+ variable_variation_map_addr,
436
+ )
@@ -9,8 +9,8 @@ from devcycle_python_sdk.exceptions import (
9
9
  NotFoundError,
10
10
  CloudClientUnauthorizedError,
11
11
  )
12
- from devcycle_python_sdk.models.user import User
13
- from devcycle_python_sdk.models.event import Event
12
+ from devcycle_python_sdk.models.user import DevCycleUser
13
+ from devcycle_python_sdk.models.event import DevCycleEvent
14
14
  from devcycle_python_sdk.models.variable import Variable
15
15
  from devcycle_python_sdk.models.feature import Feature
16
16
  from devcycle_python_sdk.util.version import sdk_version
@@ -25,7 +25,7 @@ class DevCycleCloudClient:
25
25
  sdk_version: str
26
26
 
27
27
  def __init__(self, sdk_key: str, options: DevCycleCloudOptions):
28
- self._validate_sdk_key(sdk_key)
28
+ _validate_sdk_key(sdk_key)
29
29
 
30
30
  if options is None:
31
31
  self.options = DevCycleCloudOptions()
@@ -38,34 +38,30 @@ class DevCycleCloudClient:
38
38
  self.sdk_type = "server"
39
39
  self.bucketing_api = BucketingAPIClient(sdk_key, self.options)
40
40
 
41
- def _add_platform_data_to_user(self, user: User) -> User:
41
+ def _add_platform_data_to_user(self, user: DevCycleUser) -> DevCycleUser:
42
42
  user.platform = self.platform
43
43
  user.platformVersion = self.platform_version
44
44
  user.sdkVersion = self.sdk_version
45
45
  user.sdkType = self.sdk_type
46
46
  return user
47
47
 
48
- def _validate_sdk_key(self, sdk_key: str) -> None:
49
- if sdk_key is None or len(sdk_key) == 0:
50
- raise ValueError("Missing SDK key! Call build with a valid server SDK key")
48
+ def variable_value(self, user: DevCycleUser, key: str, default_value: Any) -> Any:
49
+ """
50
+ Evaluates a variable for a user and returns the value. If the user is not bucketed into the variable, the default value will be returned
51
51
 
52
- if not sdk_key.startswith("server") and not sdk_key.startswith("dvc_server"):
53
- raise ValueError(
54
- "Invalid SDK key provided. Call build with a valid server SDK key"
55
- )
56
-
57
- def _validate_user(self, user: User) -> None:
58
- if user is None:
59
- raise ValueError("User cannot be None")
60
-
61
- if user.user_id is None or len(user.user_id) == 0:
62
- raise ValueError("userId cannot be empty")
63
-
64
- def variable_value(self, user: User, key: str, default_value: Any) -> Any:
52
+ :param user: The user to evaluate the variable for
53
+ """
65
54
  return self.variable(user, key, default_value).value
66
55
 
67
- def variable(self, user: User, key: str, default_value: Any) -> Variable:
68
- self._validate_user(user)
56
+ def variable(self, user: DevCycleUser, key: str, default_value: Any) -> Variable:
57
+ """
58
+ Evaluates a variable for a user.
59
+
60
+ :param user: The user to evaluate the variable for
61
+ :param key: The key of the variable to evaluate
62
+ :param default_value: The default value to return if the user is not bucketed into the variable
63
+ """
64
+ _validate_user(user)
69
65
  user = self._add_platform_data_to_user(user)
70
66
 
71
67
  if not key:
@@ -77,15 +73,15 @@ class DevCycleCloudClient:
77
73
  try:
78
74
  variable = self.bucketing_api.variable(key, user)
79
75
  except CloudClientUnauthorizedError as e:
80
- logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
76
+ logger.warning("DevCycle SDK key is invalid, unable to make cloud request")
81
77
  raise e
82
78
  except NotFoundError:
83
- logger.warning("DevCycle: variable not found: %s", key)
79
+ logger.warning(f"Variable not found: {key}")
84
80
  return Variable.create_default_variable(
85
81
  key=key, default_value=default_value
86
82
  )
87
83
  except Exception as e:
88
- logger.error("DevCycle: Error fetching variable: %s", e)
84
+ logger.error(f"Error evaluating variable: {e}")
89
85
  return Variable.create_default_variable(
90
86
  key=key, default_value=default_value
91
87
  )
@@ -95,10 +91,7 @@ class DevCycleCloudClient:
95
91
  # Allow default value to be a subclass of the same type as the variable
96
92
  if not isinstance(default_value, type(variable.value)):
97
93
  logger.warning(
98
- "DevCycle: variable %s is type %s, but default value is type %s",
99
- key,
100
- type(variable.value),
101
- type(default_value),
94
+ f"Variable {key} is type {type(variable.value)}, but default value is type {type(default_value)}",
102
95
  )
103
96
  return Variable.create_default_variable(
104
97
  key=key, default_value=default_value
@@ -106,41 +99,58 @@ class DevCycleCloudClient:
106
99
 
107
100
  return variable
108
101
 
109
- def all_variables(self, user: User) -> Dict[str, Variable]:
110
- self._validate_user(user)
102
+ def all_variables(self, user: DevCycleUser) -> Dict[str, Variable]:
103
+ """
104
+ Returns all segmented and bucketed variables for a user. This method will return an empty map if the user is not bucketed into any variables
105
+
106
+ :param user: The user to retrieve features for
107
+ """
108
+ _validate_user(user)
111
109
  user = self._add_platform_data_to_user(user)
112
110
 
113
111
  variable_map: Dict[str, Variable] = {}
114
112
  try:
115
113
  variable_map = self.bucketing_api.variables(user)
116
114
  except CloudClientUnauthorizedError as e:
117
- logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
115
+ logger.warning("SDK key is invalid, unable to make cloud request")
118
116
  raise e
119
117
  except Exception as e:
120
- logger.error("Error retrieving all features for a user: %s", e)
118
+ logger.error(f"Error retrieving all features for a user: {e}")
121
119
 
122
120
  return variable_map
123
121
 
124
- def all_features(self, user: User) -> Dict[str, Feature]:
125
- self._validate_user(user)
122
+ def all_features(self, user: DevCycleUser) -> Dict[str, Feature]:
123
+ """
124
+ Returns all segmented and bucketed features for a user. This method will return an empty map if the user is not bucketed into any features
125
+
126
+ :param user: The user to retrieve features for
127
+ """
128
+ _validate_user(user)
126
129
  user = self._add_platform_data_to_user(user)
127
130
 
128
131
  feature_map: Dict[str, Feature] = {}
129
132
  try:
130
133
  feature_map = self.bucketing_api.features(user)
131
134
  except CloudClientUnauthorizedError as e:
132
- logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
135
+ logger.warning("SDK key is invalid, unable to make cloud request")
133
136
  raise e
134
137
  except Exception as e:
135
- logger.error("Error retrieving all features for a user: %s", e)
138
+ logger.error(f"Error retrieving all features for a user: {e}")
136
139
 
137
140
  return feature_map
138
141
 
139
- def track(self, user: User, user_event: Event) -> None:
142
+ def track(self, user: DevCycleUser, user_event: DevCycleEvent) -> None:
143
+ """
144
+ Tracks a custom event for a user.
145
+
146
+ :param user: The user to track the event for
147
+ :param user_event: The event to track
148
+ """
149
+
140
150
  if user_event is None or not user_event.type:
141
151
  raise ValueError("Invalid Event")
142
152
 
143
- self._validate_user(user)
153
+ _validate_user(user)
144
154
  user = self._add_platform_data_to_user(user)
145
155
 
146
156
  if user_event is None or not user_event.type:
@@ -150,7 +160,25 @@ class DevCycleCloudClient:
150
160
  try:
151
161
  self.bucketing_api.track(user, events)
152
162
  except CloudClientUnauthorizedError as e:
153
- logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
163
+ logger.warning("SDK key is invalid, unable to make cloud request")
154
164
  raise e
155
165
  except Exception as e:
156
- logger.error("Error tracking event: %s", e)
166
+ logger.error(f"Error tracking event: {e}")
167
+
168
+
169
+ def _validate_sdk_key(sdk_key: str) -> None:
170
+ if sdk_key is None or len(sdk_key) == 0:
171
+ raise ValueError("Missing SDK key! Call initialize with a valid SDK key")
172
+
173
+ if not sdk_key.startswith("server") and not sdk_key.startswith("dvc_server"):
174
+ raise ValueError(
175
+ "Invalid SDK key provided. Please call initialize with a valid server SDK key"
176
+ )
177
+
178
+
179
+ def _validate_user(user: DevCycleUser) -> None:
180
+ if user is None:
181
+ raise ValueError("User cannot be None")
182
+
183
+ if user.user_id is None or len(user.user_id) == 0:
184
+ raise ValueError("userId cannot be empty")
@@ -1,21 +1,43 @@
1
1
  from typing import Optional
2
2
 
3
3
 
4
- class CloudClientError(Exception):
4
+ class APIClientError(Exception):
5
5
  def __init__(self, message: str, cause: Optional[Exception] = None):
6
6
  self.message = message
7
7
  self.__cause__ = cause
8
8
 
9
9
  def __str__(self):
10
- return f"CloudClientException: {self.message}"
10
+ return f"APIClientError: {self.message}"
11
11
 
12
12
 
13
- class CloudClientUnauthorizedError(Exception):
13
+ class APIClientUnauthorizedError(Exception):
14
14
  def __init__(self, message: str):
15
15
  self.message = message
16
16
  super().__init__(message)
17
17
 
18
18
 
19
+ class CloudClientError(APIClientError):
20
+ def __init__(self, message: str, cause: Optional[Exception] = None):
21
+ super().__init__(message, cause)
22
+
23
+ def __str__(self):
24
+ return f"CloudClientException: {self.message}"
25
+
26
+
27
+ class CloudClientUnauthorizedError(APIClientUnauthorizedError):
28
+ def __init__(self, message: str):
29
+ super().__init__(message)
30
+
31
+
19
32
  class NotFoundError(Exception):
20
33
  def __init__(self, key: str):
21
34
  self.key = key
35
+
36
+
37
+ class VariableTypeMismatchError(Exception):
38
+ def __init__(self, message: str):
39
+ super().__init__(message)
40
+
41
+
42
+ class MalformedConfigError(Exception):
43
+ pass