devcycle-python-server-sdk 3.12.0__py3-none-any.whl → 3.13.1__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 (32) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/api/local_bucketing.py +19 -4
  3. devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  4. devcycle_python_sdk/cloud_client.py +3 -3
  5. devcycle_python_sdk/local_client.py +13 -5
  6. devcycle_python_sdk/managers/config_manager.py +4 -0
  7. devcycle_python_sdk/managers/eval_hooks_manager.py +15 -4
  8. devcycle_python_sdk/managers/event_queue_manager.py +0 -1
  9. devcycle_python_sdk/models/config_metadata.py +40 -0
  10. devcycle_python_sdk/models/environment_metadata.py +26 -0
  11. devcycle_python_sdk/models/eval_hook.py +5 -2
  12. devcycle_python_sdk/models/eval_hook_context.py +23 -5
  13. devcycle_python_sdk/models/eval_reason.py +0 -1
  14. devcycle_python_sdk/models/project_metadata.py +24 -0
  15. devcycle_python_sdk/models/user.py +3 -3
  16. devcycle_python_sdk/models/variable_metadata.py +22 -0
  17. devcycle_python_sdk/open_feature_provider/provider.py +7 -5
  18. devcycle_python_sdk/options.py +1 -1
  19. devcycle_python_sdk/protobuf/utils.py +9 -3
  20. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +34 -42
  21. devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +11 -11
  22. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.1.dist-info}/METADATA +2 -2
  23. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.1.dist-info}/RECORD +32 -28
  24. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.1.dist-info}/licenses/LICENSE +1 -1
  25. example/openfeature_example.py +175 -14
  26. test/api/test_local_bucketing.py +6 -3
  27. test/models/test_user.py +0 -1
  28. test/openfeature_test/test_provider.py +0 -1
  29. test/test_cloud_client.py +41 -6
  30. test/test_local_client.py +53 -4
  31. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.1.dist-info}/WHEEL +0 -0
  32. {devcycle_python_server_sdk-3.12.0.dist-info → devcycle_python_server_sdk-3.13.1.dist-info}/top_level.txt +0 -0
@@ -7,14 +7,14 @@ from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Opti
7
7
  DESCRIPTOR: _descriptor.FileDescriptor
8
8
 
9
9
  class VariableType_PB(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
10
- __slots__ = ()
10
+ __slots__ = []
11
11
  Boolean: _ClassVar[VariableType_PB]
12
12
  Number: _ClassVar[VariableType_PB]
13
13
  String: _ClassVar[VariableType_PB]
14
14
  JSON: _ClassVar[VariableType_PB]
15
15
 
16
16
  class CustomDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
17
- __slots__ = ()
17
+ __slots__ = []
18
18
  Bool: _ClassVar[CustomDataType]
19
19
  Num: _ClassVar[CustomDataType]
20
20
  Str: _ClassVar[CustomDataType]
@@ -29,7 +29,7 @@ Str: CustomDataType
29
29
  Null: CustomDataType
30
30
 
31
31
  class NullableString(_message.Message):
32
- __slots__ = ("value", "isNull")
32
+ __slots__ = ["value", "isNull"]
33
33
  VALUE_FIELD_NUMBER: _ClassVar[int]
34
34
  ISNULL_FIELD_NUMBER: _ClassVar[int]
35
35
  value: str
@@ -37,7 +37,7 @@ class NullableString(_message.Message):
37
37
  def __init__(self, value: _Optional[str] = ..., isNull: bool = ...) -> None: ...
38
38
 
39
39
  class NullableDouble(_message.Message):
40
- __slots__ = ("value", "isNull")
40
+ __slots__ = ["value", "isNull"]
41
41
  VALUE_FIELD_NUMBER: _ClassVar[int]
42
42
  ISNULL_FIELD_NUMBER: _ClassVar[int]
43
43
  value: float
@@ -45,7 +45,7 @@ class NullableDouble(_message.Message):
45
45
  def __init__(self, value: _Optional[float] = ..., isNull: bool = ...) -> None: ...
46
46
 
47
47
  class CustomDataValue(_message.Message):
48
- __slots__ = ("type", "boolValue", "doubleValue", "stringValue")
48
+ __slots__ = ["type", "boolValue", "doubleValue", "stringValue"]
49
49
  TYPE_FIELD_NUMBER: _ClassVar[int]
50
50
  BOOLVALUE_FIELD_NUMBER: _ClassVar[int]
51
51
  DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int]
@@ -57,9 +57,9 @@ class CustomDataValue(_message.Message):
57
57
  def __init__(self, type: _Optional[_Union[CustomDataType, str]] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ...) -> None: ...
58
58
 
59
59
  class NullableCustomData(_message.Message):
60
- __slots__ = ("value", "isNull")
60
+ __slots__ = ["value", "isNull"]
61
61
  class ValueEntry(_message.Message):
62
- __slots__ = ("key", "value")
62
+ __slots__ = ["key", "value"]
63
63
  KEY_FIELD_NUMBER: _ClassVar[int]
64
64
  VALUE_FIELD_NUMBER: _ClassVar[int]
65
65
  key: str
@@ -72,7 +72,7 @@ class NullableCustomData(_message.Message):
72
72
  def __init__(self, value: _Optional[_Mapping[str, CustomDataValue]] = ..., isNull: bool = ...) -> None: ...
73
73
 
74
74
  class VariableForUserParams_PB(_message.Message):
75
- __slots__ = ("sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent")
75
+ __slots__ = ["sdkKey", "variableKey", "variableType", "user", "shouldTrackEvent"]
76
76
  SDKKEY_FIELD_NUMBER: _ClassVar[int]
77
77
  VARIABLEKEY_FIELD_NUMBER: _ClassVar[int]
78
78
  VARIABLETYPE_FIELD_NUMBER: _ClassVar[int]
@@ -86,7 +86,7 @@ class VariableForUserParams_PB(_message.Message):
86
86
  def __init__(self, sdkKey: _Optional[str] = ..., variableKey: _Optional[str] = ..., variableType: _Optional[_Union[VariableType_PB, str]] = ..., user: _Optional[_Union[DVCUser_PB, _Mapping]] = ..., shouldTrackEvent: bool = ...) -> None: ...
87
87
 
88
88
  class DVCUser_PB(_message.Message):
89
- __slots__ = ("user_id", "email", "name", "language", "country", "appBuild", "appVersion", "deviceModel", "customData", "privateCustomData")
89
+ __slots__ = ["user_id", "email", "name", "language", "country", "appBuild", "appVersion", "deviceModel", "customData", "privateCustomData"]
90
90
  USER_ID_FIELD_NUMBER: _ClassVar[int]
91
91
  EMAIL_FIELD_NUMBER: _ClassVar[int]
92
92
  NAME_FIELD_NUMBER: _ClassVar[int]
@@ -110,7 +110,7 @@ class DVCUser_PB(_message.Message):
110
110
  def __init__(self, user_id: _Optional[str] = ..., email: _Optional[_Union[NullableString, _Mapping]] = ..., name: _Optional[_Union[NullableString, _Mapping]] = ..., language: _Optional[_Union[NullableString, _Mapping]] = ..., country: _Optional[_Union[NullableString, _Mapping]] = ..., appBuild: _Optional[_Union[NullableDouble, _Mapping]] = ..., appVersion: _Optional[_Union[NullableString, _Mapping]] = ..., deviceModel: _Optional[_Union[NullableString, _Mapping]] = ..., customData: _Optional[_Union[NullableCustomData, _Mapping]] = ..., privateCustomData: _Optional[_Union[NullableCustomData, _Mapping]] = ...) -> None: ...
111
111
 
112
112
  class SDKVariable_PB(_message.Message):
113
- __slots__ = ("_id", "type", "key", "boolValue", "doubleValue", "stringValue", "evalReason", "_feature", "eval")
113
+ __slots__ = ["_id", "type", "key", "boolValue", "doubleValue", "stringValue", "evalReason", "_feature", "eval"]
114
114
  _ID_FIELD_NUMBER: _ClassVar[int]
115
115
  TYPE_FIELD_NUMBER: _ClassVar[int]
116
116
  KEY_FIELD_NUMBER: _ClassVar[int]
@@ -132,7 +132,7 @@ class SDKVariable_PB(_message.Message):
132
132
  def __init__(self, _id: _Optional[str] = ..., type: _Optional[_Union[VariableType_PB, str]] = ..., key: _Optional[str] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ..., evalReason: _Optional[_Union[NullableString, _Mapping]] = ..., _feature: _Optional[_Union[NullableString, _Mapping]] = ..., eval: _Optional[_Union[EvalReason_PB, _Mapping]] = ...) -> None: ...
133
133
 
134
134
  class EvalReason_PB(_message.Message):
135
- __slots__ = ("reason", "details", "target_id")
135
+ __slots__ = ["reason", "details", "target_id"]
136
136
  REASON_FIELD_NUMBER: _ClassVar[int]
137
137
  DETAILS_FIELD_NUMBER: _ClassVar[int]
138
138
  TARGET_ID_FIELD_NUMBER: _ClassVar[int]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devcycle-python-server-sdk
3
- Version: 3.12.0
3
+ Version: 3.13.1
4
4
  Summary: DevCycle Python SDK
5
5
  Home-page: https://github.com/devcycleHQ/python-server-sdk
6
6
  Author-email:
@@ -12,7 +12,7 @@ Requires-Dist: urllib3>=1.15.1
12
12
  Requires-Dist: requests>=2.32
13
13
  Requires-Dist: wasmtime~=30.0.0
14
14
  Requires-Dist: protobuf>=4.23.3
15
- Requires-Dist: openfeature-sdk>=0.8.0
15
+ Requires-Dist: openfeature-sdk~=0.8.0
16
16
  Requires-Dist: launchdarkly-eventsource>=1.2.1
17
17
  Requires-Dist: responses>=0.23.1
18
18
  Dynamic: description
@@ -1,56 +1,60 @@
1
- devcycle_python_sdk/VERSION.txt,sha256=TuPL9BzJKZqNU-jwsDtquUrxVYO4LjhqEL1HqlHKDZU,7
1
+ devcycle_python_sdk/VERSION.txt,sha256=d1t-sfDGOHao5o2Fv5VaJziw754bHVxGJ49Qa9H8BMo,7
2
2
  devcycle_python_sdk/__init__.py,sha256=30TcYb9PbvVi97gwk16omCGG1UoUI9lmmU7xpvxNMvc,336
3
- devcycle_python_sdk/bucketing-lib.release.wasm,sha256=wpbXlOj_TVIO2OcOD781sW5ICmGsYe9pvxG6DwYw6FY,223167
4
- devcycle_python_sdk/cloud_client.py,sha256=LMKB6gEuQNn5YePUPO0t1QIAUXot4OyshSUlUbuu07U,9413
3
+ devcycle_python_sdk/bucketing-lib.release.wasm,sha256=6iooyHRR-GBfrBH0_mleWyaNHr9uM_nVPS-IXGROGCY,224670
4
+ devcycle_python_sdk/cloud_client.py,sha256=dR2ye29Up699hO1cdrAWeoXIOBPVoSvQ4aKe2gSBRno,9414
5
5
  devcycle_python_sdk/devcycle_client.py,sha256=OqNza7OXOSRmMQq2kmyivljmH1GyN4dTiz0EhvDvt8w,1069
6
6
  devcycle_python_sdk/exceptions.py,sha256=Et7fS8pEFbHkFY86T2pJoEUstwjh4_rzhJxVH8fzAns,1053
7
- devcycle_python_sdk/local_client.py,sha256=obZlHBJ9MS79sntdpPb4JTwC29e58Q5PzzEtei3m9F0,11684
8
- devcycle_python_sdk/options.py,sha256=W3vNZfV6QNnQyaOuRf6Z5cK6ut6fWtlGqlUfhzWX0_c,5253
7
+ devcycle_python_sdk/local_client.py,sha256=HbHIGLeIw9fHOSwp7RJ8ozIO_IqQ23bzSoGoGFLp0Kk,12083
8
+ devcycle_python_sdk/options.py,sha256=DRvr1IQRW3BDHl81TvORfCtt0qVW42c8PoYjMMPkzwQ,5252
9
9
  devcycle_python_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  devcycle_python_sdk/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
11
  devcycle_python_sdk/api/backoff.py,sha256=F6WYCloaZD9CjNu6mKBMq7K20S8-i2WVU6X05ZRoawE,303
12
12
  devcycle_python_sdk/api/bucketing_client.py,sha256=BRQWtTnIyLdWoFT7sPHPGg8kstDtW-jx1N4og3_SEgI,5565
13
13
  devcycle_python_sdk/api/config_client.py,sha256=73aQAq130rTHpQrVDX6n_DwFKz0WuKxRoLDiE3vSoAM,5077
14
14
  devcycle_python_sdk/api/event_client.py,sha256=WxkuV6geEC9MgG4FN36HeEQQa22ilq2mkOVMZkpiEk4,3356
15
- devcycle_python_sdk/api/local_bucketing.py,sha256=IXOP-680OmaO1XoX31YwY8FychAD5aqeyFFGe9SE_u4,15949
15
+ devcycle_python_sdk/api/local_bucketing.py,sha256=kXggXhv2PDQxDoLseuJwWbge8EgeqL0FWTmEIn8TUQc,16704
16
16
  devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- devcycle_python_sdk/managers/config_manager.py,sha256=AIGY9jXEhUzhEeaKIA1P5F2q6GaclrezAoGHgYU4dLQ,5634
18
- devcycle_python_sdk/managers/eval_hooks_manager.py,sha256=skOGK_hyZNpgkdeNXTTa-d1GVRITjoi8T6dO5I_Cqfk,2679
19
- devcycle_python_sdk/managers/event_queue_manager.py,sha256=uPy1vO6OAisZjUNAhWELE_g17alTiOcPv42z-WwkGck,8110
17
+ devcycle_python_sdk/managers/config_manager.py,sha256=QsJPMnf0wgNND3Yqhv125ckta_YFOMUb8I8pFq64tOE,5827
18
+ devcycle_python_sdk/managers/eval_hooks_manager.py,sha256=YOLj0IvNFqDbWvPA2aOqT15yKkwsdnZT9MQ7MHTv6M4,2963
19
+ devcycle_python_sdk/managers/event_queue_manager.py,sha256=8tqPZDD0-AWmz_NcWJBb80OIMzNBpDgbafpFefiOOmk,8109
20
20
  devcycle_python_sdk/managers/sse_manager.py,sha256=hSGpnZ6gNbQXGotqNxjogoB82znAyMMFhqdlqeCc5bI,2586
21
21
  devcycle_python_sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  devcycle_python_sdk/models/bucketed_config.py,sha256=qTfhIViX7m2WS2oDBBCmKKzPsJG6DEav3zCEXwP27nA,3980
23
+ devcycle_python_sdk/models/config_metadata.py,sha256=HHhw2ncHOUAC3SH-IokVP4GrNLbflcGE9p7a2ZSuksA,1413
24
+ devcycle_python_sdk/models/environment_metadata.py,sha256=Zal9Ly15GaMcUBcD7CujNvtl5w_qfL8TQtLSRBpTHMo,629
23
25
  devcycle_python_sdk/models/error_response.py,sha256=r_dRUDAbral9UGaMYcnBsZOr9Hmkyi1SQbhGmZilgpA,442
24
- devcycle_python_sdk/models/eval_hook.py,sha256=_mDoEa8gkzMq-XpFRZrcXu_ldtpUzv5tlh-PghLpMMI,590
25
- devcycle_python_sdk/models/eval_hook_context.py,sha256=uBa8t0XxbcPBYAewNPFC2902qCjGhVoqUzZOZbGZ3n0,267
26
- devcycle_python_sdk/models/eval_reason.py,sha256=nMXcbR1DM8eriCQ1NNnIRQKkhJSYAbY8_9GgBfj7P8k,943
26
+ devcycle_python_sdk/models/eval_hook.py,sha256=k9w-AafvR-X7i8uf5nVuNEbD_PRS8QQmxW_90KVgGHA,742
27
+ devcycle_python_sdk/models/eval_hook_context.py,sha256=FYfMfXgtlW1O70khIcWF87yie2SWW4iWH2ehPex9paM,928
28
+ devcycle_python_sdk/models/eval_reason.py,sha256=obBjIWt8ZUh6knpVGsmCfWOCj4c7DXdH7K-FY-pchsU,901
27
29
  devcycle_python_sdk/models/event.py,sha256=Sj3eEopuPp8QwQl_Oz8E6Y9Ph6nLG0HxU7eR7xSbA7g,3722
28
30
  devcycle_python_sdk/models/feature.py,sha256=m3wxxIjC5jxhKpPLI4RcsBaNLuy-zsMqoAgc8ySPe0I,852
29
31
  devcycle_python_sdk/models/platform_data.py,sha256=nyQsgI6pcTd1FL3Y2ACmpvTYb08R_UtWul25OWZstps,849
30
- devcycle_python_sdk/models/user.py,sha256=mhEk-mnKJRg_tydpBlAVhGDj9rDCacJ-jNwiGeSpZiE,7027
32
+ devcycle_python_sdk/models/project_metadata.py,sha256=zRoE_oy_N3isqfrA5hAZ7S-7i5QBTSu9OGigFAvTqi8,602
33
+ devcycle_python_sdk/models/user.py,sha256=DgSP4iq9IxEOOBmYIKuCLXi6lVqYkt01_Wm0Z4-QWBo,7055
31
34
  devcycle_python_sdk/models/variable.py,sha256=NXubhYWXYRAUEh4kDe2Tu9T5QnZmQ3roYVPbP-u2H3s,3030
35
+ devcycle_python_sdk/models/variable_metadata.py,sha256=oeH5fxDFLgAyc_B1RuOYRp4w68lTaBxTPvNgnXz7alE,583
32
36
  devcycle_python_sdk/open_feature_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- devcycle_python_sdk/open_feature_provider/provider.py,sha256=l-psKU5m6jSBE53VwX3fBBas-TztekFDlfYTyTJQm6c,5558
37
+ devcycle_python_sdk/open_feature_provider/provider.py,sha256=Iby2X2YuYgR6-W00NOWE4K7YdSUHODeqjm4Eog7pACc,5693
34
38
  devcycle_python_sdk/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- devcycle_python_sdk/protobuf/utils.py,sha256=6cBL8GZ5nCZw6JoO7lAdgCj9wM1u0lp6QFQMnGE1fiM,5414
36
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=yawFsQ7m-6OB7DxrjXo2FLLkHhiTv6lDnjubhjAgw_8,5198
37
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi,sha256=tfN0zBapUPA9WGqUG5lU5XXgH_BsHTt5mgXKGsyS4-I,6623
39
+ devcycle_python_sdk/protobuf/utils.py,sha256=SwsXH5QGAfbSn1FvgwO3x4W8uTYLsL1r3E-P5nDD4Nw,5528
40
+ devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=SIFCo4AbjtlWQh2DYxkrWCR85PQajA4PIofXoRe4Gds,4956
41
+ devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi,sha256=OU4ORhjnbXy0AqdkxyARxg4JXGTPDuDzBW2W9RGdcMs,6623
38
42
  devcycle_python_sdk/util/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
39
43
  devcycle_python_sdk/util/strings.py,sha256=gH0Xm3gIXNqn6hczKPk10Jn5_9knuD7OB5MrB7bqztw,252
40
44
  devcycle_python_sdk/util/version.py,sha256=Cz7Us_EuXP-T8GI47g2WHYdBJj_zKo-5y545IDHJAjI,253
41
- devcycle_python_server_sdk-3.12.0.dist-info/licenses/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
45
+ devcycle_python_server_sdk-3.13.1.dist-info/licenses/LICENSE,sha256=RlvZsEX5o__wLAA-VE7mG5hzTPjnFzPJKNdXXKvZ854,1071
42
46
  example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
47
  example/cloud_client_example.py,sha256=YD8ZsTv3dPYfbFUKraRpRT0VsJGrPwjm8ZCuA5H54yI,2463
44
48
  example/local_bucketing_client_example.py,sha256=TaIfy3VnW9fXii9NPYs5UOuP4fLKruXMjnZApveqzl4,2796
45
- example/openfeature_example.py,sha256=kO6F-Xk20l6CPiMfIxSRreshEGmHMFHRg3zbeLXa3AI,2410
49
+ example/openfeature_example.py,sha256=8rdEtG_RiEbCwbQQWcCXtfZURz2Z393WoczyperkNug,8105
46
50
  test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
- test/test_cloud_client.py,sha256=oqOWh4aa07ObJOJeEjXMon0O2jPW_lYZMVInbFR-uzI,14586
48
- test/test_local_client.py,sha256=d0D0h2vqSiZItiqGXrlFXRRmuZG-TNg8MT84dY6w0Ks,16250
51
+ test/test_cloud_client.py,sha256=CoK4xfHPMGyJ4hXrdI98e5f4zEL51vx31u_xmC3FEtc,15957
52
+ test/test_local_client.py,sha256=yg19ddGmg9t1TfjT-4EgHp0cjfxHjfJZOGIFM8mSNX8,18078
49
53
  test/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
54
  test/api/test_bucketing_client.py,sha256=M2mjACowLvbsIjXdP-w8-fwY02xqv1X1r_h9v-pXa7A,7739
51
55
  test/api/test_config_client.py,sha256=3BwMm4y-4NQcdW2DDGppoJLIF-ffgjlUJti2Qjw1D1M,5129
52
56
  test/api/test_event_client.py,sha256=v4Xoum-PdywfczrfMV1llM5vrIJoqHybzxPLVokh09g,3310
53
- test/api/test_local_bucketing.py,sha256=0cbD8kiRYCVadvU-ldMES-sr7rV9Ld4H1q62HCx4AHU,15970
57
+ test/api/test_local_bucketing.py,sha256=ECouaWpgWrlfEBtTK5OavmYX87XiXKg-9epmzTOCMdE,16126
54
58
  test/fixture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
59
  test/fixture/data.py,sha256=6ReLxWPMhCDbJ5Ex24_iR0PJNjKzPFV1I9j_v0FVbBU,1462
56
60
  test/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -58,15 +62,15 @@ test/managers/test_config_manager.py,sha256=2TUnt8501KM7UlJNH7srIhvC-izehD7nOfqZ
58
62
  test/managers/test_event_queue_manager.py,sha256=z850atk8zAlD7sYaA1OJwgCFcfC0ps4NEk4XUnNCq1U,11629
59
63
  test/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
64
  test/models/test_bucketed_config.py,sha256=8wCEyxaHxY21nl7ZbiFzp-IzZQBZIPIVPPzeREjumQk,858
61
- test/models/test_user.py,sha256=ZUbFUndYrnefyGl8rxZgG3HdHPZs4BosQWimkT7etJc,10540
65
+ test/models/test_user.py,sha256=nuGJvR2KHilRlx5DDEzh1F-lpZ7qT7H88kqPvfcFfM8,10539
62
66
  test/openfeature_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- test/openfeature_test/test_provider.py,sha256=hIzdqqBTulGRODeTcRJUP93v76WEolUC2QRe3DccF3U,10932
67
+ test/openfeature_test/test_provider.py,sha256=vj8eKprkF-psRdy54AtitYoEqBJKhDpfEzEDdhM-3FE,10931
64
68
  test/openfeature_test/test_provider_local_sdk.py,sha256=ExkA1LYLuvW-dD4Bw2BKggzDQ8GpnIp3WyNK4rUZgJQ,5696
65
69
  test/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
70
  test/util/test_strings.py,sha256=T5H5U0af7_ld1yJC8IdDmay_Mx-iJQ5Fi5fbvKcAc7A,862
67
71
  test/util/test_utils.py,sha256=tVBJm_qnqg-4PssLpXIxqlk0JehuiAIPs4zpi2oNVnY,6671
68
72
  test/util/test_version.py,sha256=3Xg0_84m0bdHXz6pxd2Z85Oz9DoUC1T0Ol_WqEH_jac,335
69
- devcycle_python_server_sdk-3.12.0.dist-info/METADATA,sha256=-bA66ayqIdC5UFrsLx0jZ9m4eNSHzcdTuFvA-sY-IQE,723
70
- devcycle_python_server_sdk-3.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
71
- devcycle_python_server_sdk-3.12.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
72
- devcycle_python_server_sdk-3.12.0.dist-info/RECORD,,
73
+ devcycle_python_server_sdk-3.13.1.dist-info/METADATA,sha256=1mU8MQraVdQxnwAxNqGvxsQ6GmpBxsVOEk9O_MpAwqA,723
74
+ devcycle_python_server_sdk-3.13.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
75
+ devcycle_python_server_sdk-3.13.1.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
76
+ devcycle_python_server_sdk-3.13.1.dist-info/RECORD,,
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Taplytics Inc.
3
+ Copyright (c) 2025 Taplytics Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -7,19 +7,32 @@ from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
7
7
  from openfeature import api
8
8
  from openfeature.evaluation_context import EvaluationContext
9
9
 
10
- FLAG_KEY = "test-boolean-variable"
11
-
12
10
  logger = logging.getLogger(__name__)
13
11
 
14
12
 
15
13
  def main():
16
14
  """
17
15
  Sample usage of the DevCycle OpenFeature Provider along with the Python Server SDK using Local Bucketing.
16
+
17
+ This example demonstrates how to use all variable types supported by DevCycle through OpenFeature:
18
+ - Boolean variables
19
+ - String variables
20
+ - Number variables (integer and float)
21
+ - JSON object variables
22
+
23
+ See DEVCYCLE_SETUP.md for instructions on creating the required variables in DevCycle.
18
24
  """
19
25
  logging.basicConfig(level="INFO", format="%(levelname)s: %(message)s")
20
26
 
21
27
  # create an instance of the DevCycle Client object
22
- server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
28
+ server_sdk_key = os.environ.get("DEVCYCLE_SERVER_SDK_KEY")
29
+ if not server_sdk_key:
30
+ logger.error("DEVCYCLE_SERVER_SDK_KEY environment variable is not set")
31
+ logger.error(
32
+ "Please set it with: export DEVCYCLE_SERVER_SDK_KEY='your-sdk-key'"
33
+ )
34
+ exit(1)
35
+
23
36
  devcycle_client = DevCycleLocalClient(server_sdk_key, DevCycleLocalOptions())
24
37
 
25
38
  # Wait for DevCycle to initialize and load the configuration
@@ -32,6 +45,8 @@ def main():
32
45
  logger.error("DevCycle failed to initialize")
33
46
  exit(1)
34
47
 
48
+ logger.info("DevCycle initialized successfully!\n")
49
+
35
50
  # set the provider for OpenFeature
36
51
  api.set_provider(devcycle_client.get_openfeature_provider())
37
52
 
@@ -53,22 +68,168 @@ def main():
53
68
  },
54
69
  )
55
70
 
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")
71
+ logger.info("=" * 60)
72
+ logger.info("Testing Boolean Variable")
73
+ logger.info("=" * 60)
74
+
75
+ # Test Boolean Variable
76
+ boolean_details = open_feature_client.get_boolean_details(
77
+ "test-boolean-variable", False, context
78
+ )
79
+ logger.info("Variable Key: test-boolean-variable")
80
+ logger.info("Value: {boolean_details.value}")
81
+ logger.info("Reason: {boolean_details.reason}")
82
+ if boolean_details.value:
83
+ logger.info("✓ Boolean variable is ENABLED")
59
84
  else:
60
- logger.info(f"Variable {FLAG_KEY} is not enabled")
85
+ logger.info("✗ Boolean variable is DISABLED")
86
+
87
+ logger.info("\n" + "=" * 60)
88
+ logger.info("Testing String Variable")
89
+ logger.info("=" * 60)
61
90
 
62
- # Fetch a JSON object variable
63
- json_object = open_feature_client.get_object_value(
91
+ # Test String Variable
92
+ open_feature_client.get_string_details(
93
+ "test-string-variable", "default string", context
94
+ )
95
+ logger.info("Variable Key: test-string-variable")
96
+ logger.info("Value: {string_details.value}")
97
+ logger.info("Reason: {string_details.reason}")
98
+
99
+ logger.info("\n" + "=" * 60)
100
+ logger.info("Testing Number Variable (Integer)")
101
+ logger.info("=" * 60)
102
+
103
+ # Test Number Variable (Integer)
104
+ open_feature_client.get_integer_details("test-number-variable", 0, context)
105
+ logger.info("Variable Key: test-number-variable")
106
+ logger.info("Value: {integer_details.value}")
107
+ logger.info("Reason: {integer_details.reason}")
108
+
109
+ logger.info("\n" + "=" * 60)
110
+ logger.info("Testing Number Variable (Float)")
111
+ logger.info("=" * 60)
112
+
113
+ # Test Number Variable as Float
114
+ # Note: If the DevCycle variable is an integer, it will be cast to float
115
+ open_feature_client.get_float_value("test-number-variable", 0.0, context)
116
+ logger.info("Variable Key: test-number-variable (as float)")
117
+ logger.info("Value: {float_value}")
118
+
119
+ logger.info("\n" + "=" * 60)
120
+ logger.info("Testing JSON Object Variable")
121
+ logger.info("=" * 60)
122
+
123
+ # Test JSON Object Variable
124
+ open_feature_client.get_object_details(
64
125
  "test-json-variable", {"default": "value"}, context
65
126
  )
66
- logger.info(f"JSON Object Value: {json_object}")
127
+ logger.info("Variable Key: test-json-variable")
128
+ logger.info("Value: {json_details.value}")
129
+ logger.info("Reason: {json_details.reason}")
130
+
131
+ logger.info("\n" + "=" * 60)
132
+ logger.info("Testing Object Variable - Empty Dictionary")
133
+ logger.info("=" * 60)
134
+
135
+ # Test with empty dictionary default (valid per OpenFeature spec)
136
+ open_feature_client.get_object_value("test-json-variable", {}, context)
137
+ logger.info("Variable Key: test-json-variable (with empty default)")
138
+ logger.info("Value: {empty_dict_result}")
139
+
140
+ logger.info("\n" + "=" * 60)
141
+ logger.info("Testing Object Variable - Mixed Types")
142
+ logger.info("=" * 60)
143
+
144
+ # Test with flat dictionary containing mixed primitive types
145
+ # OpenFeature allows string, int, float, bool, and None in flat dictionaries
146
+ mixed_default = {
147
+ "string_key": "hello",
148
+ "int_key": 42,
149
+ "float_key": 3.14,
150
+ "bool_key": True,
151
+ "none_key": None,
152
+ }
153
+ mixed_result = open_feature_client.get_object_value(
154
+ "test-json-variable", mixed_default, context
155
+ )
156
+ logger.info("Variable Key: test-json-variable (with mixed types)")
157
+ logger.info("Value: {mixed_result}")
158
+ logger.info(
159
+ f"Value types: {[(k, type(v).__name__) for k, v in mixed_result.items()]}"
160
+ )
161
+
162
+ logger.info("\n" + "=" * 60)
163
+ logger.info("Testing Object Variable - All String Values")
164
+ logger.info("=" * 60)
165
+
166
+ # Test with all string values
167
+ string_dict_default = {
168
+ "name": "John Doe",
169
+ "email": "john@example.com",
170
+ "status": "active",
171
+ }
172
+ open_feature_client.get_object_value(
173
+ "test-json-variable", string_dict_default, context
174
+ )
175
+ logger.info("Variable Key: test-json-variable (all strings)")
176
+ logger.info("Value: {string_dict_result}")
177
+
178
+ logger.info("\n" + "=" * 60)
179
+ logger.info("Testing Object Variable - Numeric Values")
180
+ logger.info("=" * 60)
181
+
182
+ # Test with numeric values (integers and floats)
183
+ numeric_dict_default = {"count": 100, "percentage": 85.5, "threshold": 0}
184
+ open_feature_client.get_object_value(
185
+ "test-json-variable", numeric_dict_default, context
186
+ )
187
+ logger.info("Variable Key: test-json-variable (numeric)")
188
+ logger.info("Value: {numeric_dict_result}")
189
+
190
+ logger.info("\n" + "=" * 60)
191
+ logger.info("Testing Object Variable - Boolean Flags")
192
+ logger.info("=" * 60)
193
+
194
+ # Test with boolean values
195
+ bool_dict_default = {"feature_a": True, "feature_b": False, "feature_c": True}
196
+ open_feature_client.get_object_value(
197
+ "test-json-variable", bool_dict_default, context
198
+ )
199
+ logger.info("Variable Key: test-json-variable (booleans)")
200
+ logger.info("Value: {bool_dict_result}")
201
+
202
+ logger.info("\n" + "=" * 60)
203
+ logger.info("Testing Object Variable - With None Values")
204
+ logger.info("=" * 60)
205
+
206
+ # Test with None values (valid per OpenFeature spec for flat dictionaries)
207
+ none_dict_default = {
208
+ "optional_field": None,
209
+ "required_field": "value",
210
+ "nullable_count": None,
211
+ }
212
+ open_feature_client.get_object_value(
213
+ "test-json-variable", none_dict_default, context
214
+ )
215
+ logger.info("Variable Key: test-json-variable (with None)")
216
+ logger.info("Value: {none_dict_result}")
217
+
218
+ logger.info("\n" + "=" * 60)
219
+ logger.info("Testing Non-Existent Variable (Should Return Default)")
220
+ logger.info("=" * 60)
221
+
222
+ # Test non-existent variable to demonstrate default handling
223
+ open_feature_client.get_string_details(
224
+ "doesnt-exist", "default fallback value", context
225
+ )
226
+ logger.info("Variable Key: doesnt-exist")
227
+ logger.info("Value: {nonexistent_details.value}")
228
+ logger.info("Reason: {nonexistent_details.reason}")
67
229
 
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}")
230
+ logger.info("\n" + "=" * 60)
231
+ logger.info("All tests completed!")
232
+ logger.info("=" * 60)
72
233
 
73
234
  devcycle_client.close()
74
235
 
@@ -98,7 +98,7 @@ class LocalBucketingTest(unittest.TestCase):
98
98
  self.local_bucketing.set_platform_data(platform_json)
99
99
  self.local_bucketing.init_event_queue(self.client_uuid, "{}")
100
100
  user = DevCycleUser(user_id="test_user_id")
101
- result = self.local_bucketing.get_variable_for_user_protobuf(
101
+ result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
102
102
  user=user, key="string-var", default_value="default value"
103
103
  )
104
104
  self.assertIsNotNone(result)
@@ -106,6 +106,7 @@ class LocalBucketingTest(unittest.TestCase):
106
106
  self.assertEqual(result.type, TypeEnum.STRING)
107
107
  self.assertEqual(result.value, "variationOn")
108
108
  self.assertFalse(result.isDefaulted)
109
+ self.assertIsNotNone(feature_id)
109
110
 
110
111
  def test_get_variable_for_user_protobuf_special_characters(self):
111
112
  self.local_bucketing.store_config(special_character_config())
@@ -113,7 +114,7 @@ class LocalBucketingTest(unittest.TestCase):
113
114
  self.local_bucketing.set_platform_data(platform_json)
114
115
  self.local_bucketing.init_event_queue(self.client_uuid, "{}")
115
116
  user = DevCycleUser(user_id="test_user_id")
116
- result = self.local_bucketing.get_variable_for_user_protobuf(
117
+ result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
117
118
  user=user, key="string-var", default_value="default value"
118
119
  )
119
120
  self.assertIsNotNone(result)
@@ -121,6 +122,7 @@ class LocalBucketingTest(unittest.TestCase):
121
122
  self.assertEqual(result.type, TypeEnum.STRING)
122
123
  self.assertEqual(result.value, "öé 🐍 ¥ variationOn")
123
124
  self.assertFalse(result.isDefaulted)
125
+ self.assertIsNotNone(feature_id)
124
126
 
125
127
  def test_get_variable_for_user_protobuf_type_mismatch(self):
126
128
  self.local_bucketing.store_config(small_config())
@@ -131,10 +133,11 @@ class LocalBucketingTest(unittest.TestCase):
131
133
 
132
134
  # type mismatch is handled inside the WASM and will return
133
135
  # no data if the type is not correct
134
- result = self.local_bucketing.get_variable_for_user_protobuf(
136
+ result, feature_id = self.local_bucketing.get_variable_for_user_protobuf(
135
137
  user=user, key="string-var", default_value=9999
136
138
  )
137
139
  self.assertIsNone(result)
140
+ self.assertIsNone(feature_id)
138
141
 
139
142
  def test_generate_bucketed_config(self):
140
143
  self.local_bucketing.store_config(small_config())
test/models/test_user.py CHANGED
@@ -6,7 +6,6 @@ from devcycle_python_sdk.models.user import DevCycleUser
6
6
  from openfeature.provider import EvaluationContext
7
7
  from openfeature.exception import TargetingKeyMissingError, InvalidContextError
8
8
 
9
-
10
9
  logger = logging.getLogger(__name__)
11
10
 
12
11
 
@@ -21,7 +21,6 @@ from devcycle_python_sdk.open_feature_provider.provider import (
21
21
  DevCycleProvider,
22
22
  )
23
23
 
24
-
25
24
  logger = logging.getLogger(__name__)
26
25
 
27
26
 
test/test_cloud_client.py CHANGED
@@ -5,7 +5,6 @@ import uuid
5
5
  from time import time
6
6
  from unittest.mock import patch
7
7
 
8
-
9
8
  from devcycle_python_sdk import DevCycleCloudClient, DevCycleCloudOptions
10
9
  from devcycle_python_sdk.models.eval_hook import EvalHook
11
10
  from devcycle_python_sdk.models.user import DevCycleUser
@@ -99,7 +98,7 @@ class DevCycleCloudClientTest(unittest.TestCase):
99
98
  self.assertEqual(result.value, "default_value")
100
99
  self.assertTrue(result.isDefaulted)
101
100
  self.assertEqual(result.eval.reason, "DEFAULT")
102
- self.assertEqual(result.eval.details, "Missing Variable")
101
+ self.assertEqual(result.eval.details, "Error")
103
102
 
104
103
  # other exception - return default
105
104
  mock_variable_call.reset_mock()
@@ -307,10 +306,10 @@ class DevCycleCloudClientTest(unittest.TestCase):
307
306
  hook_called["before"] = True
308
307
  return context
309
308
 
310
- def after_hook(context, variable):
309
+ def after_hook(context, variable, variable_metadata):
311
310
  hook_called["after"] = True
312
311
 
313
- def finally_hook(context, variable):
312
+ def finally_hook(context, variable, variable_metadata):
314
313
  hook_called["finally"] = True
315
314
 
316
315
  def error_hook(context, error):
@@ -347,10 +346,10 @@ class DevCycleCloudClientTest(unittest.TestCase):
347
346
  hook_called["before"] = True
348
347
  raise Exception("Before hook failed")
349
348
 
350
- def after_hook(context, variable):
349
+ def after_hook(context, variable, variable_metadata):
351
350
  hook_called["after"] = True
352
351
 
353
- def finally_hook(context, variable):
352
+ def finally_hook(context, variable, variable_metadata):
354
353
  hook_called["finally"] = True
355
354
 
356
355
  def error_hook(context, error):
@@ -370,6 +369,42 @@ class DevCycleCloudClientTest(unittest.TestCase):
370
369
  self.assertTrue(hook_called["finally"])
371
370
  self.assertTrue(hook_called["error"])
372
371
 
372
+ @patch("devcycle_python_sdk.api.bucketing_client.BucketingAPIClient.variable")
373
+ def test_context_has_null_config_metadata(self, mock_variable_call):
374
+ mock_variable_call.return_value = Variable(
375
+ _id="123", key="strKey", value=999, type=TypeEnum.NUMBER
376
+ )
377
+
378
+ context_received = None
379
+
380
+ def before_hook(context):
381
+ nonlocal context_received
382
+ context_received = context
383
+ return context
384
+
385
+ def after_hook(context, variable, variable_metadata):
386
+ pass
387
+
388
+ def finally_hook(context, variable, variable_metadata):
389
+ pass
390
+
391
+ def error_hook(context, error):
392
+ pass
393
+
394
+ self.test_client.add_hook(
395
+ EvalHook(before_hook, after_hook, finally_hook, error_hook)
396
+ )
397
+
398
+ # Test that context has config_metadata field but it's null for cloud client
399
+ variable = self.test_client.variable(self.test_user, "strKey", 42)
400
+
401
+ # Verify the variable evaluation worked
402
+ self.assertIsNotNone(variable)
403
+ self.assertIsNotNone(context_received)
404
+ self.assertTrue(hasattr(context_received, "config_metadata"))
405
+ # Cloud client should have null config_metadata since it's not implemented
406
+ self.assertIsNone(context_received.config_metadata)
407
+
373
408
 
374
409
  if __name__ == "__main__":
375
410
  unittest.main()