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.
Files changed (50) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/__init__.py +3 -3
  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 +429 -2
  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/config_manager.py +83 -15
  12. devcycle_python_sdk/managers/event_queue_manager.py +213 -8
  13. devcycle_python_sdk/models/bucketed_config.py +150 -0
  14. devcycle_python_sdk/models/event.py +111 -8
  15. devcycle_python_sdk/models/feature.py +12 -0
  16. devcycle_python_sdk/models/platform_data.py +33 -0
  17. devcycle_python_sdk/models/user.py +52 -7
  18. devcycle_python_sdk/models/variable.py +17 -5
  19. devcycle_python_sdk/options.py +106 -0
  20. devcycle_python_sdk/protobuf/__init__.py +0 -0
  21. devcycle_python_sdk/protobuf/utils.py +129 -0
  22. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +46 -0
  23. devcycle_python_sdk/util/version.py +1 -1
  24. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/METADATA +3 -1
  25. devcycle_python_server_sdk-3.0.0.dist-info/RECORD +55 -0
  26. example/{example.py → cloud_client_example.py} +24 -20
  27. example/local_bucketing_client_example.py +74 -0
  28. test/api/test_bucketing_client.py +10 -5
  29. test/api/test_config_client.py +133 -0
  30. test/api/test_event_client.py +97 -0
  31. test/api/test_local_bucketing.py +379 -4
  32. test/fixture/__init__.py +0 -0
  33. test/fixture/data.py +57 -0
  34. test/managers/test_config_manager.py +120 -5
  35. test/managers/test_event_queue_manager.py +259 -5
  36. test/models/__init__.py +0 -0
  37. test/models/test_bucketed_config.py +26 -0
  38. test/{test_dvc_cloud_client.py → test_cloud_client.py} +11 -17
  39. test/test_local_client.py +397 -0
  40. test/util/test_utils.py +183 -0
  41. test/util/test_version.py +2 -2
  42. devcycle_python_sdk/dvc_local_client.py +0 -152
  43. devcycle_python_sdk/dvc_options.py +0 -45
  44. devcycle_python_server_sdk-2.0.1.dist-info/RECORD +0 -41
  45. test/test_bucketing_client.py +0 -228
  46. test/test_dvc_local_client.py +0 -143
  47. test/test_version.py +0 -18
  48. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/LICENSE +0 -0
  49. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/WHEEL +0 -0
  50. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,46 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: variableForUserParams.proto
4
+ """Generated protocol buffer code."""
5
+ from google.protobuf.internal import builder as _builder
6
+ from google.protobuf import descriptor as _descriptor
7
+ from google.protobuf import descriptor_pool as _descriptor_pool
8
+ from google.protobuf import symbol_database as _symbol_database
9
+ # @@protoc_insertion_point(imports)
10
+
11
+ _sym_db = _symbol_database.Default()
12
+
13
+
14
+
15
+
16
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bvariableForUserParams.proto\"/\n\x0eNullableString\x12\r\n\x05value\x18\x01 \x01(\t\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\"/\n\x0eNullableDouble\x12\r\n\x05value\x18\x01 \x01(\x01\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\"m\n\x0f\x43ustomDataValue\x12\x1d\n\x04type\x18\x01 \x01(\x0e\x32\x0f.CustomDataType\x12\x11\n\tboolValue\x18\x02 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x03 \x01(\x01\x12\x13\n\x0bstringValue\x18\x04 \x01(\t\"\x93\x01\n\x12NullableCustomData\x12-\n\x05value\x18\x01 \x03(\x0b\x32\x1e.NullableCustomData.ValueEntry\x12\x0e\n\x06isNull\x18\x02 \x01(\x08\x1a>\n\nValueEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x1f\n\x05value\x18\x02 \x01(\x0b\x32\x10.CustomDataValue:\x02\x38\x01\"\x9c\x01\n\x18VariableForUserParams_PB\x12\x0e\n\x06sdkKey\x18\x01 \x01(\t\x12\x13\n\x0bvariableKey\x18\x02 \x01(\t\x12&\n\x0cvariableType\x18\x03 \x01(\x0e\x32\x10.VariableType_PB\x12\x19\n\x04user\x18\x04 \x01(\x0b\x32\x0b.DVCUser_PB\x12\x18\n\x10shouldTrackEvent\x18\x05 \x01(\x08\"\xe8\x02\n\nDVCUser_PB\x12\x0f\n\x07user_id\x18\x01 \x01(\t\x12\x1e\n\x05\x65mail\x18\x02 \x01(\x0b\x32\x0f.NullableString\x12\x1d\n\x04name\x18\x03 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08language\x18\x04 \x01(\x0b\x32\x0f.NullableString\x12 \n\x07\x63ountry\x18\x05 \x01(\x0b\x32\x0f.NullableString\x12!\n\x08\x61ppBuild\x18\x06 \x01(\x0b\x32\x0f.NullableDouble\x12#\n\nappVersion\x18\x07 \x01(\x0b\x32\x0f.NullableString\x12$\n\x0b\x64\x65viceModel\x18\x08 \x01(\x0b\x32\x0f.NullableString\x12\'\n\ncustomData\x18\t \x01(\x0b\x32\x13.NullableCustomData\x12.\n\x11privateCustomData\x18\n \x01(\x0b\x32\x13.NullableCustomData\"\xac\x01\n\x0eSDKVariable_PB\x12\x0b\n\x03_id\x18\x01 \x01(\t\x12\x1e\n\x04type\x18\x02 \x01(\x0e\x32\x10.VariableType_PB\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x11\n\tboolValue\x18\x04 \x01(\x08\x12\x13\n\x0b\x64oubleValue\x18\x05 \x01(\x01\x12\x13\n\x0bstringValue\x18\x06 \x01(\t\x12#\n\nevalReason\x18\x07 \x01(\x0b\x32\x0f.NullableString*@\n\x0fVariableType_PB\x12\x0b\n\x07\x42oolean\x10\x00\x12\n\n\x06Number\x10\x01\x12\n\n\x06String\x10\x02\x12\x08\n\x04JSON\x10\x03*6\n\x0e\x43ustomDataType\x12\x08\n\x04\x42ool\x10\x00\x12\x07\n\x03Num\x10\x01\x12\x07\n\x03Str\x10\x02\x12\x08\n\x04Null\x10\x03\x42X\n&com.devcycle.sdk.server.local.protobufP\x01Z\x07./proto\xaa\x02\"DevCycle.SDK.Server.Local.Protobufb\x06proto3')
17
+
18
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
19
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'variableForUserParams_pb2', globals())
20
+ if _descriptor._USE_C_DESCRIPTORS == False:
21
+
22
+ DESCRIPTOR._options = None
23
+ DESCRIPTOR._serialized_options = b'\n&com.devcycle.sdk.server.local.protobufP\001Z\007./proto\252\002\"DevCycle.SDK.Server.Local.Protobuf'
24
+ _NULLABLECUSTOMDATA_VALUEENTRY._options = None
25
+ _NULLABLECUSTOMDATA_VALUEENTRY._serialized_options = b'8\001'
26
+ _VARIABLETYPE_PB._serialized_start=1087
27
+ _VARIABLETYPE_PB._serialized_end=1151
28
+ _CUSTOMDATATYPE._serialized_start=1153
29
+ _CUSTOMDATATYPE._serialized_end=1207
30
+ _NULLABLESTRING._serialized_start=31
31
+ _NULLABLESTRING._serialized_end=78
32
+ _NULLABLEDOUBLE._serialized_start=80
33
+ _NULLABLEDOUBLE._serialized_end=127
34
+ _CUSTOMDATAVALUE._serialized_start=129
35
+ _CUSTOMDATAVALUE._serialized_end=238
36
+ _NULLABLECUSTOMDATA._serialized_start=241
37
+ _NULLABLECUSTOMDATA._serialized_end=388
38
+ _NULLABLECUSTOMDATA_VALUEENTRY._serialized_start=326
39
+ _NULLABLECUSTOMDATA_VALUEENTRY._serialized_end=388
40
+ _VARIABLEFORUSERPARAMS_PB._serialized_start=391
41
+ _VARIABLEFORUSERPARAMS_PB._serialized_end=547
42
+ _DVCUSER_PB._serialized_start=550
43
+ _DVCUSER_PB._serialized_end=910
44
+ _SDKVARIABLE_PB._serialized_start=913
45
+ _SDKVARIABLE_PB._serialized_end=1085
46
+ # @@protoc_insertion_point(module_scope)
@@ -5,5 +5,5 @@ def sdk_version() -> str:
5
5
  """
6
6
  Returns the current version of this SDK as a semantic version string
7
7
  """
8
- with open(os.path.dirname(__file__) + '/../VERSION.txt', 'r') as version_file:
8
+ with open(os.path.dirname(__file__) + "/../VERSION.txt", "r") as version_file:
9
9
  return version_file.read().strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devcycle-python-server-sdk
3
- Version: 2.0.1
3
+ Version: 3.0.0
4
4
  Summary: DevCycle Python SDK
5
5
  Home-page: https://github.com/devcycleHQ/python-server-sdk
6
6
  Author-email:
@@ -9,6 +9,8 @@ License-File: LICENSE
9
9
  Requires-Dist: setuptools (>=21.0.0)
10
10
  Requires-Dist: urllib3 (>=1.15.1)
11
11
  Requires-Dist: requests (~=2.31.0)
12
+ Requires-Dist: wasmtime (==9.0.0)
13
+ Requires-Dist: protobuf (>=4.23.3)
12
14
 
13
15
  The DevCycle Python SDK used for feature management.
14
16
 
@@ -0,0 +1,55 @@
1
+ devcycle_python_sdk/VERSION.txt,sha256=yRY_8h8fKwOQ3Ei92kcXlxj3cvUHp8687KWc4acSkCk,5
2
+ devcycle_python_sdk/__init__.py,sha256=vOoglQ8L7RsHTp-GSa8g7k2AAPrR_obhiDSC09LpG8w,265
3
+ devcycle_python_sdk/cloud_client.py,sha256=k9Vr24ej6oNZV3WkbO8KKCcp0ERTnsvmCySI7-A8QYw,6675
4
+ devcycle_python_sdk/exceptions.py,sha256=Et7fS8pEFbHkFY86T2pJoEUstwjh4_rzhJxVH8fzAns,1053
5
+ devcycle_python_sdk/local_client.py,sha256=fJDW4JVQY2ePKAN9OUeDhuHOhMqGGeKrv4Z7eMpfmZU,8605
6
+ devcycle_python_sdk/options.py,sha256=HSny3e5sXydrCamoTSyNkmkdcuaGjY_9etNPngw9Avc,4525
7
+ devcycle_python_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ devcycle_python_sdk/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
9
+ devcycle_python_sdk/api/backoff.py,sha256=F6WYCloaZD9CjNu6mKBMq7K20S8-i2WVU6X05ZRoawE,303
10
+ devcycle_python_sdk/api/bucketing_client.py,sha256=cchUuSngJEgDnhR4HeOsCgjH2gJu-yDbz9HFLDT2Bzk,5074
11
+ devcycle_python_sdk/api/config_client.py,sha256=jtnDmGQBaxOCxF346iDfqjIMKclqdOZiApXeXLDakbs,4226
12
+ devcycle_python_sdk/api/event_client.py,sha256=y1u7HQLTM_FHxiwyZ_2hahnVjYaNv2HuSzlNcIHIiOk,3319
13
+ devcycle_python_sdk/api/local_bucketing.py,sha256=96UNaq18Xnx69xIKBQZLOY8C1jkjsDc0vJvR9Jm2_Qw,16322
14
+ devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ devcycle_python_sdk/managers/config_manager.py,sha256=qv71EMtV0qvHOjkclP2fE54eXe8v_MnehjyIkr-YPsM,3122
16
+ devcycle_python_sdk/managers/event_queue_manager.py,sha256=JvOefMEnK3-zbco5TCGr9vysa0nYmI5Eg58Dg4SDUuU,7909
17
+ devcycle_python_sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ devcycle_python_sdk/models/bucketed_config.py,sha256=rSKpxMTF-IbUVQ9gAWrWnb9Q9YF94ATqEFWNWuoQntk,3814
19
+ devcycle_python_sdk/models/error_response.py,sha256=r_dRUDAbral9UGaMYcnBsZOr9Hmkyi1SQbhGmZilgpA,442
20
+ devcycle_python_sdk/models/event.py,sha256=-6bhKp72mAgaoSxvhS__G4vvfQ9Psj83fc0Igelzw3o,3713
21
+ devcycle_python_sdk/models/feature.py,sha256=m3wxxIjC5jxhKpPLI4RcsBaNLuy-zsMqoAgc8ySPe0I,852
22
+ devcycle_python_sdk/models/platform_data.py,sha256=kIFevH5trsxI32qp7BW6glUCpipgpaVBKGqJWzQd5Xo,783
23
+ devcycle_python_sdk/models/user.py,sha256=BGMtVM6shDX7rUfGfBbI55IUEsQ_4qrnmtwTqOcB-8A,2616
24
+ devcycle_python_sdk/models/variable.py,sha256=qp00SyCNyfYiMlTwnDSDJ4ZetRF35JHTbYZQOBxFIqY,1711
25
+ devcycle_python_sdk/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ devcycle_python_sdk/protobuf/utils.py,sha256=zi-kVx6yvgS2c04cUobLd2IDc_ZqKkWduay9YcpjoGc,4684
27
+ devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=Zf4-8n67UnulC0Sz90aqLYsXCFEgywOqfiSHuXZomLk,4244
28
+ devcycle_python_sdk/util/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
29
+ devcycle_python_sdk/util/version.py,sha256=Cz7Us_EuXP-T8GI47g2WHYdBJj_zKo-5y545IDHJAjI,253
30
+ example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ example/cloud_client_example.py,sha256=YD8ZsTv3dPYfbFUKraRpRT0VsJGrPwjm8ZCuA5H54yI,2463
32
+ example/local_bucketing_client_example.py,sha256=WRyJOUayO1siwnkfPhXvMg3Oz53akcA-Y4EMBI1e4q0,2676
33
+ test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ test/test_cloud_client.py,sha256=zjk302m7qKMCQryolkc1WKLFmbGWUxA0VtHlZk0rY-0,11442
35
+ test/test_local_client.py,sha256=mEBhq0gnHzjE-i_w4hMrmfhoHDJvCXGt3G8lx0EA9OE,12671
36
+ test/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ test/api/test_bucketing_client.py,sha256=Zomy0LQ-abI6nZvazSgBZQX6OjPwqBbsmfXR7KpdNOk,7565
38
+ test/api/test_config_client.py,sha256=gV86fwGBFN5r81jrfGKyptldgR5YGEXdUdFlXwFaupg,4275
39
+ test/api/test_event_client.py,sha256=jaWFWY0jrFI0amCfRsByi-JHPkoWKH_Z7V2nNcbvepM,3211
40
+ test/api/test_local_bucketing.py,sha256=E7alurvIScT0nilcuY2xGLCXzwxHrNWPBGdWxG6bR5A,15156
41
+ test/fixture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ test/fixture/data.py,sha256=OFHR29FiiMWhh2bmyeEbCck4Jyy8Ah7MsoLg0JZN0vk,1372
43
+ test/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
+ test/managers/test_config_manager.py,sha256=_MtvyxK06xpBzzEsN1zsLdYrk3DsPsbCmSj7XorINmI,5366
45
+ test/managers/test_event_queue_manager.py,sha256=KquHBuYiDdoVStm5Ga9mIB-wvmJnChiFRBNC8sMKaxQ,11053
46
+ test/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
+ test/models/test_bucketed_config.py,sha256=8wCEyxaHxY21nl7ZbiFzp-IzZQBZIPIVPPzeREjumQk,858
48
+ test/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ test/util/test_utils.py,sha256=tVBJm_qnqg-4PssLpXIxqlk0JehuiAIPs4zpi2oNVnY,6671
50
+ test/util/test_version.py,sha256=4Qhi1grX84lmyQRJ6aQMJgMerl3ce2_jKEl71vhqG5E,368
51
+ devcycle_python_server_sdk-3.0.0.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
52
+ devcycle_python_server_sdk-3.0.0.dist-info/METADATA,sha256=unv6PBvFApXc0qamRTmXPzgRJTFO5qGNllrmCElhSgs,452
53
+ devcycle_python_server_sdk-3.0.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
54
+ devcycle_python_server_sdk-3.0.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
55
+ devcycle_python_server_sdk-3.0.0.dist-info/RECORD,,
@@ -1,9 +1,11 @@
1
+ import datetime
1
2
  import logging
2
3
  import os
3
4
 
4
5
  from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions
5
- from devcycle_python_sdk.models.user import User
6
- from devcycle_python_sdk.models.event import Event
6
+
7
+ from devcycle_python_sdk.models.user import DevCycleUser
8
+ from devcycle_python_sdk.models.event import DevCycleEvent, EventType
7
9
 
8
10
  VARIABLE_KEY = "test-boolean-variable"
9
11
 
@@ -12,37 +14,34 @@ logger = logging.getLogger(__name__)
12
14
 
13
15
  def main():
14
16
  """
15
- Sample generic usage of the Python SDK.
17
+ Sample usage of the Python Server SDK using Cloud Bucketing.
16
18
  For a Django specific sample app, please see https://github.com/DevCycleHQ/python-django-example-app/
17
-
18
19
  """
19
20
  logging.basicConfig(level="INFO", format="%(levelname)s: %(message)s")
20
21
 
22
+ # create an instance of the DevCycle Client object
23
+ server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
21
24
  options = DevCycleCloudOptions(enable_edge_db=True)
25
+ client = DevCycleCloudClient(server_sdk_key, options)
22
26
 
23
- # create an instance of the API class
24
- server_sdk_key = os.environ["DVC_SERVER_SDK_KEY"]
25
- dvc = DevCycleCloudClient(server_sdk_key, options)
26
-
27
- user = User(user_id="test", email="yo@yo.ca", country="CA")
28
- event = Event(type="customEvent", target="somevariable.key")
27
+ user = DevCycleUser(user_id="test-1234", email="test-user@domain.com", country="US")
29
28
 
30
29
  # Use variable_value to access the value of a variable directly
31
- if dvc.variable_value(user, VARIABLE_KEY, False):
30
+ if client.variable_value(user, VARIABLE_KEY, False):
32
31
  logger.info(f"Variable {VARIABLE_KEY} is enabled")
33
32
  else:
34
33
  logger.info(f"Variable {VARIABLE_KEY} is not enabled")
35
34
 
36
35
  # DevCycle handles missing or wrongly typed variables by returning the default value
37
36
  # You can check this explicitly by using the variable method
38
- variable = dvc.variable(user, VARIABLE_KEY + "-does-not-exist", False)
37
+ variable = client.variable(user, VARIABLE_KEY + "-does-not-exist", False)
39
38
  if variable.isDefaulted:
40
39
  logger.info(f"Variable {variable.key} is defaulted to {variable.value}")
41
40
 
42
41
  try:
43
42
  # Get all variables by key for user data
44
- all_variables_response = dvc.all_variables(user)
45
- logger.info("All variables:\n%s", all_variables_response)
43
+ all_variables_response = client.all_variables(user)
44
+ logger.info(f"All variables:\n{all_variables_response}")
46
45
 
47
46
  if VARIABLE_KEY not in all_variables_response:
48
47
  logger.warning(
@@ -50,14 +49,19 @@ def main():
50
49
  )
51
50
 
52
51
  # Get all features by key for user data
53
- all_features_response = dvc.all_features(user)
54
- logger.info("All features:\n%s", all_features_response)
52
+ all_features_response = client.all_features(user)
53
+ logger.info(f"All features:\n{all_features_response}")
54
+
55
+ # Post a custom event to DevCycle for user
56
+ event = DevCycleEvent(
57
+ type=EventType.CustomEvent,
58
+ target="some.variable.key",
59
+ date=datetime.datetime.now(),
60
+ )
61
+ client.track(user, event)
55
62
 
56
- # Post events to DevCycle for user
57
- track_response = dvc.track(user, event)
58
- logger.info(track_response)
59
63
  except Exception as e:
60
- logger.exception("Exception when calling Devcycle API: %s\n" % e)
64
+ logger.exception(f"Exception when calling Devcycle API: {e}\n")
61
65
 
62
66
 
63
67
  if __name__ == "__main__":
@@ -0,0 +1,74 @@
1
+ import logging
2
+ import datetime
3
+ import os
4
+ import time
5
+
6
+ from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
7
+ from devcycle_python_sdk.models.user import DevCycleUser
8
+ from devcycle_python_sdk.models.event import DevCycleEvent, EventType
9
+
10
+ VARIABLE_KEY = "test-boolean-variable"
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def main():
16
+ """
17
+ Sample usage of the Python Server SDK using Local Bucketing.
18
+ For a Django specific sample app, please see https://github.com/DevCycleHQ/python-django-example-app/
19
+ """
20
+ logging.basicConfig(level="INFO", format="%(levelname)s: %(message)s")
21
+
22
+ # create an instance of the DevCycle Client object
23
+ server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
24
+ options = DevCycleLocalOptions()
25
+ client = DevCycleLocalClient(server_sdk_key, options)
26
+
27
+ # Wait for DevCycle to initialize and load the configuration
28
+ while not client.is_initialized():
29
+ logger.info("Waiting for DevCycle to initialize...")
30
+ time.sleep(1)
31
+
32
+ user = DevCycleUser(user_id="test-1234", email="test-user@domain.com", country="US")
33
+
34
+ # Use variable_value to access the value of a variable directly
35
+ if client.variable_value(user, VARIABLE_KEY, False):
36
+ logger.info(f"Variable {VARIABLE_KEY} is enabled")
37
+ else:
38
+ logger.info(f"Variable {VARIABLE_KEY} is not enabled")
39
+
40
+ # DevCycle handles missing or wrongly typed variables by returning the default value
41
+ # You can check this explicitly by using the variable method
42
+ variable = client.variable(user, VARIABLE_KEY + "-does-not-exist", False)
43
+ if variable.isDefaulted:
44
+ logger.info(f"Variable {variable.key} is defaulted to {variable.value}")
45
+
46
+ try:
47
+ # Get all variables by key for user data
48
+ all_variables_response = client.all_variables(user)
49
+ logger.info(f"All variables:\n{all_variables_response}")
50
+
51
+ if VARIABLE_KEY not in all_variables_response:
52
+ logger.warning(
53
+ f"Variable {VARIABLE_KEY} does not exist - create it in the dashboard for this example"
54
+ )
55
+
56
+ # Get all features by key for user data
57
+ all_features_response = client.all_features(user)
58
+ logger.info(f"All features:\n{all_features_response}")
59
+
60
+ # Post a custom event to DevCycle for user
61
+ event = DevCycleEvent(
62
+ type=EventType.CustomEvent,
63
+ target="some.variable.key",
64
+ date=datetime.datetime.now(),
65
+ )
66
+ client.track(user, event)
67
+ except Exception as e:
68
+ logger.exception(f"Exception when calling DevCycle API: {e}")
69
+ finally:
70
+ client.close()
71
+
72
+
73
+ if __name__ == "__main__":
74
+ main()
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import logging
2
3
  import requests
3
4
  import responses
@@ -11,10 +12,10 @@ from devcycle_python_sdk.exceptions import (
11
12
  CloudClientUnauthorizedError,
12
13
  NotFoundError,
13
14
  )
14
- from devcycle_python_sdk.dvc_options import DevCycleCloudOptions
15
- from devcycle_python_sdk.models.event import Event
15
+ from devcycle_python_sdk.options import DevCycleCloudOptions
16
+ from devcycle_python_sdk.models.event import DevCycleEvent
16
17
  from devcycle_python_sdk.models.feature import Feature
17
- from devcycle_python_sdk.models.user import User
18
+ from devcycle_python_sdk.models.user import DevCycleUser
18
19
  from devcycle_python_sdk.models.variable import Variable
19
20
 
20
21
  logger = logging.getLogger(__name__)
@@ -25,7 +26,7 @@ class BucketingClientTest(unittest.TestCase):
25
26
  sdk_key = "dvc_server_" + str(uuid.uuid4())
26
27
  options = DevCycleCloudOptions(retry_delay=0)
27
28
  self.test_client = BucketingAPIClient(sdk_key, options)
28
- self.test_user = User(user_id="test_user_id")
29
+ self.test_user = DevCycleUser(user_id="test_user_id")
29
30
 
30
31
  @responses.activate
31
32
  def test_variable(self):
@@ -216,12 +217,16 @@ class BucketingClientTest(unittest.TestCase):
216
217
  result = self.test_client.track(
217
218
  self.test_user,
218
219
  events=[
219
- Event(
220
+ DevCycleEvent(
220
221
  type="sample-event",
221
222
  )
222
223
  ],
223
224
  )
224
225
  self.assertEqual(result, "success")
226
+ data = json.loads(responses.calls[0].request.body)
227
+ self.assertTrue(type(data["events"][0]["date"]) == int)
228
+ self.assertEqual(len(data["events"]), 1)
229
+ self.assertEqual(data["events"][0]["type"], "sample-event")
225
230
 
226
231
 
227
232
  if __name__ == "__main__":
@@ -0,0 +1,133 @@
1
+ import logging
2
+ import unittest
3
+ import uuid
4
+ from os.path import join
5
+ from http import HTTPStatus
6
+
7
+ import requests
8
+ import responses
9
+ from responses.registries import OrderedRegistry
10
+
11
+ from devcycle_python_sdk.api.config_client import ConfigAPIClient
12
+ from devcycle_python_sdk.options import DevCycleLocalOptions
13
+ from devcycle_python_sdk.exceptions import (
14
+ APIClientError,
15
+ APIClientUnauthorizedError,
16
+ NotFoundError,
17
+ )
18
+ from test.fixture.data import small_config_json
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ConfigAPIClientTest(unittest.TestCase):
24
+ def setUp(self) -> None:
25
+ self.sdk_key = "dvc_server_" + str(uuid.uuid4())
26
+ self.config_url = (
27
+ join(
28
+ "https://config-cdn.devcycle.com/",
29
+ "config",
30
+ "v1",
31
+ "server",
32
+ self.sdk_key,
33
+ )
34
+ + ".json"
35
+ )
36
+
37
+ options = DevCycleLocalOptions(config_retry_delay_ms=0)
38
+ self.test_client = ConfigAPIClient(self.sdk_key, options)
39
+ self.test_etag = str(uuid.uuid4())
40
+ self.test_config_json: dict = small_config_json()
41
+
42
+ @responses.activate
43
+ def test_get_config(self):
44
+ new_etag = str(uuid.uuid4())
45
+ responses.add(
46
+ responses.GET,
47
+ self.config_url,
48
+ headers={"ETag": new_etag},
49
+ json=self.test_config_json,
50
+ )
51
+ result, etag = self.test_client.get_config(config_etag=self.test_etag)
52
+ self.assertDictEqual(result, self.test_config_json)
53
+ self.assertEqual(etag, new_etag)
54
+
55
+ @responses.activate(registry=OrderedRegistry)
56
+ def test_get_config_retries(self):
57
+ responses.add(
58
+ responses.GET,
59
+ self.config_url,
60
+ status=500,
61
+ )
62
+ responses.add(
63
+ responses.GET,
64
+ self.config_url,
65
+ headers={"ETag": self.test_etag},
66
+ json=self.test_config_json,
67
+ )
68
+ result, etag = self.test_client.get_config(config_etag=self.test_etag)
69
+
70
+ self.assertDictEqual(result, self.test_config_json)
71
+ self.assertEqual(etag, self.test_etag)
72
+
73
+ @responses.activate(registry=OrderedRegistry)
74
+ def test_get_config_retries_network_error(self):
75
+ responses.add(
76
+ responses.GET,
77
+ self.config_url,
78
+ body=requests.exceptions.ConnectionError("Network Error"),
79
+ )
80
+ responses.add(
81
+ responses.GET,
82
+ self.config_url,
83
+ headers={"ETag": self.test_etag},
84
+ json=self.test_config_json,
85
+ )
86
+ result, etag = self.test_client.get_config(config_etag=self.test_etag)
87
+ self.assertDictEqual(result, self.test_config_json)
88
+ self.assertEqual(etag, self.test_etag)
89
+
90
+ @responses.activate(registry=OrderedRegistry)
91
+ def test_get_config_retries_exceeded(self):
92
+ for i in range(2):
93
+ responses.add(
94
+ responses.GET,
95
+ self.config_url,
96
+ status=HTTPStatus.INTERNAL_SERVER_ERROR,
97
+ )
98
+ with self.assertRaises(APIClientError):
99
+ self.test_client.get_config(config_etag=self.test_etag)
100
+
101
+ @responses.activate
102
+ def test_get_config_not_found(self):
103
+ for i in range(2):
104
+ responses.add(
105
+ responses.GET,
106
+ self.config_url,
107
+ status=HTTPStatus.NOT_FOUND,
108
+ )
109
+ with self.assertRaises(NotFoundError):
110
+ self.test_client.get_config(config_etag=self.test_etag)
111
+
112
+ @responses.activate
113
+ def test_get_config_unauthorized(self):
114
+ for i in range(2):
115
+ responses.add(
116
+ responses.GET,
117
+ self.config_url,
118
+ status=HTTPStatus.UNAUTHORIZED,
119
+ )
120
+ with self.assertRaises(APIClientUnauthorizedError):
121
+ self.test_client.get_config(config_etag=self.test_etag)
122
+
123
+ @responses.activate
124
+ def test_get_config_not_modified(self):
125
+ responses.add(
126
+ responses.GET,
127
+ self.config_url,
128
+ status=HTTPStatus.NOT_MODIFIED,
129
+ )
130
+
131
+ new_config, new_etag = self.test_client.get_config(config_etag=self.test_etag)
132
+ self.assertIsNone(new_config)
133
+ self.assertEqual(new_etag, self.test_etag)
@@ -0,0 +1,97 @@
1
+ import logging
2
+ import unittest
3
+ import uuid
4
+ from http import HTTPStatus
5
+
6
+ import responses
7
+ from responses.registries import OrderedRegistry
8
+
9
+ from devcycle_python_sdk.api.event_client import EventAPIClient
10
+ from devcycle_python_sdk.options import DevCycleLocalOptions
11
+ from devcycle_python_sdk.exceptions import (
12
+ APIClientError,
13
+ APIClientUnauthorizedError,
14
+ NotFoundError,
15
+ )
16
+ from devcycle_python_sdk.models.event import (
17
+ UserEventsBatchRecord,
18
+ RequestEvent,
19
+ EventType,
20
+ )
21
+ from devcycle_python_sdk.models.user import DevCycleUser
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class EventAPIClientTest(unittest.TestCase):
27
+ def setUp(self) -> None:
28
+ self.sdk_key = "dvc_server_" + str(uuid.uuid4())
29
+ options = DevCycleLocalOptions(events_api_uri="http://localhost:8080")
30
+
31
+ self.test_client = EventAPIClient(self.sdk_key, options)
32
+ self.test_batch_url = "http://localhost:8080/v1/events/batch"
33
+
34
+ self.test_user = DevCycleUser(user_id="123")
35
+ events = [
36
+ RequestEvent(
37
+ type=EventType.VariableDefaulted,
38
+ user_id="123",
39
+ date="2023-06-27T12:50:19.871Z",
40
+ clientDate="2023-06-27T12:50:19.871Z",
41
+ )
42
+ ]
43
+ self.test_batch = [UserEventsBatchRecord(user=self.test_user, events=events)]
44
+
45
+ @responses.activate
46
+ def test_publish_events(self):
47
+ responses.add(
48
+ responses.POST,
49
+ self.test_batch_url,
50
+ json={"message": "Successfully received 1 event batches."},
51
+ )
52
+
53
+ message = self.test_client.publish_events(self.test_batch)
54
+ self.assertEqual(message, "Successfully received 1 event batches.")
55
+
56
+ @responses.activate
57
+ def test_publish_events_empty_batch(self):
58
+ responses.add(
59
+ responses.POST,
60
+ self.test_batch_url,
61
+ json={"message": "Successfully received 1 event batches."},
62
+ )
63
+ message = self.test_client.publish_events([])
64
+ self.assertEqual(message, "Successfully received 1 event batches.")
65
+
66
+ @responses.activate(registry=OrderedRegistry)
67
+ def test_publish_events_retries_exceeded(self):
68
+ for i in range(2):
69
+ responses.add(
70
+ responses.POST,
71
+ self.test_batch_url,
72
+ status=HTTPStatus.INTERNAL_SERVER_ERROR,
73
+ )
74
+ with self.assertRaises(APIClientError):
75
+ self.test_client.publish_events(self.test_batch)
76
+
77
+ @responses.activate
78
+ def test_publish_events_not_found(self):
79
+ for i in range(2):
80
+ responses.add(
81
+ responses.POST,
82
+ self.test_batch_url,
83
+ status=HTTPStatus.NOT_FOUND,
84
+ )
85
+ with self.assertRaises(NotFoundError):
86
+ self.test_client.publish_events(self.test_batch)
87
+
88
+ @responses.activate
89
+ def test_publish_events_unauthorized(self):
90
+ for i in range(2):
91
+ responses.add(
92
+ responses.POST,
93
+ self.test_batch_url,
94
+ status=HTTPStatus.UNAUTHORIZED,
95
+ )
96
+ with self.assertRaises(APIClientUnauthorizedError):
97
+ self.test_client.publish_events(self.test_batch)