devcycle-python-server-sdk 3.3.6__py3-none-any.whl → 3.6.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 (28) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/__init__.py +1 -0
  3. devcycle_python_sdk/api/config_client.py +18 -6
  4. devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
  5. devcycle_python_sdk/cloud_client.py +19 -2
  6. devcycle_python_sdk/devcycle_client.py +40 -0
  7. devcycle_python_sdk/local_client.py +16 -2
  8. devcycle_python_sdk/managers/config_manager.py +7 -2
  9. devcycle_python_sdk/models/bucketed_config.py +10 -6
  10. devcycle_python_sdk/models/user.py +86 -3
  11. devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
  12. devcycle_python_sdk/open_feature_provider/provider.py +125 -0
  13. {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/METADATA +3 -2
  14. {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/RECORD +28 -20
  15. {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/WHEEL +1 -1
  16. example/local_bucketing_client_example.py +1 -1
  17. example/openfeature_example.py +77 -0
  18. test/api/test_config_client.py +24 -6
  19. test/api/test_local_bucketing.py +15 -12
  20. test/managers/test_config_manager.py +32 -10
  21. test/models/test_user.py +188 -0
  22. test/openfeature_test/__init__.py +0 -0
  23. test/openfeature_test/test_provider.py +219 -0
  24. test/openfeature_test/test_provider_local_sdk.py +136 -0
  25. test/test_local_client.py +10 -0
  26. test/util/test_version.py +1 -1
  27. {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/LICENSE +0 -0
  28. {devcycle_python_server_sdk-3.3.6.dist-info → devcycle_python_server_sdk-3.6.0.dist-info}/top_level.txt +0 -0
@@ -1 +1 @@
1
- 3.3.6
1
+ 3.6.0
@@ -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
@@ -2,7 +2,7 @@ import logging
2
2
  import time
3
3
  from http import HTTPStatus
4
4
  from typing import Optional, Tuple
5
-
5
+ import email.utils
6
6
  import requests
7
7
 
8
8
  from devcycle_python_sdk.api.backoff import exponential_backoff
@@ -36,15 +36,15 @@ class ConfigAPIClient:
36
36
  )
37
37
 
38
38
  def get_config(
39
- self, config_etag: Optional[str] = None
40
- ) -> Tuple[Optional[dict], Optional[str]]:
39
+ self, config_etag: Optional[str] = None, last_modified: Optional[str] = None
40
+ ) -> Tuple[Optional[dict], Optional[str], Optional[str]]:
41
41
  """
42
42
  Get the config from the server. If the config_etag is provided, the server will only return the config if it
43
43
  has changed since the last request. If the config hasn't changed, the server will return a 304 Not Modified
44
44
  response.
45
45
 
46
46
  :param config_etag: The etag of the last config request
47
-
47
+ :param last_modified: Last modified RFC1123 Timestamp of the stored config
48
48
  :return: A tuple containing the config and the etag of the config. If the config hasn't changed since the last
49
49
  request, the config will be None and the etag will be the same as the last request.
50
50
  """
@@ -56,6 +56,8 @@ class ConfigAPIClient:
56
56
  headers = {}
57
57
  if config_etag:
58
58
  headers["If-None-Match"] = config_etag
59
+ if last_modified:
60
+ headers["If-Modified-Since"] = last_modified
59
61
 
60
62
  attempts = 1
61
63
  while retries_remaining > 0:
@@ -74,7 +76,7 @@ class ConfigAPIClient:
74
76
  elif res.status_code == HTTPStatus.NOT_MODIFIED:
75
77
  # the config hasn't changed since the last request
76
78
  # don't return anything
77
- return None, config_etag
79
+ return None, config_etag, last_modified
78
80
  elif res.status_code == HTTPStatus.NOT_FOUND:
79
81
  # Not a retryable error
80
82
  raise NotFoundError(url)
@@ -113,6 +115,16 @@ class ConfigAPIClient:
113
115
  raise APIClientError(message="Retries exceeded", cause=request_error)
114
116
 
115
117
  new_etag = res.headers.get("ETag", None)
118
+ new_lastmodified = res.headers.get("Last-Modified", None)
119
+
120
+ if last_modified and new_lastmodified:
121
+ stored_lm = email.utils.parsedate_to_datetime(last_modified)
122
+ response_lm = email.utils.parsedate_to_datetime(new_lastmodified)
123
+ if stored_lm > response_lm:
124
+ logger.warning(
125
+ "Request returned a last modified header older than the current stored timestamp. not saving config"
126
+ )
127
+ return None, None, None
116
128
 
117
129
  data: dict = res.json()
118
- return data, new_etag
130
+ return data, new_etag, new_lastmodified
@@ -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
 
@@ -30,6 +30,7 @@ class EnvironmentConfigManager(threading.Thread):
30
30
 
31
31
  self._config: Optional[dict] = None
32
32
  self._config_etag: Optional[str] = None
33
+ self._config_lastmodified: Optional[str] = None
33
34
 
34
35
  self._config_api_client = ConfigAPIClient(self._sdk_key, self._options)
35
36
 
@@ -42,10 +43,13 @@ class EnvironmentConfigManager(threading.Thread):
42
43
 
43
44
  def _get_config(self):
44
45
  try:
45
- new_config, new_etag = self._config_api_client.get_config(
46
- config_etag=self._config_etag
46
+ new_config, new_etag, new_lastmodified = self._config_api_client.get_config(
47
+ config_etag=self._config_etag, last_modified=self._config_lastmodified
47
48
  )
48
49
 
50
+ # Abort early if the last modified is before the sent one.
51
+ if new_config is None and new_etag is None and new_lastmodified is None:
52
+ return
49
53
  if new_config is None and new_etag == self._config_etag:
50
54
  # api not returning data and the etag is the same
51
55
  # no change to the config since last request
@@ -60,6 +64,7 @@ class EnvironmentConfigManager(threading.Thread):
60
64
 
61
65
  self._config = new_config
62
66
  self._config_etag = new_etag
67
+ self._config_lastmodified = new_lastmodified
63
68
 
64
69
  json_config = json.dumps(self._config)
65
70
  self._local_bucketing.store_config(json_config)
@@ -55,14 +55,16 @@ 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":
61
62
  return cls(
62
- edge_db=EdgeDBSettings.from_json(data["edgeDB"])
63
- if "edgeDB" in data
64
- else None,
63
+ edge_db=(
64
+ EdgeDBSettings.from_json(data["edgeDB"]) if "edgeDB" in data else None
65
+ ),
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
 
@@ -79,9 +81,11 @@ class Project:
79
81
  id=data["_id"],
80
82
  key=data["key"],
81
83
  a0_organization=data["a0_organization"],
82
- settings=ProjectSettings.from_json(data["settings"])
83
- if "settings" in data
84
- else None,
84
+ settings=(
85
+ ProjectSettings.from_json(data["settings"])
86
+ if "settings" in data
87
+ else None
88
+ ),
85
89
  )
86
90
 
87
91
 
@@ -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, str]] = None
18
+ customData: Optional[Dict[str, Any]] = None
17
19
  createdDate: datetime = field(default_factory=lambda: datetime.utcnow())
18
- privateCustomData: Optional[Dict[str, str]] = None
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,125 @@
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=(
62
+ Reason.DEFAULT
63
+ if variable.isDefaulted
64
+ else Reason.TARGETING_MATCH
65
+ ),
66
+ )
67
+ except ValueError as e:
68
+ # occurs if the key or default value is None
69
+ raise InvalidContextError(str(e))
70
+ else:
71
+ return FlagResolutionDetails(
72
+ value=default_value,
73
+ reason=Reason.ERROR,
74
+ error_code=ErrorCode.PROVIDER_NOT_READY,
75
+ )
76
+
77
+ def resolve_boolean_details(
78
+ self,
79
+ flag_key: str,
80
+ default_value: bool,
81
+ evaluation_context: Optional[EvaluationContext] = None,
82
+ ) -> FlagResolutionDetails[bool]:
83
+ return self._resolve(flag_key, default_value, evaluation_context)
84
+
85
+ def resolve_string_details(
86
+ self,
87
+ flag_key: str,
88
+ default_value: str,
89
+ evaluation_context: Optional[EvaluationContext] = None,
90
+ ) -> FlagResolutionDetails[str]:
91
+ return self._resolve(flag_key, default_value, evaluation_context)
92
+
93
+ def resolve_integer_details(
94
+ self,
95
+ flag_key: str,
96
+ default_value: int,
97
+ evaluation_context: Optional[EvaluationContext] = None,
98
+ ) -> FlagResolutionDetails[int]:
99
+ return self._resolve(flag_key, default_value, evaluation_context)
100
+
101
+ def resolve_float_details(
102
+ self,
103
+ flag_key: str,
104
+ default_value: float,
105
+ evaluation_context: Optional[EvaluationContext] = None,
106
+ ) -> FlagResolutionDetails[float]:
107
+ return self._resolve(flag_key, default_value, evaluation_context)
108
+
109
+ def resolve_object_details(
110
+ self,
111
+ flag_key: str,
112
+ default_value: Union[dict, list],
113
+ evaluation_context: Optional[EvaluationContext] = None,
114
+ ) -> FlagResolutionDetails[Union[dict, list]]:
115
+ if not isinstance(default_value, dict):
116
+ raise TypeMismatchError("Default value must be a flat dictionary")
117
+
118
+ if default_value:
119
+ for k, v in default_value.items():
120
+ if not isinstance(v, (str, int, float, bool)) or v is None:
121
+ raise TypeMismatchError(
122
+ "Default value must be a flat dictionary containing only strings, numbers, booleans or None values"
123
+ )
124
+
125
+ return self._resolve(flag_key, default_value, evaluation_context)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devcycle-python-server-sdk
3
- Version: 3.3.6
3
+ Version: 3.6.0
4
4
  Summary: DevCycle Python SDK
5
5
  Home-page: https://github.com/devcycleHQ/python-server-sdk
6
6
  Author: UNKNOWN
@@ -10,9 +10,10 @@ Keywords: DevCycle
10
10
  Platform: UNKNOWN
11
11
  Requires-Dist: setuptools >=21.0.0
12
12
  Requires-Dist: urllib3 >=1.15.1
13
- Requires-Dist: requests ~=2.31.0
13
+ Requires-Dist: requests ~=2.31
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
 
@@ -1,28 +1,31 @@
1
- devcycle_python_sdk/VERSION.txt,sha256=NnbsD-Jmzgwy0kLCa3npOIpV_H1Uvl9iyKC5uFowDYw,6
2
- devcycle_python_sdk/__init__.py,sha256=vOoglQ8L7RsHTp-GSa8g7k2AAPrR_obhiDSC09LpG8w,265
3
- devcycle_python_sdk/bucketing-lib.release.wasm,sha256=oNVNiF5h5YO4kejqbdogK44DVgWgwn5UJZ7KUsR9aZA,200891
4
- devcycle_python_sdk/cloud_client.py,sha256=wkDNwz8GFfnJybD1e96Lr41SY_2saQUTMD5gByFIbIg,6766
1
+ devcycle_python_sdk/VERSION.txt,sha256=E4ID4_YEoI-tDE0KKX7-LEhiGYINzdBT5yt9hDUEx6E,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=h5NrP4-x26cz-yla7OUy4SL7VJgplUFH8Fj0jTqz2Os,8845
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
10
11
  devcycle_python_sdk/api/backoff.py,sha256=F6WYCloaZD9CjNu6mKBMq7K20S8-i2WVU6X05ZRoawE,303
11
12
  devcycle_python_sdk/api/bucketing_client.py,sha256=-S0y9uAffippCTfjYgxI5j4j7YNfMxQJYUba1219nps,5111
12
- devcycle_python_sdk/api/config_client.py,sha256=BbPAjvB8c4engCiVnrRDXAXabHJLKYc1uxCtoPpvffg,4293
13
+ devcycle_python_sdk/api/config_client.py,sha256=NnqHmPcwK__LNqPzmKo5EBqULAQEteH1YmzRE_eIXJs,5077
13
14
  devcycle_python_sdk/api/event_client.py,sha256=WxkuV6geEC9MgG4FN36HeEQQa22ilq2mkOVMZkpiEk4,3356
14
15
  devcycle_python_sdk/api/local_bucketing.py,sha256=-TfG3Jb0syVF3sqzzlFyKxHqLQ6a37C4mW0oj53J-yQ,16332
15
16
  devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- devcycle_python_sdk/managers/config_manager.py,sha256=_-1kWwyyAcwVF_DibXjnehXi3Qj6HR8orYfgNfgWJOc,3248
17
+ devcycle_python_sdk/managers/config_manager.py,sha256=YfgGKW4mtMhqdwWZiZ_Y67AYLC9VTpTjYhpDgfGFA4g,3599
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=rSKpxMTF-IbUVQ9gAWrWnb9Q9YF94ATqEFWNWuoQntk,3814
20
+ devcycle_python_sdk/models/bucketed_config.py,sha256=NsdT-6RbEuDy5QuK5qlr6UqNKQQnwjoXVYN-uQp0P-E,3999
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=BGMtVM6shDX7rUfGfBbI55IUEsQ_4qrnmtwTqOcB-8A,2616
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=VjUMhCYiMLixXUSSIjQ7kVCQrls1QD_Nltooe5EjGks,4539
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,28 +34,33 @@ 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=zSJsllQ5aSvUTj-VFvt0w7pVTMRW6z-gTj9R7ZNPlyE,2797
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=mEBhq0gnHzjE-i_w4hMrmfhoHDJvCXGt3G8lx0EA9OE,12671
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
- test/api/test_config_client.py,sha256=oZMBlXGw4UHb5-rSNF_ARTrAjvbvkbFgb8GAK4ExR14,4413
44
+ test/api/test_config_client.py,sha256=jT48_llov5QQs0jNDbHANRqySVT1HamXm82uPZXKSrE,5129
41
45
  test/api/test_event_client.py,sha256=v4Xoum-PdywfczrfMV1llM5vrIJoqHybzxPLVokh09g,3310
42
- test/api/test_local_bucketing.py,sha256=E7alurvIScT0nilcuY2xGLCXzwxHrNWPBGdWxG6bR5A,15156
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
46
- test/managers/test_config_manager.py,sha256=_MtvyxK06xpBzzEsN1zsLdYrk3DsPsbCmSj7XorINmI,5366
50
+ test/managers/test_config_manager.py,sha256=X0DxzWdjkvkUmarJSgzTDvO2Ma0TU6RNwL0WUwX2uA8,5963
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=lx0HFefK_I5D35bmjEc7t5_6oYB4LBWnG-zxqCz1lV4,343
54
- devcycle_python_server_sdk-3.3.6.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
55
- devcycle_python_server_sdk-3.3.6.dist-info/METADATA,sha256=r-YR9iEwUiLS5koBavumhlUhjc41IZ8X4di9iYUiwmw,471
56
- devcycle_python_server_sdk-3.3.6.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
57
- devcycle_python_server_sdk-3.3.6.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
58
- devcycle_python_server_sdk-3.3.6.dist-info/RECORD,,
61
+ test/util/test_version.py,sha256=3Xg0_84m0bdHXz6pxd2Z85Oz9DoUC1T0Ol_WqEH_jac,335
62
+ devcycle_python_server_sdk-3.6.0.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
63
+ devcycle_python_server_sdk-3.6.0.dist-info/METADATA,sha256=mEHWiA0xrfnZ4-GLr2uyc0_HOPyEc2ENqvD6CLe8SKo,508
64
+ devcycle_python_server_sdk-3.6.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
65
+ devcycle_python_server_sdk-3.6.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
66
+ devcycle_python_server_sdk-3.6.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.43.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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 = "test-boolean-variable"
10
+ VARIABLE_KEY = "python-example-tests"
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13