devcycle-python-server-sdk 3.3.3__tar.gz → 3.5.0__tar.gz
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-server-sdk-3.5.0/LICENSE +21 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/PKG-INFO +1 -1
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/README.md +17 -2
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/__init__.py +1 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/api/bucketing_client.py +2 -2
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/api/config_client.py +4 -2
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/api/event_client.py +2 -2
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/api/local_bucketing.py +1 -1
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/cloud_client.py +29 -12
- devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/devcycle_client.py +40 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/local_client.py +36 -14
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/managers/config_manager.py +9 -5
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/managers/event_queue_manager.py +15 -11
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/bucketed_config.py +2 -0
- devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/user.py +159 -0
- devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/open_feature_provider/provider.py +123 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/options.py +5 -5
- devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/util/strings.py +6 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_server_sdk.egg-info/PKG-INFO +1 -1
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_server_sdk.egg-info/SOURCES.txt +11 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_server_sdk.egg-info/requires.txt +1 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/example/local_bucketing_client_example.py +1 -1
- devcycle-python-server-sdk-3.5.0/example/openfeature_example.py +77 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/pyproject.toml +17 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/api/test_bucketing_client.py +5 -1
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/api/test_config_client.py +5 -2
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/api/test_event_client.py +3 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/api/test_local_bucketing.py +15 -12
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/fixture/data.py +5 -5
- devcycle-python-server-sdk-3.5.0/test/models/test_user.py +188 -0
- devcycle-python-server-sdk-3.5.0/test/openfeature_test/__init__.py +0 -0
- devcycle-python-server-sdk-3.5.0/test/openfeature_test/test_provider.py +219 -0
- devcycle-python-server-sdk-3.5.0/test/openfeature_test/test_provider_local_sdk.py +136 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/test_local_client.py +10 -0
- devcycle-python-server-sdk-3.5.0/test/util/__init__.py +0 -0
- devcycle-python-server-sdk-3.5.0/test/util/test_strings.py +25 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/util/test_version.py +1 -1
- devcycle-python-server-sdk-3.3.3/devcycle_python_sdk/models/user.py +0 -76
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/api/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/api/backoff.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/exceptions.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/managers/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/error_response.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/event.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/feature.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/platform_data.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/models/variable.py +0 -0
- {devcycle-python-server-sdk-3.3.3/devcycle_python_sdk/protobuf → devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/open_feature_provider}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3/example → devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/protobuf}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/protobuf/utils.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/util/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_sdk/util/version.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_server_sdk.egg-info/dependency_links.txt +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/devcycle_python_server_sdk.egg-info/top_level.txt +0 -0
- {devcycle-python-server-sdk-3.3.3/test → devcycle-python-server-sdk-3.5.0/example}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/example/cloud_client_example.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/setup.cfg +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/setup.py +0 -0
- {devcycle-python-server-sdk-3.3.3/test/api → devcycle-python-server-sdk-3.5.0/test}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3/test/fixture → devcycle-python-server-sdk-3.5.0/test/api}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3/test/managers → devcycle-python-server-sdk-3.5.0/test/fixture}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3/test/models → devcycle-python-server-sdk-3.5.0/test/managers}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/managers/test_config_manager.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/managers/test_event_queue_manager.py +0 -0
- {devcycle-python-server-sdk-3.3.3/test/util → devcycle-python-server-sdk-3.5.0/test/models}/__init__.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/models/test_bucketed_config.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/test_cloud_client.py +0 -0
- {devcycle-python-server-sdk-3.3.3 → devcycle-python-server-sdk-3.5.0}/test/util/test_utils.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Taplytics Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -6,7 +6,7 @@ This SDK allows your application to interface with the [DevCycle Bucketing API](
|
|
|
6
6
|
|
|
7
7
|
## Requirements.
|
|
8
8
|
|
|
9
|
-
* Python 3.
|
|
9
|
+
* Python 3.8+
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -39,13 +39,28 @@ value = client.variable_value(user, 'variable-key', 'default-value')
|
|
|
39
39
|
|
|
40
40
|
The DevCycle client is designed to work as a singleton in your application. You should create a single instance of the client during application initialization
|
|
41
41
|
|
|
42
|
+
## OpenFeature Support
|
|
43
|
+
|
|
44
|
+
This SDK provides an alpha implementation of the [OpenFeature](https://openfeature.dev/) Provider interface. Use the `get_openfeature_provider()` function on the DevCycle SDK client to obtain a provider for OpenFeature.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from openfeature import api
|
|
48
|
+
|
|
49
|
+
devcycle_client = DevCycleLocalClient('DEVCYCLE_SERVER_SDK_KEY', options)
|
|
50
|
+
api.set_provider(devcycle_client.get_openfeature_provider())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
More details are in the [DevCycle Python SDK OpenFeature Provider](OpenFeature.md) guide.
|
|
54
|
+
|
|
55
|
+
> :warning: **OpenFeature support is in an early release and may have some rough edges**. Please report any issues to us and we'll be happy to help!
|
|
56
|
+
|
|
42
57
|
## Usage
|
|
43
58
|
|
|
44
59
|
To find usage documentation, visit our [docs](https://docs.devcycle.com/docs/sdk/server-side-sdks/python#usage).
|
|
45
60
|
|
|
46
61
|
## Development
|
|
47
62
|
|
|
48
|
-
When developing the SDK it is recommended that you have both a 3.
|
|
63
|
+
When developing the SDK it is recommended that you have both a 3.8 and 3.12 python interpreter installed in order to verify changes across different versions of python.
|
|
49
64
|
|
|
50
65
|
### Dependencies
|
|
51
66
|
|
|
@@ -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
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
|
-
from os.path import join
|
|
4
3
|
from typing import Dict, List, Optional
|
|
5
4
|
|
|
6
5
|
import requests
|
|
@@ -16,6 +15,7 @@ from devcycle_python_sdk.models.event import DevCycleEvent
|
|
|
16
15
|
from devcycle_python_sdk.models.feature import Feature
|
|
17
16
|
from devcycle_python_sdk.models.user import DevCycleUser
|
|
18
17
|
from devcycle_python_sdk.models.variable import Variable
|
|
18
|
+
from devcycle_python_sdk.util.strings import slash_join
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
@@ -33,7 +33,7 @@ class BucketingAPIClient:
|
|
|
33
33
|
self.session.max_redirects = 0
|
|
34
34
|
|
|
35
35
|
def _url(self, *path_args: str) -> str:
|
|
36
|
-
return
|
|
36
|
+
return slash_join(self.options.bucketing_api_uri, "v1", *path_args)
|
|
37
37
|
|
|
38
38
|
def request(self, method: str, url: str, **kwargs) -> dict:
|
|
39
39
|
retries_remaining = self.options.request_retries + 1
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import time
|
|
3
3
|
from http import HTTPStatus
|
|
4
|
-
from os.path import join
|
|
5
4
|
from typing import Optional, Tuple
|
|
6
5
|
|
|
7
6
|
import requests
|
|
@@ -13,6 +12,7 @@ from devcycle_python_sdk.exceptions import (
|
|
|
13
12
|
NotFoundError,
|
|
14
13
|
APIClientUnauthorizedError,
|
|
15
14
|
)
|
|
15
|
+
from devcycle_python_sdk.util.strings import slash_join
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
@@ -29,7 +29,9 @@ class ConfigAPIClient:
|
|
|
29
29
|
self.session.max_redirects = 0
|
|
30
30
|
self.max_config_retries = 2
|
|
31
31
|
self.config_file_url = (
|
|
32
|
-
|
|
32
|
+
slash_join(
|
|
33
|
+
self.options.config_cdn_uri, "config", "v1", "server", self.sdk_key
|
|
34
|
+
)
|
|
33
35
|
+ ".json"
|
|
34
36
|
)
|
|
35
37
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
-
from os.path import join
|
|
5
4
|
from typing import Optional, List
|
|
6
5
|
|
|
7
6
|
import requests
|
|
@@ -14,6 +13,7 @@ from devcycle_python_sdk.exceptions import (
|
|
|
14
13
|
APIClientUnauthorizedError,
|
|
15
14
|
)
|
|
16
15
|
from devcycle_python_sdk.models.event import UserEventsBatchRecord
|
|
16
|
+
from devcycle_python_sdk.util.strings import slash_join
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
@@ -29,7 +29,7 @@ class EventAPIClient:
|
|
|
29
29
|
}
|
|
30
30
|
self.session.max_redirects = 0
|
|
31
31
|
self.max_batch_retries = 0 # we don't retry events batches
|
|
32
|
-
self.batch_url =
|
|
32
|
+
self.batch_url = slash_join(self.options.events_api_uri, "v1/events/batch")
|
|
33
33
|
|
|
34
34
|
def publish_events(self, batch: List[UserEventsBatchRecord]) -> str:
|
|
35
35
|
"""
|
|
@@ -105,7 +105,7 @@ class LocalBucketing:
|
|
|
105
105
|
|
|
106
106
|
def __console_log_func(message_ptr) -> None:
|
|
107
107
|
message: str = self._read_assembly_script_string(message_ptr)
|
|
108
|
-
logger.warning(f"WASM console: {message!r}")
|
|
108
|
+
logger.warning(f"DevCycle: WASM console: {message!r}")
|
|
109
109
|
|
|
110
110
|
wasm_linker.define_func(
|
|
111
111
|
"env", "console.log", FuncType([ValType.i32()], []), __console_log_func
|
|
@@ -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
|
|
@@ -73,15 +90,15 @@ class DevCycleCloudClient:
|
|
|
73
90
|
try:
|
|
74
91
|
variable = self.bucketing_api.variable(key, user)
|
|
75
92
|
except CloudClientUnauthorizedError as e:
|
|
76
|
-
logger.warning("DevCycle SDK key is invalid, unable to make cloud request")
|
|
93
|
+
logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
|
|
77
94
|
raise e
|
|
78
95
|
except NotFoundError:
|
|
79
|
-
logger.warning(f"Variable not found: {key}")
|
|
96
|
+
logger.warning(f"DevCycle: Variable not found: {key}")
|
|
80
97
|
return Variable.create_default_variable(
|
|
81
98
|
key=key, default_value=default_value
|
|
82
99
|
)
|
|
83
100
|
except Exception as e:
|
|
84
|
-
logger.error(f"Error evaluating variable: {e}")
|
|
101
|
+
logger.error(f"DevCycle: Error evaluating variable: {e}")
|
|
85
102
|
return Variable.create_default_variable(
|
|
86
103
|
key=key, default_value=default_value
|
|
87
104
|
)
|
|
@@ -91,7 +108,7 @@ class DevCycleCloudClient:
|
|
|
91
108
|
# Allow default value to be a subclass of the same type as the variable
|
|
92
109
|
if not isinstance(default_value, type(variable.value)):
|
|
93
110
|
logger.warning(
|
|
94
|
-
f"Variable {key} is type {type(variable.value)}, but default value is type {type(default_value)}",
|
|
111
|
+
f"DevCycle: Variable {key} is type {type(variable.value)}, but default value is type {type(default_value)}",
|
|
95
112
|
)
|
|
96
113
|
return Variable.create_default_variable(
|
|
97
114
|
key=key, default_value=default_value
|
|
@@ -112,10 +129,10 @@ class DevCycleCloudClient:
|
|
|
112
129
|
try:
|
|
113
130
|
variable_map = self.bucketing_api.variables(user)
|
|
114
131
|
except CloudClientUnauthorizedError as e:
|
|
115
|
-
logger.warning("SDK key is invalid, unable to make cloud request")
|
|
132
|
+
logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
|
|
116
133
|
raise e
|
|
117
134
|
except Exception as e:
|
|
118
|
-
logger.error(f"Error retrieving all features for a user: {e}")
|
|
135
|
+
logger.error(f"DevCycle: Error retrieving all features for a user: {e}")
|
|
119
136
|
|
|
120
137
|
return variable_map
|
|
121
138
|
|
|
@@ -132,10 +149,10 @@ class DevCycleCloudClient:
|
|
|
132
149
|
try:
|
|
133
150
|
feature_map = self.bucketing_api.features(user)
|
|
134
151
|
except CloudClientUnauthorizedError as e:
|
|
135
|
-
logger.warning("SDK key is invalid, unable to make cloud request")
|
|
152
|
+
logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
|
|
136
153
|
raise e
|
|
137
154
|
except Exception as e:
|
|
138
|
-
logger.error(f"Error retrieving all features for a user: {e}")
|
|
155
|
+
logger.error(f"DevCycle: Error retrieving all features for a user: {e}")
|
|
139
156
|
|
|
140
157
|
return feature_map
|
|
141
158
|
|
|
@@ -160,10 +177,10 @@ class DevCycleCloudClient:
|
|
|
160
177
|
try:
|
|
161
178
|
self.bucketing_api.track(user, events)
|
|
162
179
|
except CloudClientUnauthorizedError as e:
|
|
163
|
-
logger.warning("SDK key is invalid, unable to make cloud request")
|
|
180
|
+
logger.warning("DevCycle: SDK key is invalid, unable to make cloud request")
|
|
164
181
|
raise e
|
|
165
182
|
except Exception as e:
|
|
166
|
-
logger.error(f"Error tracking event: {e}")
|
|
183
|
+
logger.error(f"DevCycle: Error tracking event: {e}")
|
|
167
184
|
|
|
168
185
|
|
|
169
186
|
def _validate_sdk_key(sdk_key: str) -> None:
|
|
@@ -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
|
|
|
@@ -57,7 +71,9 @@ class DevCycleLocalClient:
|
|
|
57
71
|
are not permitted
|
|
58
72
|
"""
|
|
59
73
|
if not self.is_initialized():
|
|
60
|
-
logger.debug(
|
|
74
|
+
logger.debug(
|
|
75
|
+
"DevCycle: set_client_custom_data called before client has initialized"
|
|
76
|
+
)
|
|
61
77
|
return
|
|
62
78
|
|
|
63
79
|
if custom_data:
|
|
@@ -65,7 +81,7 @@ class DevCycleLocalClient:
|
|
|
65
81
|
custom_data_json = json.dumps(custom_data)
|
|
66
82
|
self.local_bucketing.set_client_custom_data(custom_data_json)
|
|
67
83
|
except Exception as e:
|
|
68
|
-
logger.error("Error setting custom data: " + str(e))
|
|
84
|
+
logger.error("DevCycle: Error setting custom data: " + str(e))
|
|
69
85
|
|
|
70
86
|
def variable_value(self, user: DevCycleUser, key: str, default_value: Any) -> Any:
|
|
71
87
|
"""
|
|
@@ -92,7 +108,7 @@ class DevCycleLocalClient:
|
|
|
92
108
|
raise ValueError("Missing parameter: defaultValue")
|
|
93
109
|
|
|
94
110
|
if not self.is_initialized():
|
|
95
|
-
logger.debug("variable called before client has initialized")
|
|
111
|
+
logger.debug("DevCycle: variable called before client has initialized")
|
|
96
112
|
try:
|
|
97
113
|
self.event_queue_manager.queue_aggregate_event(
|
|
98
114
|
event=DevCycleEvent(
|
|
@@ -102,7 +118,7 @@ class DevCycleLocalClient:
|
|
|
102
118
|
)
|
|
103
119
|
except Exception as e:
|
|
104
120
|
logger.warning(
|
|
105
|
-
f"Unable to track AggVariableDefaulted event for Variable {key}: {e}"
|
|
121
|
+
f"DevCycle: Unable to track AggVariableDefaulted event for Variable {key}: {e}"
|
|
106
122
|
)
|
|
107
123
|
return Variable.create_default_variable(key, default_value)
|
|
108
124
|
|
|
@@ -113,9 +129,9 @@ class DevCycleLocalClient:
|
|
|
113
129
|
if variable:
|
|
114
130
|
return variable
|
|
115
131
|
except VariableTypeMismatchError:
|
|
116
|
-
logger.debug("Variable type mismatch, returning default value")
|
|
132
|
+
logger.debug("DevCycle: Variable type mismatch, returning default value")
|
|
117
133
|
except Exception as e:
|
|
118
|
-
logger.warning(f"Error retrieving variable for user: {e}")
|
|
134
|
+
logger.warning(f"DevCycle: Error retrieving variable for user: {e}")
|
|
119
135
|
|
|
120
136
|
return Variable.create_default_variable(key, default_value)
|
|
121
137
|
|
|
@@ -137,7 +153,9 @@ class DevCycleLocalClient:
|
|
|
137
153
|
_validate_user(user)
|
|
138
154
|
|
|
139
155
|
if not self.is_initialized():
|
|
140
|
-
logger.warning(
|
|
156
|
+
logger.warning(
|
|
157
|
+
"DevCycle: all_variables called before client has initialized"
|
|
158
|
+
)
|
|
141
159
|
return {}
|
|
142
160
|
|
|
143
161
|
variable_map: Dict[str, Variable] = {}
|
|
@@ -145,7 +163,9 @@ class DevCycleLocalClient:
|
|
|
145
163
|
try:
|
|
146
164
|
return self.local_bucketing.generate_bucketed_config(user).variables
|
|
147
165
|
except Exception as e:
|
|
148
|
-
logger.exception(
|
|
166
|
+
logger.exception(
|
|
167
|
+
f"DevCycle: Error retrieving all variables for a user: {e}"
|
|
168
|
+
)
|
|
149
169
|
return {}
|
|
150
170
|
|
|
151
171
|
return variable_map
|
|
@@ -159,14 +179,16 @@ class DevCycleLocalClient:
|
|
|
159
179
|
_validate_user(user)
|
|
160
180
|
|
|
161
181
|
if not self.is_initialized():
|
|
162
|
-
logger.warning(
|
|
182
|
+
logger.warning(
|
|
183
|
+
"DevCycle: all_features called before client has initialized"
|
|
184
|
+
)
|
|
163
185
|
return {}
|
|
164
186
|
|
|
165
187
|
feature_map: Dict[str, Feature] = {}
|
|
166
188
|
try:
|
|
167
189
|
return self.local_bucketing.generate_bucketed_config(user).features
|
|
168
190
|
except Exception as e:
|
|
169
|
-
logger.exception(f"Error retrieving all features for a user: {e}")
|
|
191
|
+
logger.exception(f"DevCycle: Error retrieving all features for a user: {e}")
|
|
170
192
|
|
|
171
193
|
return feature_map
|
|
172
194
|
|
|
@@ -186,13 +208,13 @@ class DevCycleLocalClient:
|
|
|
186
208
|
raise ValueError("Missing parameter: type")
|
|
187
209
|
|
|
188
210
|
if not self.is_initialized():
|
|
189
|
-
logger.debug("track called before client has initialized")
|
|
211
|
+
logger.debug("DevCycle: track called before client has initialized")
|
|
190
212
|
return
|
|
191
213
|
|
|
192
214
|
try:
|
|
193
215
|
self.event_queue_manager.queue_event(user, user_event)
|
|
194
216
|
except Exception as e:
|
|
195
|
-
logger.error(f"Error tracking event: {e}")
|
|
217
|
+
logger.error(f"DevCycle: Error tracking event: {e}")
|
|
196
218
|
|
|
197
219
|
def close(self) -> None:
|
|
198
220
|
"""
|
|
@@ -52,7 +52,7 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
52
52
|
return
|
|
53
53
|
elif new_config is None:
|
|
54
54
|
logger.warning(
|
|
55
|
-
"Config CDN fetch returned no data with a different etag"
|
|
55
|
+
"DevCycle: Config CDN fetch returned no data with a different etag"
|
|
56
56
|
)
|
|
57
57
|
return
|
|
58
58
|
|
|
@@ -73,12 +73,14 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
73
73
|
except Exception as e:
|
|
74
74
|
# consume any error
|
|
75
75
|
logger.warning(
|
|
76
|
-
f"Error received from on_client_initialized callback: {str(e)}"
|
|
76
|
+
f"DevCycle: Error received from on_client_initialized callback: {str(e)}"
|
|
77
77
|
)
|
|
78
78
|
except CloudClientError as e:
|
|
79
|
-
logger.warning(f"Config fetch failed. Status: {str(e)}")
|
|
79
|
+
logger.warning(f"DevCycle: Config fetch failed. Status: {str(e)}")
|
|
80
80
|
except CloudClientUnauthorizedError:
|
|
81
|
-
logger.error(
|
|
81
|
+
logger.error(
|
|
82
|
+
"DevCycle: Unauthorized to get config. Aborting config polling."
|
|
83
|
+
)
|
|
82
84
|
self._polling_enabled = False
|
|
83
85
|
|
|
84
86
|
def run(self):
|
|
@@ -88,7 +90,9 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
88
90
|
except Exception as e:
|
|
89
91
|
if self._polling_enabled:
|
|
90
92
|
# Only log a warning if we're still polling
|
|
91
|
-
logger.warning(
|
|
93
|
+
logger.warning(
|
|
94
|
+
f"DevCycle: Error polling for config changes: {str(e)}"
|
|
95
|
+
)
|
|
92
96
|
time.sleep(self._options.config_polling_interval_ms / 1000.0)
|
|
93
97
|
|
|
94
98
|
def close(self):
|
|
@@ -91,15 +91,17 @@ class EventQueueManager(threading.Thread):
|
|
|
91
91
|
try:
|
|
92
92
|
payloads = self._local_bucketing.flush_event_queue()
|
|
93
93
|
except Exception as e:
|
|
94
|
-
logger.error(f"Error flushing event payloads: {str(e)}")
|
|
94
|
+
logger.error(f"DevCycle: Error flushing event payloads: {str(e)}")
|
|
95
95
|
|
|
96
96
|
event_count = 0
|
|
97
97
|
if payloads:
|
|
98
|
-
logger.debug(f"Flush {len(payloads)} event payloads")
|
|
98
|
+
logger.debug(f"DevCycle: Flush {len(payloads)} event payloads")
|
|
99
99
|
for payload in payloads:
|
|
100
100
|
event_count += payload.eventCount
|
|
101
101
|
self._publish_event_payload(payload)
|
|
102
|
-
logger.debug(
|
|
102
|
+
logger.debug(
|
|
103
|
+
f"DevCycle: Flush {event_count} events, for {len(payloads)} users"
|
|
104
|
+
)
|
|
103
105
|
return event_count
|
|
104
106
|
|
|
105
107
|
def _publish_event_payload(self, payload: FlushPayload) -> None:
|
|
@@ -109,20 +111,20 @@ class EventQueueManager(threading.Thread):
|
|
|
109
111
|
self._local_bucketing.on_event_payload_success(payload.payloadId)
|
|
110
112
|
except APIClientUnauthorizedError:
|
|
111
113
|
logger.error(
|
|
112
|
-
"Unauthorized to publish events, please check your SDK key"
|
|
114
|
+
"DevCycle: Unauthorized to publish events, please check your SDK key"
|
|
113
115
|
)
|
|
114
116
|
# stop the thread
|
|
115
117
|
self._stop_running()
|
|
116
118
|
self._local_bucketing.on_event_payload_failure(payload.payloadId, False)
|
|
117
119
|
except NotFoundError as e:
|
|
118
120
|
logger.error(
|
|
119
|
-
f"Unable to reach the DevCycle Events API service: {str(e)}"
|
|
121
|
+
f"DevCycle: Unable to reach the DevCycle Events API service: {str(e)}"
|
|
120
122
|
)
|
|
121
123
|
self._stop_running()
|
|
122
124
|
self._local_bucketing.on_event_payload_failure(payload.payloadId, False)
|
|
123
125
|
except APIClientError as e:
|
|
124
126
|
logger.warning(
|
|
125
|
-
f"Error publishing events to DevCycle Events API service: {str(e)}"
|
|
127
|
+
f"DevCycle: Error publishing events to DevCycle Events API service: {str(e)}"
|
|
126
128
|
)
|
|
127
129
|
self._local_bucketing.on_event_payload_failure(payload.payloadId, True)
|
|
128
130
|
|
|
@@ -142,7 +144,7 @@ class EventQueueManager(threading.Thread):
|
|
|
142
144
|
try:
|
|
143
145
|
self._flush_events()
|
|
144
146
|
except Exception as e:
|
|
145
|
-
logger.warning(f"
|
|
147
|
+
logger.warning(f"DevCycle: flushing events: {str(e)}")
|
|
146
148
|
|
|
147
149
|
self._sleep()
|
|
148
150
|
|
|
@@ -155,12 +157,14 @@ class EventQueueManager(threading.Thread):
|
|
|
155
157
|
# Because the sleeping between batches is interruptible, this is only
|
|
156
158
|
# providing time for an in-flight batch to finish.
|
|
157
159
|
if not self._wait_for_exit(1.0):
|
|
158
|
-
logger.error(
|
|
160
|
+
logger.error(
|
|
161
|
+
"DevCycle: Timed out waiting for event flushing thread to stop"
|
|
162
|
+
)
|
|
159
163
|
|
|
160
164
|
try:
|
|
161
165
|
self._flush_events()
|
|
162
166
|
except Exception as e:
|
|
163
|
-
logger.warning(f"
|
|
167
|
+
logger.warning(f"DevCycle: flushing events when closing client: {str(e)}")
|
|
164
168
|
|
|
165
169
|
def queue_event(self, user: DevCycleUser, event: DevCycleEvent) -> None:
|
|
166
170
|
if user is None:
|
|
@@ -175,7 +179,7 @@ class EventQueueManager(threading.Thread):
|
|
|
175
179
|
try:
|
|
176
180
|
self._check_queue_status()
|
|
177
181
|
except QueueFullError:
|
|
178
|
-
logger.warning("Event queue is full, dropping user event")
|
|
182
|
+
logger.warning("DevCycle: Event queue is full, dropping user event")
|
|
179
183
|
return
|
|
180
184
|
|
|
181
185
|
user_json = json.dumps(user.to_json())
|
|
@@ -197,7 +201,7 @@ class EventQueueManager(threading.Thread):
|
|
|
197
201
|
try:
|
|
198
202
|
self._check_queue_status()
|
|
199
203
|
except QueueFullError:
|
|
200
|
-
logger.warning("Event queue is full, dropping aggregate event")
|
|
204
|
+
logger.warning("DevCycle: Event queue is full, dropping aggregate event")
|
|
201
205
|
return
|
|
202
206
|
|
|
203
207
|
event_json = json.dumps(event.to_json())
|
|
@@ -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
|
|