devcycle-python-server-sdk 3.3.6__py3-none-any.whl → 3.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devcycle_python_sdk/VERSION.txt +1 -1
- devcycle_python_sdk/__init__.py +1 -0
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/cloud_client.py +19 -2
- devcycle_python_sdk/devcycle_client.py +40 -0
- devcycle_python_sdk/local_client.py +16 -2
- devcycle_python_sdk/models/bucketed_config.py +2 -0
- devcycle_python_sdk/models/user.py +86 -3
- devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
- devcycle_python_sdk/open_feature_provider/provider.py +123 -0
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/METADATA +2 -1
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/RECORD +24 -16
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/WHEEL +1 -1
- example/local_bucketing_client_example.py +1 -1
- example/openfeature_example.py +77 -0
- test/api/test_local_bucketing.py +15 -12
- test/models/test_user.py +188 -0
- test/openfeature_test/__init__.py +0 -0
- test/openfeature_test/test_provider.py +219 -0
- test/openfeature_test/test_provider_local_sdk.py +136 -0
- test/test_local_client.py +10 -0
- test/util/test_version.py +1 -1
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/LICENSE +0 -0
- {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/top_level.txt +0 -0
devcycle_python_sdk/VERSION.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.5.0
|
devcycle_python_sdk/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Simplify imports for the SDK entry point objects
|
|
2
2
|
|
|
3
3
|
from devcycle_python_sdk.options import DevCycleCloudOptions, DevCycleLocalOptions
|
|
4
|
+
from devcycle_python_sdk.devcycle_client import AbstractDevCycleClient
|
|
4
5
|
from devcycle_python_sdk.cloud_client import DevCycleCloudClient
|
|
5
6
|
from devcycle_python_sdk.local_client import DevCycleLocalClient
|
|
Binary file
|
|
@@ -3,7 +3,7 @@ import platform
|
|
|
3
3
|
|
|
4
4
|
from typing import Any, Dict
|
|
5
5
|
|
|
6
|
-
from devcycle_python_sdk import DevCycleCloudOptions
|
|
6
|
+
from devcycle_python_sdk import DevCycleCloudOptions, AbstractDevCycleClient
|
|
7
7
|
from devcycle_python_sdk.api.bucketing_client import BucketingAPIClient
|
|
8
8
|
from devcycle_python_sdk.exceptions import (
|
|
9
9
|
NotFoundError,
|
|
@@ -14,11 +14,18 @@ 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
|
|
17
|
+
from devcycle_python_sdk.open_feature_provider.provider import DevCycleProvider
|
|
18
|
+
|
|
19
|
+
from openfeature.provider import AbstractProvider
|
|
17
20
|
|
|
18
21
|
logger = logging.getLogger(__name__)
|
|
19
22
|
|
|
20
23
|
|
|
21
|
-
class DevCycleCloudClient:
|
|
24
|
+
class DevCycleCloudClient(AbstractDevCycleClient):
|
|
25
|
+
"""
|
|
26
|
+
The DevCycle Python SDK that utilizes the DevCycle Bucketing API for feature and variable evaluation
|
|
27
|
+
"""
|
|
28
|
+
|
|
22
29
|
options: DevCycleCloudOptions
|
|
23
30
|
platform: str
|
|
24
31
|
platform_version: str
|
|
@@ -37,6 +44,13 @@ class DevCycleCloudClient:
|
|
|
37
44
|
self.sdk_version = sdk_version()
|
|
38
45
|
self.sdk_type = "server"
|
|
39
46
|
self.bucketing_api = BucketingAPIClient(sdk_key, self.options)
|
|
47
|
+
self._openfeature_provider = DevCycleProvider(self)
|
|
48
|
+
|
|
49
|
+
def get_sdk_platform(self) -> str:
|
|
50
|
+
return "Cloud"
|
|
51
|
+
|
|
52
|
+
def get_openfeature_provider(self) -> AbstractProvider:
|
|
53
|
+
return self._openfeature_provider
|
|
40
54
|
|
|
41
55
|
def _add_platform_data_to_user(self, user: DevCycleUser) -> DevCycleUser:
|
|
42
56
|
user.platform = self.platform
|
|
@@ -45,6 +59,9 @@ class DevCycleCloudClient:
|
|
|
45
59
|
user.sdkType = self.sdk_type
|
|
46
60
|
return user
|
|
47
61
|
|
|
62
|
+
def is_initialized(self) -> bool:
|
|
63
|
+
return True
|
|
64
|
+
|
|
48
65
|
def variable_value(self, user: DevCycleUser, key: str, default_value: Any) -> Any:
|
|
49
66
|
"""
|
|
50
67
|
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
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
|
|
4
|
+
from devcycle_python_sdk.models.user import DevCycleUser
|
|
5
|
+
from devcycle_python_sdk.models.variable import Variable
|
|
6
|
+
|
|
7
|
+
from openfeature.provider import AbstractProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AbstractDevCycleClient:
|
|
11
|
+
"""
|
|
12
|
+
A common interface for all DevCycle Clients
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def is_initialized(self) -> bool:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def variable(
|
|
21
|
+
self, user: DevCycleUser, key: str, default_value: typing.Any
|
|
22
|
+
) -> Variable:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def variable_value(
|
|
27
|
+
self, user: DevCycleUser, key: str, default_value: typing.Any
|
|
28
|
+
) -> typing.Any:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def get_openfeature_provider(self) -> AbstractProvider:
|
|
33
|
+
"""
|
|
34
|
+
Returns the OpenFeature provider for this client
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_sdk_platform(self) -> str:
|
|
40
|
+
pass
|
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
from numbers import Real
|
|
4
4
|
from typing import Any, Dict, Union
|
|
5
5
|
|
|
6
|
-
from devcycle_python_sdk import DevCycleLocalOptions
|
|
6
|
+
from devcycle_python_sdk import DevCycleLocalOptions, AbstractDevCycleClient
|
|
7
7
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
8
8
|
from devcycle_python_sdk.exceptions import VariableTypeMismatchError
|
|
9
9
|
from devcycle_python_sdk.managers.config_manager import EnvironmentConfigManager
|
|
@@ -14,11 +14,17 @@ from devcycle_python_sdk.models.feature import Feature
|
|
|
14
14
|
from devcycle_python_sdk.models.platform_data import default_platform_data
|
|
15
15
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
16
16
|
from devcycle_python_sdk.models.variable import Variable
|
|
17
|
+
from devcycle_python_sdk.open_feature_provider.provider import DevCycleProvider
|
|
18
|
+
from openfeature.provider import AbstractProvider
|
|
17
19
|
|
|
18
20
|
logger = logging.getLogger(__name__)
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
class DevCycleLocalClient:
|
|
23
|
+
class DevCycleLocalClient(AbstractDevCycleClient):
|
|
24
|
+
"""
|
|
25
|
+
The DevCycle Python SDK that utilizes the local bucketing library for feature and variable evaluation
|
|
26
|
+
"""
|
|
27
|
+
|
|
22
28
|
def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
|
|
23
29
|
_validate_sdk_key(sdk_key)
|
|
24
30
|
self._sdk_key = sdk_key
|
|
@@ -42,6 +48,14 @@ class DevCycleLocalClient:
|
|
|
42
48
|
sdk_key, self.options, self.local_bucketing
|
|
43
49
|
)
|
|
44
50
|
|
|
51
|
+
self._openfeature_provider = DevCycleProvider(self)
|
|
52
|
+
|
|
53
|
+
def get_sdk_platform(self) -> str:
|
|
54
|
+
return "Local"
|
|
55
|
+
|
|
56
|
+
def get_openfeature_provider(self) -> AbstractProvider:
|
|
57
|
+
return self._openfeature_provider
|
|
58
|
+
|
|
45
59
|
def is_initialized(self) -> bool:
|
|
46
60
|
return self.config_manager.is_initialized()
|
|
47
61
|
|
|
@@ -55,6 +55,7 @@ class OptInSettings:
|
|
|
55
55
|
class ProjectSettings:
|
|
56
56
|
edge_db: Optional[EdgeDBSettings]
|
|
57
57
|
opt_in: Optional[OptInSettings]
|
|
58
|
+
disable_passthrough_rollouts: Optional[bool]
|
|
58
59
|
|
|
59
60
|
@classmethod
|
|
60
61
|
def from_json(cls, data: dict) -> "ProjectSettings":
|
|
@@ -63,6 +64,7 @@ class ProjectSettings:
|
|
|
63
64
|
if "edgeDB" in data
|
|
64
65
|
else None,
|
|
65
66
|
opt_in=OptInSettings.from_json(data["optIn"]) if "optIn" in data else None,
|
|
67
|
+
disable_passthrough_rollouts=data.get("disablePassthroughRollouts", False),
|
|
66
68
|
)
|
|
67
69
|
|
|
68
70
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# ruff: noqa: N815
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
|
-
from typing import Dict, Optional
|
|
4
|
+
from typing import Dict, Optional, Any
|
|
5
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
6
|
+
from openfeature.exception import TargetingKeyMissingError, InvalidContextError
|
|
5
7
|
|
|
6
8
|
|
|
7
9
|
@dataclass(order=False)
|
|
@@ -13,9 +15,9 @@ class DevCycleUser:
|
|
|
13
15
|
country: Optional[str] = None
|
|
14
16
|
appVersion: Optional[str] = None
|
|
15
17
|
appBuild: Optional[str] = None
|
|
16
|
-
customData: Optional[Dict[str,
|
|
18
|
+
customData: Optional[Dict[str, Any]] = None
|
|
17
19
|
createdDate: datetime = field(default_factory=lambda: datetime.utcnow())
|
|
18
|
-
privateCustomData: Optional[Dict[str,
|
|
20
|
+
privateCustomData: Optional[Dict[str, Any]] = None
|
|
19
21
|
lastSeenDate: Optional[datetime] = None
|
|
20
22
|
platform: Optional[str] = None
|
|
21
23
|
platformVersion: Optional[str] = None
|
|
@@ -74,3 +76,84 @@ class DevCycleUser:
|
|
|
74
76
|
sdkType=data.get("sdkType"),
|
|
75
77
|
sdkVersion=data.get("sdkVersion"),
|
|
76
78
|
)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _set_custom_value(custom_data: Dict[str, Any], key: str, value: Optional[Any]):
|
|
82
|
+
"""
|
|
83
|
+
Sets a custom value in the custom data dictionary. Custom data properties can
|
|
84
|
+
only be strings, numbers, or booleans. Nested dictionaries and lists are
|
|
85
|
+
not permitted.
|
|
86
|
+
|
|
87
|
+
Invalid values will generate an error
|
|
88
|
+
"""
|
|
89
|
+
if key and (value is None or isinstance(value, (str, int, float, bool))):
|
|
90
|
+
custom_data[key] = value
|
|
91
|
+
else:
|
|
92
|
+
raise InvalidContextError(
|
|
93
|
+
"Custom property values must be strings, numbers, booleans or None"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def create_user_from_context(
|
|
98
|
+
context: Optional[EvaluationContext],
|
|
99
|
+
) -> "DevCycleUser":
|
|
100
|
+
"""
|
|
101
|
+
Builds a DevCycleUser instance from the evaluation context. Will raise a TargetingKeyMissingError if
|
|
102
|
+
the context does not contain a valid targeting key or user_id attribute
|
|
103
|
+
|
|
104
|
+
:param context: The evaluation context to build the user from
|
|
105
|
+
:return: A DevCycleUser instance
|
|
106
|
+
"""
|
|
107
|
+
user_id = None
|
|
108
|
+
|
|
109
|
+
if context:
|
|
110
|
+
if context.targeting_key:
|
|
111
|
+
user_id = context.targeting_key
|
|
112
|
+
elif context.attributes and "user_id" in context.attributes.keys():
|
|
113
|
+
user_id = context.attributes["user_id"]
|
|
114
|
+
|
|
115
|
+
if not user_id or not isinstance(user_id, str):
|
|
116
|
+
raise TargetingKeyMissingError(
|
|
117
|
+
"DevCycle: Evaluation context does not contain a valid targeting key or user_id attribute"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
user = DevCycleUser(user_id=user_id)
|
|
121
|
+
custom_data: Dict[str, Any] = {}
|
|
122
|
+
private_custom_data: Dict[str, Any] = {}
|
|
123
|
+
if context and context.attributes:
|
|
124
|
+
for key, value in context.attributes.items():
|
|
125
|
+
if key == "user_id":
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if value:
|
|
129
|
+
if key == "email" and isinstance(value, str):
|
|
130
|
+
user.email = value
|
|
131
|
+
elif key == "name" and isinstance(value, str):
|
|
132
|
+
user.name = value
|
|
133
|
+
elif key == "language" and isinstance(value, str):
|
|
134
|
+
user.language = value
|
|
135
|
+
elif key == "country" and isinstance(value, str):
|
|
136
|
+
user.country = value
|
|
137
|
+
elif key == "appVersion" and isinstance(value, str):
|
|
138
|
+
user.appVersion = value
|
|
139
|
+
elif key == "appBuild" and isinstance(value, str):
|
|
140
|
+
user.appBuild = value
|
|
141
|
+
elif key == "deviceModel" and isinstance(value, str):
|
|
142
|
+
user.deviceModel = value
|
|
143
|
+
elif key == "customData" and isinstance(value, dict):
|
|
144
|
+
for k, v in value.items():
|
|
145
|
+
DevCycleUser._set_custom_value(custom_data, k, v)
|
|
146
|
+
elif key == "privateCustomData" and isinstance(value, dict):
|
|
147
|
+
for k, v in value.items():
|
|
148
|
+
DevCycleUser._set_custom_value(private_custom_data, k, v)
|
|
149
|
+
else:
|
|
150
|
+
# unrecognized keys are just added to public custom data
|
|
151
|
+
DevCycleUser._set_custom_value(custom_data, key, value)
|
|
152
|
+
|
|
153
|
+
if custom_data:
|
|
154
|
+
user.customData = custom_data
|
|
155
|
+
|
|
156
|
+
if private_custom_data:
|
|
157
|
+
user.privateCustomData = private_custom_data
|
|
158
|
+
|
|
159
|
+
return user
|
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional, Union, List
|
|
4
|
+
|
|
5
|
+
from devcycle_python_sdk import AbstractDevCycleClient
|
|
6
|
+
from devcycle_python_sdk.models.user import DevCycleUser
|
|
7
|
+
|
|
8
|
+
from openfeature.provider import AbstractProvider
|
|
9
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
10
|
+
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
|
|
11
|
+
from openfeature.exception import ErrorCode, InvalidContextError, TypeMismatchError
|
|
12
|
+
from openfeature.hook import Hook
|
|
13
|
+
from openfeature.provider.metadata import Metadata
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DevCycleProvider(AbstractProvider):
|
|
19
|
+
"""
|
|
20
|
+
Openfeature provider wrapper for the DevCycle SDK.
|
|
21
|
+
|
|
22
|
+
Can be initialized with either a DevCycleLocalClient or DevCycleCloudClient instance.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, devcycle_client: AbstractDevCycleClient):
|
|
26
|
+
self.client = devcycle_client
|
|
27
|
+
self.meta_data = Metadata(name=f"DevCycle {self.client.get_sdk_platform()}")
|
|
28
|
+
|
|
29
|
+
def get_metadata(self) -> Metadata:
|
|
30
|
+
return self.meta_data
|
|
31
|
+
|
|
32
|
+
def get_provider_hooks(self) -> List[Hook]:
|
|
33
|
+
return []
|
|
34
|
+
|
|
35
|
+
def _resolve(
|
|
36
|
+
self,
|
|
37
|
+
flag_key: str,
|
|
38
|
+
default_value: Any,
|
|
39
|
+
evaluation_context: Optional[EvaluationContext] = None,
|
|
40
|
+
) -> FlagResolutionDetails[Any]:
|
|
41
|
+
if self.client.is_initialized():
|
|
42
|
+
try:
|
|
43
|
+
user: DevCycleUser = DevCycleUser.create_user_from_context(
|
|
44
|
+
evaluation_context
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
variable = self.client.variable(
|
|
48
|
+
key=flag_key, user=user, default_value=default_value
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if variable is None:
|
|
52
|
+
# this technically should never happen
|
|
53
|
+
# as the DevCycle client should at least return a default Variable instance
|
|
54
|
+
return FlagResolutionDetails(
|
|
55
|
+
value=default_value,
|
|
56
|
+
reason=Reason.DEFAULT,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
return FlagResolutionDetails(
|
|
60
|
+
value=variable.value,
|
|
61
|
+
reason=Reason.DEFAULT
|
|
62
|
+
if variable.isDefaulted
|
|
63
|
+
else Reason.TARGETING_MATCH,
|
|
64
|
+
)
|
|
65
|
+
except ValueError as e:
|
|
66
|
+
# occurs if the key or default value is None
|
|
67
|
+
raise InvalidContextError(str(e))
|
|
68
|
+
else:
|
|
69
|
+
return FlagResolutionDetails(
|
|
70
|
+
value=default_value,
|
|
71
|
+
reason=Reason.ERROR,
|
|
72
|
+
error_code=ErrorCode.PROVIDER_NOT_READY,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def resolve_boolean_details(
|
|
76
|
+
self,
|
|
77
|
+
flag_key: str,
|
|
78
|
+
default_value: bool,
|
|
79
|
+
evaluation_context: Optional[EvaluationContext] = None,
|
|
80
|
+
) -> FlagResolutionDetails[bool]:
|
|
81
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
82
|
+
|
|
83
|
+
def resolve_string_details(
|
|
84
|
+
self,
|
|
85
|
+
flag_key: str,
|
|
86
|
+
default_value: str,
|
|
87
|
+
evaluation_context: Optional[EvaluationContext] = None,
|
|
88
|
+
) -> FlagResolutionDetails[str]:
|
|
89
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
90
|
+
|
|
91
|
+
def resolve_integer_details(
|
|
92
|
+
self,
|
|
93
|
+
flag_key: str,
|
|
94
|
+
default_value: int,
|
|
95
|
+
evaluation_context: Optional[EvaluationContext] = None,
|
|
96
|
+
) -> FlagResolutionDetails[int]:
|
|
97
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
98
|
+
|
|
99
|
+
def resolve_float_details(
|
|
100
|
+
self,
|
|
101
|
+
flag_key: str,
|
|
102
|
+
default_value: float,
|
|
103
|
+
evaluation_context: Optional[EvaluationContext] = None,
|
|
104
|
+
) -> FlagResolutionDetails[float]:
|
|
105
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
|
106
|
+
|
|
107
|
+
def resolve_object_details(
|
|
108
|
+
self,
|
|
109
|
+
flag_key: str,
|
|
110
|
+
default_value: Union[dict, list],
|
|
111
|
+
evaluation_context: Optional[EvaluationContext] = None,
|
|
112
|
+
) -> FlagResolutionDetails[Union[dict, list]]:
|
|
113
|
+
if not isinstance(default_value, dict):
|
|
114
|
+
raise TypeMismatchError("Default value must be a flat dictionary")
|
|
115
|
+
|
|
116
|
+
if default_value:
|
|
117
|
+
for k, v in default_value.items():
|
|
118
|
+
if not isinstance(v, (str, int, float, bool)) or v is None:
|
|
119
|
+
raise TypeMismatchError(
|
|
120
|
+
"Default value must be a flat dictionary containing only strings, numbers, booleans or None values"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return self._resolve(flag_key, default_value, evaluation_context)
|
{devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: devcycle-python-server-sdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: DevCycle Python SDK
|
|
5
5
|
Home-page: https://github.com/devcycleHQ/python-server-sdk
|
|
6
6
|
Author: UNKNOWN
|
|
@@ -13,6 +13,7 @@ Requires-Dist: urllib3 >=1.15.1
|
|
|
13
13
|
Requires-Dist: requests ~=2.31.0
|
|
14
14
|
Requires-Dist: wasmtime ==9.0.0
|
|
15
15
|
Requires-Dist: protobuf >=4.23.3
|
|
16
|
+
Requires-Dist: openfeature-sdk >=0.7.0
|
|
16
17
|
|
|
17
18
|
The DevCycle Python SDK used for feature management.
|
|
18
19
|
|
{devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/RECORD
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
devcycle_python_sdk/VERSION.txt,sha256=
|
|
2
|
-
devcycle_python_sdk/__init__.py,sha256=
|
|
3
|
-
devcycle_python_sdk/bucketing-lib.release.wasm,sha256=
|
|
4
|
-
devcycle_python_sdk/cloud_client.py,sha256=
|
|
1
|
+
devcycle_python_sdk/VERSION.txt,sha256=dhIMHyxDEcIVTCKmtY4A5BHg6-dq-uDa9gjZGWxh8RQ,6
|
|
2
|
+
devcycle_python_sdk/__init__.py,sha256=30TcYb9PbvVi97gwk16omCGG1UoUI9lmmU7xpvxNMvc,336
|
|
3
|
+
devcycle_python_sdk/bucketing-lib.release.wasm,sha256=yrNDrAxZS1h9VlTzsdOrNLP4ngnM04PmKxcWop1yrt4,202623
|
|
4
|
+
devcycle_python_sdk/cloud_client.py,sha256=UMfXvXwO4v3pZW9ZA52ivxPH1Lp4Goy5gpV_s0qCpxQ,7352
|
|
5
|
+
devcycle_python_sdk/devcycle_client.py,sha256=W5MeOslkXjTqZtoSy_Bbbug8gVoicjxshpKjDteEr6E,917
|
|
5
6
|
devcycle_python_sdk/exceptions.py,sha256=Et7fS8pEFbHkFY86T2pJoEUstwjh4_rzhJxVH8fzAns,1053
|
|
6
|
-
devcycle_python_sdk/local_client.py,sha256=
|
|
7
|
+
devcycle_python_sdk/local_client.py,sha256=5PFEUmBs-QsgaiXWBQfhhjcN2TbB64JH3iVwpJteG7o,9373
|
|
7
8
|
devcycle_python_sdk/options.py,sha256=Ejr6HbdyqzZajFVkEqWLk5XLfdtqCIkvsyWaRt_lgwU,4575
|
|
8
9
|
devcycle_python_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
10
|
devcycle_python_sdk/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
@@ -16,13 +17,15 @@ devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
|
|
|
16
17
|
devcycle_python_sdk/managers/config_manager.py,sha256=_-1kWwyyAcwVF_DibXjnehXi3Qj6HR8orYfgNfgWJOc,3248
|
|
17
18
|
devcycle_python_sdk/managers/event_queue_manager.py,sha256=06TcdlRoB89C7vGH1UTnnoDFsSnT71tPwf0gn1NK-W0,8071
|
|
18
19
|
devcycle_python_sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
devcycle_python_sdk/models/bucketed_config.py,sha256=
|
|
20
|
+
devcycle_python_sdk/models/bucketed_config.py,sha256=76bdF7pgUdIAn2y8YPs3AO7Qa2eyJQ_jf4QhG3IERaE,3951
|
|
20
21
|
devcycle_python_sdk/models/error_response.py,sha256=r_dRUDAbral9UGaMYcnBsZOr9Hmkyi1SQbhGmZilgpA,442
|
|
21
22
|
devcycle_python_sdk/models/event.py,sha256=-6bhKp72mAgaoSxvhS__G4vvfQ9Psj83fc0Igelzw3o,3713
|
|
22
23
|
devcycle_python_sdk/models/feature.py,sha256=m3wxxIjC5jxhKpPLI4RcsBaNLuy-zsMqoAgc8ySPe0I,852
|
|
23
24
|
devcycle_python_sdk/models/platform_data.py,sha256=kIFevH5trsxI32qp7BW6glUCpipgpaVBKGqJWzQd5Xo,783
|
|
24
|
-
devcycle_python_sdk/models/user.py,sha256=
|
|
25
|
+
devcycle_python_sdk/models/user.py,sha256=57d_n8Yy8g-zsGzVF0hmSf4McPtFPJJ0LLY_ql75FWk,6266
|
|
25
26
|
devcycle_python_sdk/models/variable.py,sha256=qp00SyCNyfYiMlTwnDSDJ4ZetRF35JHTbYZQOBxFIqY,1711
|
|
27
|
+
devcycle_python_sdk/open_feature_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
devcycle_python_sdk/open_feature_provider/provider.py,sha256=FjNxmyKrnUO4ma4c-Z39qWigLVC_P-L_Nly0mycA1Vc,4475
|
|
26
29
|
devcycle_python_sdk/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
30
|
devcycle_python_sdk/protobuf/utils.py,sha256=zi-kVx6yvgS2c04cUobLd2IDc_ZqKkWduay9YcpjoGc,4684
|
|
28
31
|
devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=Zf4-8n67UnulC0Sz90aqLYsXCFEgywOqfiSHuXZomLk,4244
|
|
@@ -31,15 +34,16 @@ devcycle_python_sdk/util/strings.py,sha256=gH0Xm3gIXNqn6hczKPk10Jn5_9knuD7OB5MrB
|
|
|
31
34
|
devcycle_python_sdk/util/version.py,sha256=Cz7Us_EuXP-T8GI47g2WHYdBJj_zKo-5y545IDHJAjI,253
|
|
32
35
|
example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
36
|
example/cloud_client_example.py,sha256=YD8ZsTv3dPYfbFUKraRpRT0VsJGrPwjm8ZCuA5H54yI,2463
|
|
34
|
-
example/local_bucketing_client_example.py,sha256=
|
|
37
|
+
example/local_bucketing_client_example.py,sha256=TaIfy3VnW9fXii9NPYs5UOuP4fLKruXMjnZApveqzl4,2796
|
|
38
|
+
example/openfeature_example.py,sha256=kO6F-Xk20l6CPiMfIxSRreshEGmHMFHRg3zbeLXa3AI,2410
|
|
35
39
|
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
40
|
test/test_cloud_client.py,sha256=zjk302m7qKMCQryolkc1WKLFmbGWUxA0VtHlZk0rY-0,11442
|
|
37
|
-
test/test_local_client.py,sha256=
|
|
41
|
+
test/test_local_client.py,sha256=Ds6VIfLcaefboyvGrhQTGsdJj_DGB4IVnR2_16xDpSY,13022
|
|
38
42
|
test/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
43
|
test/api/test_bucketing_client.py,sha256=M2mjACowLvbsIjXdP-w8-fwY02xqv1X1r_h9v-pXa7A,7739
|
|
40
44
|
test/api/test_config_client.py,sha256=oZMBlXGw4UHb5-rSNF_ARTrAjvbvkbFgb8GAK4ExR14,4413
|
|
41
45
|
test/api/test_event_client.py,sha256=v4Xoum-PdywfczrfMV1llM5vrIJoqHybzxPLVokh09g,3310
|
|
42
|
-
test/api/test_local_bucketing.py,sha256=
|
|
46
|
+
test/api/test_local_bucketing.py,sha256=lPL3ThZcGG-8-67jFxRvGrwL8NB6-sM5qGJb0p2O__Q,15254
|
|
43
47
|
test/fixture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
48
|
test/fixture/data.py,sha256=6ReLxWPMhCDbJ5Ex24_iR0PJNjKzPFV1I9j_v0FVbBU,1462
|
|
45
49
|
test/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -47,12 +51,16 @@ test/managers/test_config_manager.py,sha256=_MtvyxK06xpBzzEsN1zsLdYrk3DsPsbCmSj7
|
|
|
47
51
|
test/managers/test_event_queue_manager.py,sha256=KquHBuYiDdoVStm5Ga9mIB-wvmJnChiFRBNC8sMKaxQ,11053
|
|
48
52
|
test/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
53
|
test/models/test_bucketed_config.py,sha256=8wCEyxaHxY21nl7ZbiFzp-IzZQBZIPIVPPzeREjumQk,858
|
|
54
|
+
test/models/test_user.py,sha256=ZNTBSKLDrqrsqZuaw5AHtc_FW4empSTaeNPZHZaFlI4,6736
|
|
55
|
+
test/openfeature_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
+
test/openfeature_test/test_provider.py,sha256=UPxAD5e7NvpIhmqNFCrTY_yrKrMyYxBc2xuMAgdSGvw,7653
|
|
57
|
+
test/openfeature_test/test_provider_local_sdk.py,sha256=5NjfUDalW4F7Oe8n1740VO7l5OIBEOldnSryM98uO2w,4426
|
|
50
58
|
test/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
59
|
test/util/test_strings.py,sha256=T5H5U0af7_ld1yJC8IdDmay_Mx-iJQ5Fi5fbvKcAc7A,862
|
|
52
60
|
test/util/test_utils.py,sha256=tVBJm_qnqg-4PssLpXIxqlk0JehuiAIPs4zpi2oNVnY,6671
|
|
53
|
-
test/util/test_version.py,sha256=
|
|
54
|
-
devcycle_python_server_sdk-3.
|
|
55
|
-
devcycle_python_server_sdk-3.
|
|
56
|
-
devcycle_python_server_sdk-3.
|
|
57
|
-
devcycle_python_server_sdk-3.
|
|
58
|
-
devcycle_python_server_sdk-3.
|
|
61
|
+
test/util/test_version.py,sha256=3Xg0_84m0bdHXz6pxd2Z85Oz9DoUC1T0Ol_WqEH_jac,335
|
|
62
|
+
devcycle_python_server_sdk-3.5.0.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
|
|
63
|
+
devcycle_python_server_sdk-3.5.0.dist-info/METADATA,sha256=IAUJWGuZDWtOoCfJ6Tj6aKURxJODAtUuutO1g94hLho,510
|
|
64
|
+
devcycle_python_server_sdk-3.5.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
65
|
+
devcycle_python_server_sdk-3.5.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
|
|
66
|
+
devcycle_python_server_sdk-3.5.0.dist-info/RECORD,,
|
|
@@ -7,7 +7,7 @@ from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
|
|
|
7
7
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
8
8
|
from devcycle_python_sdk.models.event import DevCycleEvent, EventType
|
|
9
9
|
|
|
10
|
-
VARIABLE_KEY = "
|
|
10
|
+
VARIABLE_KEY = "python-example-tests"
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
|
|
6
|
+
|
|
7
|
+
from openfeature import api
|
|
8
|
+
from openfeature.evaluation_context import EvaluationContext
|
|
9
|
+
|
|
10
|
+
FLAG_KEY = "test-boolean-variable"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
"""
|
|
17
|
+
Sample usage of the DevCycle OpenFeature Provider along with the Python Server SDK using Local Bucketing.
|
|
18
|
+
"""
|
|
19
|
+
logging.basicConfig(level="INFO", format="%(levelname)s: %(message)s")
|
|
20
|
+
|
|
21
|
+
# create an instance of the DevCycle Client object
|
|
22
|
+
server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
|
|
23
|
+
devcycle_client = DevCycleLocalClient(server_sdk_key, DevCycleLocalOptions())
|
|
24
|
+
|
|
25
|
+
# Wait for DevCycle to initialize and load the configuration
|
|
26
|
+
for i in range(10):
|
|
27
|
+
if devcycle_client.is_initialized():
|
|
28
|
+
break
|
|
29
|
+
logger.info("Waiting for DevCycle to initialize...")
|
|
30
|
+
time.sleep(0.5)
|
|
31
|
+
else:
|
|
32
|
+
logger.error("DevCycle failed to initialize")
|
|
33
|
+
exit(1)
|
|
34
|
+
|
|
35
|
+
# set the provider for OpenFeature
|
|
36
|
+
api.set_provider(devcycle_client.get_openfeature_provider())
|
|
37
|
+
|
|
38
|
+
# get the OpenFeature client
|
|
39
|
+
open_feature_client = api.get_client()
|
|
40
|
+
|
|
41
|
+
# create the request context for the user
|
|
42
|
+
context = EvaluationContext(
|
|
43
|
+
targeting_key="test-1234",
|
|
44
|
+
attributes={
|
|
45
|
+
"email": "test-user@domain.com",
|
|
46
|
+
"name": "Test User",
|
|
47
|
+
"language": "en",
|
|
48
|
+
"country": "CA",
|
|
49
|
+
"appVersion": "1.0.11",
|
|
50
|
+
"appBuild": 1000,
|
|
51
|
+
"customData": {"custom": "data"},
|
|
52
|
+
"privateCustomData": {"private": "data"},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Look up the value of the flag
|
|
57
|
+
if open_feature_client.get_boolean_value(FLAG_KEY, False, context):
|
|
58
|
+
logger.info(f"Variable {FLAG_KEY} is enabled")
|
|
59
|
+
else:
|
|
60
|
+
logger.info(f"Variable {FLAG_KEY} is not enabled")
|
|
61
|
+
|
|
62
|
+
# Fetch a JSON object variable
|
|
63
|
+
json_object = open_feature_client.get_object_value(
|
|
64
|
+
"test-json-variable", {"default": "value"}, context
|
|
65
|
+
)
|
|
66
|
+
logger.info(f"JSON Object Value: {json_object}")
|
|
67
|
+
|
|
68
|
+
# Retrieve a string variable along with resolution details
|
|
69
|
+
details = open_feature_client.get_string_details("doesnt-exist", "default", context)
|
|
70
|
+
logger.info(f"String Value: {details.value}")
|
|
71
|
+
logger.info(f"Eval Reason: {details.reason}")
|
|
72
|
+
|
|
73
|
+
devcycle_client.close()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
test/api/test_local_bucketing.py
CHANGED
|
@@ -5,10 +5,7 @@ import unittest
|
|
|
5
5
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing, WASMAbortError
|
|
6
6
|
from devcycle_python_sdk.models.bucketed_config import (
|
|
7
7
|
Environment,
|
|
8
|
-
EdgeDBSettings,
|
|
9
8
|
FeatureVariation,
|
|
10
|
-
OptInColors,
|
|
11
|
-
OptInSettings,
|
|
12
9
|
Project,
|
|
13
10
|
ProjectSettings,
|
|
14
11
|
)
|
|
@@ -197,27 +194,29 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
197
194
|
defaultValue=None,
|
|
198
195
|
evalReason=None,
|
|
199
196
|
),
|
|
197
|
+
"float-var": Variable(
|
|
198
|
+
_id="61200363125123fca69d3a7a",
|
|
199
|
+
key="float-var",
|
|
200
|
+
type="Number",
|
|
201
|
+
value=3.14159,
|
|
202
|
+
isDefaulted=None,
|
|
203
|
+
defaultValue=None,
|
|
204
|
+
evalReason=None,
|
|
205
|
+
),
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
self.assertIsNotNone(result)
|
|
203
209
|
self.assertEqual(result.user, user)
|
|
204
210
|
self.assertEqual(
|
|
205
|
-
result.project,
|
|
206
211
|
Project(
|
|
207
212
|
id="61f97628ff4afcb6d057dbf0",
|
|
208
213
|
key="emma-project",
|
|
209
214
|
a0_organization="org_tPyJN5dvNNirKar7",
|
|
210
215
|
settings=ProjectSettings(
|
|
211
|
-
edge_db=
|
|
212
|
-
opt_in=OptInSettings(
|
|
213
|
-
enabled=True,
|
|
214
|
-
title="EarlyAccess",
|
|
215
|
-
description="Getearlyaccesstobetafeaturesbelow!",
|
|
216
|
-
image_url="",
|
|
217
|
-
colors=OptInColors(primary="#531cd9", secondary="#16dec0"),
|
|
218
|
-
),
|
|
216
|
+
edge_db=None, opt_in=None, disable_passthrough_rollouts=False
|
|
219
217
|
),
|
|
220
218
|
),
|
|
219
|
+
result.project,
|
|
221
220
|
)
|
|
222
221
|
self.assertEqual(
|
|
223
222
|
result.environment,
|
|
@@ -250,6 +249,10 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
250
249
|
feature="62fbf6566f1ba302829f9e32",
|
|
251
250
|
variation="62fbf6566f1ba302829f9e39",
|
|
252
251
|
),
|
|
252
|
+
"float-var": FeatureVariation(
|
|
253
|
+
feature="62fbf6566f1ba302829f9e32",
|
|
254
|
+
variation="62fbf6566f1ba302829f9e39",
|
|
255
|
+
),
|
|
253
256
|
},
|
|
254
257
|
)
|
|
255
258
|
self.assertEqual(
|
test/models/test_user.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from devcycle_python_sdk.models.user import DevCycleUser
|
|
5
|
+
|
|
6
|
+
from openfeature.provider import EvaluationContext
|
|
7
|
+
from openfeature.exception import TargetingKeyMissingError, InvalidContextError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DevCycleUserTest(unittest.TestCase):
|
|
14
|
+
def test_create_user_from_context_no_context(self):
|
|
15
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
16
|
+
DevCycleUser.create_user_from_context(None)
|
|
17
|
+
|
|
18
|
+
def test_create_user_from_context_no_user_id(self):
|
|
19
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
20
|
+
DevCycleUser.create_user_from_context(
|
|
21
|
+
EvaluationContext(targeting_key=None, attributes={})
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
25
|
+
DevCycleUser.create_user_from_context(
|
|
26
|
+
EvaluationContext(targeting_key=None, attributes=None)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
30
|
+
DevCycleUser.create_user_from_context(
|
|
31
|
+
EvaluationContext(targeting_key=None, attributes={"user_id": None})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def test_create_user_from_context_only_user_id(self):
|
|
35
|
+
test_user_id = "12345"
|
|
36
|
+
context = EvaluationContext(targeting_key=test_user_id, attributes=None)
|
|
37
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
38
|
+
self.assertIsNotNone(user)
|
|
39
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
40
|
+
|
|
41
|
+
context = EvaluationContext(
|
|
42
|
+
targeting_key=None, attributes={"user_id": test_user_id}
|
|
43
|
+
)
|
|
44
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
45
|
+
self.assertIsNotNone(user)
|
|
46
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
47
|
+
|
|
48
|
+
# ensure that targeting_key takes precedence over user_id
|
|
49
|
+
context = EvaluationContext(
|
|
50
|
+
targeting_key=test_user_id, attributes={"user_id": "99999"}
|
|
51
|
+
)
|
|
52
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
53
|
+
self.assertIsNotNone(user)
|
|
54
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
55
|
+
|
|
56
|
+
def test_create_user_from_context_with_attributes(self):
|
|
57
|
+
test_user_id = "12345"
|
|
58
|
+
context = EvaluationContext(
|
|
59
|
+
targeting_key=test_user_id,
|
|
60
|
+
attributes={
|
|
61
|
+
"user_id": "1234",
|
|
62
|
+
"email": "someone@example.com",
|
|
63
|
+
"name": "John Doe",
|
|
64
|
+
"language": "en",
|
|
65
|
+
"country": "US",
|
|
66
|
+
"appVersion": "1.0.0",
|
|
67
|
+
"appBuild": "1",
|
|
68
|
+
"deviceModel": "iPhone X21",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
72
|
+
self.assertIsNotNone(user)
|
|
73
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
74
|
+
self.assertEqual(user.email, context.attributes["email"])
|
|
75
|
+
self.assertEqual(user.name, context.attributes["name"])
|
|
76
|
+
self.assertEqual(user.language, context.attributes["language"])
|
|
77
|
+
self.assertEqual(user.country, context.attributes["country"])
|
|
78
|
+
self.assertEqual(user.appVersion, context.attributes["appVersion"])
|
|
79
|
+
self.assertEqual(user.appBuild, context.attributes["appBuild"])
|
|
80
|
+
self.assertEqual(user.deviceModel, context.attributes["deviceModel"])
|
|
81
|
+
|
|
82
|
+
def test_create_user_from_context_with_custom_data(self):
|
|
83
|
+
test_user_id = "12345"
|
|
84
|
+
context = EvaluationContext(
|
|
85
|
+
targeting_key=test_user_id,
|
|
86
|
+
attributes={
|
|
87
|
+
"customData": {
|
|
88
|
+
"strValue": "hello",
|
|
89
|
+
"intValue": 123,
|
|
90
|
+
"floatValue": 3.1456,
|
|
91
|
+
"boolValue": True,
|
|
92
|
+
},
|
|
93
|
+
"privateCustomData": {
|
|
94
|
+
"strValue": "world",
|
|
95
|
+
"intValue": 789,
|
|
96
|
+
"floatValue": 0.0001,
|
|
97
|
+
"BoolValue": False,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
102
|
+
self.assertIsNotNone(user)
|
|
103
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
104
|
+
|
|
105
|
+
self.assertEqual(
|
|
106
|
+
user.customData,
|
|
107
|
+
{
|
|
108
|
+
"strValue": "hello",
|
|
109
|
+
"intValue": 123,
|
|
110
|
+
"floatValue": 3.1456,
|
|
111
|
+
"boolValue": True,
|
|
112
|
+
},
|
|
113
|
+
)
|
|
114
|
+
self.assertEqual(
|
|
115
|
+
user.privateCustomData,
|
|
116
|
+
{
|
|
117
|
+
"strValue": "world",
|
|
118
|
+
"intValue": 789,
|
|
119
|
+
"floatValue": 0.0001,
|
|
120
|
+
"BoolValue": False,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def test_create_user_from_context_with_unknown_data_fields(self):
|
|
125
|
+
test_user_id = "12345"
|
|
126
|
+
context = EvaluationContext(
|
|
127
|
+
targeting_key=test_user_id,
|
|
128
|
+
attributes={
|
|
129
|
+
"strValue": "hello",
|
|
130
|
+
"intValue": 123,
|
|
131
|
+
"floatValue": 3.1456,
|
|
132
|
+
"boolValue": True,
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
user = DevCycleUser.create_user_from_context(context)
|
|
136
|
+
self.assertIsNotNone(user)
|
|
137
|
+
self.assertEqual(user.user_id, test_user_id)
|
|
138
|
+
|
|
139
|
+
# the fields should get reassigned to custom_data
|
|
140
|
+
self.assertEqual(
|
|
141
|
+
user.customData,
|
|
142
|
+
{
|
|
143
|
+
"strValue": "hello",
|
|
144
|
+
"intValue": 123,
|
|
145
|
+
"floatValue": 3.1456,
|
|
146
|
+
"boolValue": True,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
self.assertEqual(user.privateCustomData, None)
|
|
150
|
+
|
|
151
|
+
def test_set_custom_value(self):
|
|
152
|
+
custom_data = {}
|
|
153
|
+
with self.assertRaises(InvalidContextError):
|
|
154
|
+
DevCycleUser._set_custom_value(custom_data, None, None)
|
|
155
|
+
|
|
156
|
+
custom_data = {}
|
|
157
|
+
DevCycleUser._set_custom_value(custom_data, "key1", None)
|
|
158
|
+
self.assertDictEqual(custom_data, {"key1": None})
|
|
159
|
+
|
|
160
|
+
custom_data = {}
|
|
161
|
+
DevCycleUser._set_custom_value(custom_data, "key1", "value1")
|
|
162
|
+
self.assertDictEqual(custom_data, {"key1": "value1"})
|
|
163
|
+
|
|
164
|
+
custom_data = {}
|
|
165
|
+
DevCycleUser._set_custom_value(custom_data, "key1", 1)
|
|
166
|
+
self.assertDictEqual(custom_data, {"key1": 1})
|
|
167
|
+
|
|
168
|
+
custom_data = {}
|
|
169
|
+
DevCycleUser._set_custom_value(custom_data, "key1", 3.1456)
|
|
170
|
+
self.assertDictEqual(custom_data, {"key1": 3.1456})
|
|
171
|
+
|
|
172
|
+
custom_data = {}
|
|
173
|
+
DevCycleUser._set_custom_value(custom_data, "key1", True)
|
|
174
|
+
self.assertDictEqual(custom_data, {"key1": True})
|
|
175
|
+
|
|
176
|
+
custom_data = {}
|
|
177
|
+
with self.assertRaises(InvalidContextError):
|
|
178
|
+
DevCycleUser._set_custom_value(custom_data, "map_data", {"hello": "world"})
|
|
179
|
+
|
|
180
|
+
custom_data = {}
|
|
181
|
+
with self.assertRaises(InvalidContextError):
|
|
182
|
+
DevCycleUser._set_custom_value(
|
|
183
|
+
custom_data, "list_data", ["one", "two", "three"]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
unittest.main()
|
|
File without changes
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import MagicMock
|
|
4
|
+
|
|
5
|
+
from openfeature.provider import EvaluationContext
|
|
6
|
+
from openfeature.flag_evaluation import Reason
|
|
7
|
+
from openfeature.exception import (
|
|
8
|
+
ErrorCode,
|
|
9
|
+
TargetingKeyMissingError,
|
|
10
|
+
InvalidContextError,
|
|
11
|
+
TypeMismatchError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from devcycle_python_sdk.models.variable import Variable, TypeEnum
|
|
15
|
+
|
|
16
|
+
from devcycle_python_sdk.open_feature_provider.provider import (
|
|
17
|
+
DevCycleProvider,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DevCycleProviderTest(unittest.TestCase):
|
|
25
|
+
def setUp(self):
|
|
26
|
+
self.client = MagicMock()
|
|
27
|
+
self.client.is_initialized.return_value = True
|
|
28
|
+
self.provider = DevCycleProvider(self.client)
|
|
29
|
+
|
|
30
|
+
def test_resolve_details_client_not_ready(self):
|
|
31
|
+
self.client.is_initialized.return_value = False
|
|
32
|
+
|
|
33
|
+
details = self.provider._resolve(
|
|
34
|
+
"test-flag", False, EvaluationContext("test-user")
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
self.assertIsNotNone(details)
|
|
38
|
+
self.assertEqual(details.value, False)
|
|
39
|
+
self.assertEqual(details.reason, Reason.ERROR)
|
|
40
|
+
self.assertEqual(details.error_code, ErrorCode.PROVIDER_NOT_READY)
|
|
41
|
+
|
|
42
|
+
def test_resolve_details_client_no_user_in_context(self):
|
|
43
|
+
context = EvaluationContext(targeting_key=None, attributes={})
|
|
44
|
+
|
|
45
|
+
with self.assertRaises(TargetingKeyMissingError):
|
|
46
|
+
self.provider._resolve("test-flag", False, context)
|
|
47
|
+
|
|
48
|
+
def test_resolve_details_no_key(self):
|
|
49
|
+
self.client.variable.side_effect = ValueError("test error")
|
|
50
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
51
|
+
with self.assertRaises(InvalidContextError):
|
|
52
|
+
self.provider._resolve(None, False, context)
|
|
53
|
+
|
|
54
|
+
def test_resolve_details_no_default_value(self):
|
|
55
|
+
self.client.variable.side_effect = ValueError("test error")
|
|
56
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
57
|
+
with self.assertRaises(InvalidContextError):
|
|
58
|
+
self.provider._resolve("test-flag", None, context)
|
|
59
|
+
|
|
60
|
+
def test_resolve_details_client_returns_none(self):
|
|
61
|
+
self.client.variable.return_value = None
|
|
62
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
63
|
+
|
|
64
|
+
details = self.provider._resolve("test-flag", False, context)
|
|
65
|
+
|
|
66
|
+
self.assertIsNotNone(details)
|
|
67
|
+
self.assertEqual(details.value, False)
|
|
68
|
+
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
69
|
+
|
|
70
|
+
def test_resolve_details_client_returns_default_variable(self):
|
|
71
|
+
self.client.variable.return_value = Variable.create_default_variable(
|
|
72
|
+
key="test-flag", default_value=False
|
|
73
|
+
)
|
|
74
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
75
|
+
details = self.provider._resolve("test-flag", False, context)
|
|
76
|
+
|
|
77
|
+
self.assertIsNotNone(details)
|
|
78
|
+
self.assertEqual(details.value, False)
|
|
79
|
+
self.assertEqual(details.reason, Reason.DEFAULT)
|
|
80
|
+
|
|
81
|
+
def test_resolve_boolean_details(self):
|
|
82
|
+
key = "test-flag"
|
|
83
|
+
variable_value = True
|
|
84
|
+
default_value = False
|
|
85
|
+
|
|
86
|
+
self.client.variable.return_value = Variable(
|
|
87
|
+
_id=None,
|
|
88
|
+
value=variable_value,
|
|
89
|
+
key=key,
|
|
90
|
+
type=TypeEnum.BOOLEAN,
|
|
91
|
+
isDefaulted=False,
|
|
92
|
+
defaultValue=False,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
96
|
+
details = self.provider.resolve_boolean_details(key, default_value, context)
|
|
97
|
+
|
|
98
|
+
self.assertIsNotNone(details)
|
|
99
|
+
self.assertEqual(details.value, variable_value)
|
|
100
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
101
|
+
|
|
102
|
+
def test_resolve_string_details(self):
|
|
103
|
+
key = "test-flag"
|
|
104
|
+
variable_value = "some string"
|
|
105
|
+
default_value = "default string"
|
|
106
|
+
|
|
107
|
+
self.client.variable.return_value = Variable(
|
|
108
|
+
_id=None,
|
|
109
|
+
value=variable_value,
|
|
110
|
+
key=key,
|
|
111
|
+
type=TypeEnum.STRING,
|
|
112
|
+
isDefaulted=False,
|
|
113
|
+
defaultValue=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
117
|
+
details = self.provider.resolve_string_details(key, default_value, context)
|
|
118
|
+
|
|
119
|
+
self.assertIsNotNone(details)
|
|
120
|
+
self.assertEqual(details.value, variable_value)
|
|
121
|
+
self.assertIsInstance(details.value, str)
|
|
122
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
123
|
+
|
|
124
|
+
def test_resolve_integer_details(self):
|
|
125
|
+
key = "test-flag"
|
|
126
|
+
variable_value = 12345
|
|
127
|
+
default_value = 0
|
|
128
|
+
|
|
129
|
+
self.client.variable.return_value = Variable(
|
|
130
|
+
_id=None,
|
|
131
|
+
value=variable_value,
|
|
132
|
+
key=key,
|
|
133
|
+
type=TypeEnum.STRING,
|
|
134
|
+
isDefaulted=False,
|
|
135
|
+
defaultValue=False,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
139
|
+
details = self.provider.resolve_integer_details(key, default_value, context)
|
|
140
|
+
|
|
141
|
+
self.assertIsNotNone(details)
|
|
142
|
+
self.assertIsInstance(details.value, int)
|
|
143
|
+
self.assertEqual(details.value, variable_value)
|
|
144
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
145
|
+
|
|
146
|
+
def test_resolve_float_details(self):
|
|
147
|
+
key = "test-flag"
|
|
148
|
+
variable_value = 3.145
|
|
149
|
+
default_value = 0.0
|
|
150
|
+
|
|
151
|
+
self.client.variable.return_value = Variable(
|
|
152
|
+
_id=None,
|
|
153
|
+
value=variable_value,
|
|
154
|
+
key=key,
|
|
155
|
+
type=TypeEnum.STRING,
|
|
156
|
+
isDefaulted=False,
|
|
157
|
+
defaultValue=False,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
161
|
+
details = self.provider.resolve_float_details(key, default_value, context)
|
|
162
|
+
|
|
163
|
+
self.assertIsNotNone(details)
|
|
164
|
+
self.assertIsInstance(details.value, float)
|
|
165
|
+
self.assertEqual(details.value, variable_value)
|
|
166
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
167
|
+
|
|
168
|
+
def test_resolve_object_details_verify_default_value(self):
|
|
169
|
+
key = "test-flag"
|
|
170
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
171
|
+
|
|
172
|
+
# Only flat dictionaries are supported as objects
|
|
173
|
+
# lists, primitives and nested objects are not supported
|
|
174
|
+
with self.assertRaises(TypeMismatchError):
|
|
175
|
+
self.provider.resolve_object_details(key, [], context)
|
|
176
|
+
|
|
177
|
+
with self.assertRaises(TypeMismatchError):
|
|
178
|
+
self.provider.resolve_object_details(key, 1234, context)
|
|
179
|
+
|
|
180
|
+
with self.assertRaises(TypeMismatchError):
|
|
181
|
+
self.provider.resolve_object_details(key, 3.14, context)
|
|
182
|
+
|
|
183
|
+
with self.assertRaises(TypeMismatchError):
|
|
184
|
+
self.provider.resolve_object_details(key, False, context)
|
|
185
|
+
|
|
186
|
+
with self.assertRaises(TypeMismatchError):
|
|
187
|
+
self.provider.resolve_object_details(key, "some string", context)
|
|
188
|
+
|
|
189
|
+
# test dictionaries with nested objects
|
|
190
|
+
with self.assertRaises(TypeMismatchError):
|
|
191
|
+
self.provider.resolve_object_details(
|
|
192
|
+
key, {"nestedMap": {"someProp": "some value"}}, context
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def test_resolve_object_details(self):
|
|
196
|
+
key = "test-flag"
|
|
197
|
+
variable_value = {"some": "value"}
|
|
198
|
+
default_value = {}
|
|
199
|
+
|
|
200
|
+
self.client.variable.return_value = Variable(
|
|
201
|
+
_id=None,
|
|
202
|
+
value=variable_value,
|
|
203
|
+
key=key,
|
|
204
|
+
type=TypeEnum.STRING,
|
|
205
|
+
isDefaulted=False,
|
|
206
|
+
defaultValue=False,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
context = EvaluationContext(targeting_key="user-1234")
|
|
210
|
+
details = self.provider.resolve_object_details(key, default_value, context)
|
|
211
|
+
|
|
212
|
+
self.assertIsNotNone(details)
|
|
213
|
+
self.assertIsInstance(details.value, dict)
|
|
214
|
+
self.assertDictEqual(details.value, variable_value)
|
|
215
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
unittest.main()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import responses
|
|
6
|
+
|
|
7
|
+
from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
|
|
8
|
+
from test.fixture.data import small_config_json
|
|
9
|
+
|
|
10
|
+
from openfeature.provider import EvaluationContext
|
|
11
|
+
from openfeature.flag_evaluation import Reason
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
17
|
+
"""
|
|
18
|
+
Tests the DevCycleProvider with actual data from the Local SDK client to confirm data translations work
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def setUp(self) -> None:
|
|
22
|
+
self.sdk_key = "dvc_server_949e4962-c624-4d20-a1ea-7f2501b2ba79"
|
|
23
|
+
self.test_config_json = small_config_json()
|
|
24
|
+
self.test_etag = "2f71454e-3279-4ca7-a8e7-802ce97bef43"
|
|
25
|
+
|
|
26
|
+
config_url = "http://localhost/config/v1/server/" + self.sdk_key + ".json"
|
|
27
|
+
|
|
28
|
+
responses.add(
|
|
29
|
+
responses.GET,
|
|
30
|
+
config_url,
|
|
31
|
+
headers={"ETag": self.test_etag},
|
|
32
|
+
json=self.test_config_json,
|
|
33
|
+
status=200,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.options = DevCycleLocalOptions(
|
|
37
|
+
config_polling_interval_ms=5000,
|
|
38
|
+
config_cdn_uri="http://localhost/",
|
|
39
|
+
disable_custom_event_logging=True,
|
|
40
|
+
disable_automatic_event_logging=True,
|
|
41
|
+
)
|
|
42
|
+
self.client = None
|
|
43
|
+
|
|
44
|
+
def tearDown(self) -> None:
|
|
45
|
+
if self.client:
|
|
46
|
+
self.client.close()
|
|
47
|
+
responses.reset()
|
|
48
|
+
|
|
49
|
+
def setup_client(self):
|
|
50
|
+
self.client = DevCycleLocalClient(self.sdk_key, self.options)
|
|
51
|
+
while not self.client.is_initialized():
|
|
52
|
+
time.sleep(0.05)
|
|
53
|
+
|
|
54
|
+
@responses.activate
|
|
55
|
+
def test_resolve_boolean_details(self):
|
|
56
|
+
self.setup_client()
|
|
57
|
+
provider = self.client.get_openfeature_provider()
|
|
58
|
+
|
|
59
|
+
key = "a-cool-new-feature"
|
|
60
|
+
expected_value = True
|
|
61
|
+
default_value = False
|
|
62
|
+
|
|
63
|
+
context = EvaluationContext(targeting_key="1234")
|
|
64
|
+
details = provider.resolve_boolean_details(key, default_value, context)
|
|
65
|
+
|
|
66
|
+
self.assertIsNotNone(details)
|
|
67
|
+
self.assertEqual(details.value, expected_value)
|
|
68
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
69
|
+
|
|
70
|
+
@responses.activate
|
|
71
|
+
def test_resolve_integer_details(self):
|
|
72
|
+
self.setup_client()
|
|
73
|
+
provider = self.client.get_openfeature_provider()
|
|
74
|
+
|
|
75
|
+
key = "num-var"
|
|
76
|
+
expected_value = 12345
|
|
77
|
+
default_value = 0
|
|
78
|
+
|
|
79
|
+
context = EvaluationContext(targeting_key="1234")
|
|
80
|
+
details = provider.resolve_integer_details(key, default_value, context)
|
|
81
|
+
|
|
82
|
+
self.assertIsNotNone(details)
|
|
83
|
+
self.assertEqual(details.value, expected_value)
|
|
84
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
85
|
+
|
|
86
|
+
@responses.activate
|
|
87
|
+
def test_resolve_float_details(self):
|
|
88
|
+
self.setup_client()
|
|
89
|
+
provider = self.client.get_openfeature_provider()
|
|
90
|
+
|
|
91
|
+
key = "float-var"
|
|
92
|
+
expected_value = 3.14159
|
|
93
|
+
default_value = 0.0001
|
|
94
|
+
|
|
95
|
+
context = EvaluationContext(targeting_key="1234")
|
|
96
|
+
details = provider.resolve_float_details(key, default_value, context)
|
|
97
|
+
|
|
98
|
+
self.assertIsNotNone(details)
|
|
99
|
+
self.assertEqual(details.value, expected_value)
|
|
100
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
101
|
+
|
|
102
|
+
@responses.activate
|
|
103
|
+
def test_resolve_string_details(self):
|
|
104
|
+
self.setup_client()
|
|
105
|
+
provider = self.client.get_openfeature_provider()
|
|
106
|
+
|
|
107
|
+
key = "string-var"
|
|
108
|
+
expected_value = "variationOn"
|
|
109
|
+
default_value = "default_value"
|
|
110
|
+
|
|
111
|
+
context = EvaluationContext(targeting_key="1234")
|
|
112
|
+
details = provider.resolve_string_details(key, default_value, context)
|
|
113
|
+
|
|
114
|
+
self.assertIsNotNone(details)
|
|
115
|
+
self.assertEqual(details.value, expected_value)
|
|
116
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
|
117
|
+
|
|
118
|
+
@responses.activate
|
|
119
|
+
def test_resolve_object_details(self):
|
|
120
|
+
self.setup_client()
|
|
121
|
+
provider = self.client.get_openfeature_provider()
|
|
122
|
+
|
|
123
|
+
key = "json-var"
|
|
124
|
+
expected_value = {
|
|
125
|
+
"displayText": "This variation is on",
|
|
126
|
+
"showDialog": True,
|
|
127
|
+
"maxUsers": 100,
|
|
128
|
+
}
|
|
129
|
+
default_value = {"default": "value"}
|
|
130
|
+
|
|
131
|
+
context = EvaluationContext(targeting_key="1234")
|
|
132
|
+
details = provider.resolve_string_details(key, default_value, context)
|
|
133
|
+
|
|
134
|
+
self.assertIsNotNone(details)
|
|
135
|
+
self.assertEqual(details.value, expected_value)
|
|
136
|
+
self.assertEqual(details.reason, Reason.TARGETING_MATCH)
|
test/test_local_client.py
CHANGED
|
@@ -217,6 +217,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
217
217
|
("string-var", "default_value", "variationOn", TypeEnum.STRING),
|
|
218
218
|
("a-cool-new-feature", False, True, TypeEnum.BOOLEAN),
|
|
219
219
|
("num-var", 0, 12345, TypeEnum.NUMBER),
|
|
220
|
+
("float-var", 0, 3.14159, TypeEnum.NUMBER),
|
|
220
221
|
(
|
|
221
222
|
"json-var",
|
|
222
223
|
{"default": "value"},
|
|
@@ -333,6 +334,15 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
333
334
|
defaultValue=None,
|
|
334
335
|
evalReason=None,
|
|
335
336
|
),
|
|
337
|
+
"float-var": Variable(
|
|
338
|
+
_id="61200363125123fca69d3a7a",
|
|
339
|
+
key="float-var",
|
|
340
|
+
type="Number",
|
|
341
|
+
value=3.14159,
|
|
342
|
+
isDefaulted=None,
|
|
343
|
+
defaultValue=None,
|
|
344
|
+
evalReason=None,
|
|
345
|
+
),
|
|
336
346
|
}
|
|
337
347
|
self.assertEqual(result, expected_variables)
|
|
338
348
|
|
test/util/test_version.py
CHANGED
|
@@ -10,7 +10,7 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
class VersionTest(unittest.TestCase):
|
|
11
11
|
def test_sdk_version(self):
|
|
12
12
|
version = sdk_version()
|
|
13
|
-
self.
|
|
13
|
+
self.assertRegex(version, r"^\d+\.\d+\.\d+$")
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
if __name__ == "__main__":
|
{devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.5.0.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|