devcycle-python-server-sdk 3.5.0__py3-none-any.whl → 3.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devcycle_python_sdk/VERSION.txt +1 -1
- devcycle_python_sdk/api/config_client.py +19 -7
- devcycle_python_sdk/api/local_bucketing.py +5 -2
- devcycle_python_sdk/bucketing-lib.release.wasm +0 -0
- devcycle_python_sdk/local_client.py +3 -1
- devcycle_python_sdk/managers/config_manager.py +59 -9
- devcycle_python_sdk/managers/event_queue_manager.py +2 -1
- devcycle_python_sdk/managers/sse_manager.py +63 -0
- devcycle_python_sdk/models/bucketed_config.py +8 -6
- devcycle_python_sdk/open_feature_provider/provider.py +5 -3
- devcycle_python_sdk/options.py +2 -0
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi +120 -0
- devcycle_python_server_sdk-3.7.1.dist-info/METADATA +20 -0
- {devcycle_python_server_sdk-3.5.0.dist-info → devcycle_python_server_sdk-3.7.1.dist-info}/RECORD +24 -22
- {devcycle_python_server_sdk-3.5.0.dist-info → devcycle_python_server_sdk-3.7.1.dist-info}/WHEEL +1 -1
- example/local_bucketing_client_example.py +1 -1
- test/api/test_config_client.py +25 -7
- test/api/test_local_bucketing.py +17 -11
- test/managers/test_config_manager.py +32 -10
- test/managers/test_event_queue_manager.py +39 -11
- test/openfeature_test/test_provider_local_sdk.py +1 -1
- test/test_local_client.py +2 -2
- devcycle_python_server_sdk-3.5.0.dist-info/METADATA +0 -20
- {devcycle_python_server_sdk-3.5.0.dist-info → devcycle_python_server_sdk-3.7.1.dist-info}/LICENSE +0 -0
- {devcycle_python_server_sdk-3.5.0.dist-info → devcycle_python_server_sdk-3.7.1.dist-info}/top_level.txt +0 -0
devcycle_python_sdk/VERSION.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.7.1
|
|
@@ -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
|
|
@@ -30,21 +30,21 @@ class ConfigAPIClient:
|
|
|
30
30
|
self.max_config_retries = 2
|
|
31
31
|
self.config_file_url = (
|
|
32
32
|
slash_join(
|
|
33
|
-
self.options.config_cdn_uri, "config", "
|
|
33
|
+
self.options.config_cdn_uri, "config", "v2", "server", self.sdk_key
|
|
34
34
|
)
|
|
35
35
|
+ ".json"
|
|
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
|
|
@@ -289,10 +289,13 @@ class LocalBucketing:
|
|
|
289
289
|
|
|
290
290
|
return bytes(ret)
|
|
291
291
|
|
|
292
|
-
def init_event_queue(self, options_json: str) -> None:
|
|
292
|
+
def init_event_queue(self, client_uuid, options_json: str) -> None:
|
|
293
293
|
with self.wasm_lock:
|
|
294
|
+
client_uuid_addr = self._new_assembly_script_string(client_uuid)
|
|
294
295
|
options_addr = self._new_assembly_script_string(options_json)
|
|
295
|
-
self.initEventQueue(
|
|
296
|
+
self.initEventQueue(
|
|
297
|
+
self.wasm_store, self.sdk_key_addr, client_uuid_addr, options_addr
|
|
298
|
+
)
|
|
296
299
|
|
|
297
300
|
def get_variable_for_user_protobuf(
|
|
298
301
|
self, user: DevCycleUser, key: str, default_value: Any
|
|
Binary file
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import uuid
|
|
3
4
|
from numbers import Real
|
|
4
5
|
from typing import Any, Dict, Union
|
|
5
6
|
|
|
@@ -28,6 +29,7 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
28
29
|
def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
|
|
29
30
|
_validate_sdk_key(sdk_key)
|
|
30
31
|
self._sdk_key = sdk_key
|
|
32
|
+
self.client_uuid = str(uuid.uuid4())
|
|
31
33
|
|
|
32
34
|
if options is None:
|
|
33
35
|
self.options: DevCycleLocalOptions = DevCycleLocalOptions()
|
|
@@ -45,7 +47,7 @@ class DevCycleLocalClient(AbstractDevCycleClient):
|
|
|
45
47
|
sdk_key, self.options, self.local_bucketing
|
|
46
48
|
)
|
|
47
49
|
self.event_queue_manager: EventQueueManager = EventQueueManager(
|
|
48
|
-
sdk_key, self.options, self.local_bucketing
|
|
50
|
+
sdk_key, self.client_uuid, self.options, self.local_bucketing
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
self._openfeature_provider = DevCycleProvider(self)
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
1
3
|
import threading
|
|
2
4
|
import time
|
|
3
|
-
import
|
|
4
|
-
import json
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import ld_eventsource.actions
|
|
9
|
+
|
|
9
10
|
from devcycle_python_sdk.api.config_client import ConfigAPIClient
|
|
11
|
+
from devcycle_python_sdk.api.local_bucketing import LocalBucketing
|
|
10
12
|
from devcycle_python_sdk.exceptions import (
|
|
11
13
|
CloudClientUnauthorizedError,
|
|
12
14
|
CloudClientError,
|
|
13
15
|
)
|
|
16
|
+
from wsgiref.handlers import format_date_time
|
|
17
|
+
from devcycle_python_sdk.options import DevCycleLocalOptions
|
|
18
|
+
from devcycle_python_sdk.managers.sse_manager import SSEManager
|
|
14
19
|
|
|
15
20
|
logger = logging.getLogger(__name__)
|
|
16
21
|
|
|
@@ -27,9 +32,12 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
27
32
|
self._sdk_key = sdk_key
|
|
28
33
|
self._options = options
|
|
29
34
|
self._local_bucketing = local_bucketing
|
|
30
|
-
|
|
35
|
+
self._sse_manager: Optional[SSEManager] = None
|
|
36
|
+
self._sse_polling_interval = 1000 * 60 * 15 * 60
|
|
37
|
+
self._sse_connected = False
|
|
31
38
|
self._config: Optional[dict] = None
|
|
32
39
|
self._config_etag: Optional[str] = None
|
|
40
|
+
self._config_lastmodified: Optional[str] = None
|
|
33
41
|
|
|
34
42
|
self._config_api_client = ConfigAPIClient(self._sdk_key, self._options)
|
|
35
43
|
|
|
@@ -40,12 +48,20 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
40
48
|
def is_initialized(self) -> bool:
|
|
41
49
|
return self._config is not None
|
|
42
50
|
|
|
43
|
-
def _get_config(self):
|
|
51
|
+
def _get_config(self, last_modified: Optional[float] = None):
|
|
44
52
|
try:
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
lm_header = self._config_lastmodified
|
|
54
|
+
if last_modified is not None:
|
|
55
|
+
lm_timestamp = datetime.fromtimestamp(last_modified)
|
|
56
|
+
lm_header = format_date_time(time.mktime(lm_timestamp.timetuple()))
|
|
57
|
+
|
|
58
|
+
new_config, new_etag, new_lastmodified = self._config_api_client.get_config(
|
|
59
|
+
config_etag=self._config_etag, last_modified=lm_header
|
|
47
60
|
)
|
|
48
61
|
|
|
62
|
+
# Abort early if the last modified is before the sent one.
|
|
63
|
+
if new_config is None and new_etag is None and new_lastmodified is None:
|
|
64
|
+
return
|
|
49
65
|
if new_config is None and new_etag == self._config_etag:
|
|
50
66
|
# api not returning data and the etag is the same
|
|
51
67
|
# no change to the config since last request
|
|
@@ -60,9 +76,18 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
60
76
|
|
|
61
77
|
self._config = new_config
|
|
62
78
|
self._config_etag = new_etag
|
|
79
|
+
self._config_lastmodified = new_lastmodified
|
|
63
80
|
|
|
64
81
|
json_config = json.dumps(self._config)
|
|
65
82
|
self._local_bucketing.store_config(json_config)
|
|
83
|
+
if self._options.enable_beta_realtime_updates:
|
|
84
|
+
if self._sse_manager is None:
|
|
85
|
+
self._sse_manager = SSEManager(
|
|
86
|
+
self.sse_state,
|
|
87
|
+
self.sse_error,
|
|
88
|
+
self.sse_message,
|
|
89
|
+
)
|
|
90
|
+
self._sse_manager.update(self._config)
|
|
66
91
|
|
|
67
92
|
if (
|
|
68
93
|
trigger_on_client_initialized
|
|
@@ -93,7 +118,32 @@ class EnvironmentConfigManager(threading.Thread):
|
|
|
93
118
|
logger.warning(
|
|
94
119
|
f"DevCycle: Error polling for config changes: {str(e)}"
|
|
95
120
|
)
|
|
96
|
-
|
|
121
|
+
if self._sse_connected:
|
|
122
|
+
time.sleep(self._sse_polling_interval / 1000.0)
|
|
123
|
+
else:
|
|
124
|
+
time.sleep(self._options.config_polling_interval_ms / 1000.0)
|
|
125
|
+
|
|
126
|
+
def sse_message(self, message: ld_eventsource.actions.Event):
|
|
127
|
+
if self._sse_connected is False:
|
|
128
|
+
self._sse_connected = True
|
|
129
|
+
logger.info("DevCycle: Connected to SSE stream")
|
|
130
|
+
logger.info(f"DevCycle: Received message: {message.data}")
|
|
131
|
+
sse_message = json.loads(message.data)
|
|
132
|
+
dvc_data = json.loads(sse_message.get("data"))
|
|
133
|
+
if (
|
|
134
|
+
dvc_data.get("type") == "refetchConfig"
|
|
135
|
+
or dvc_data.get("type") == ""
|
|
136
|
+
or dvc_data.get("type") is None
|
|
137
|
+
):
|
|
138
|
+
logger.info("DevCycle: Received refetchConfig message - updating config")
|
|
139
|
+
self._get_config(dvc_data["lastModified"])
|
|
140
|
+
|
|
141
|
+
def sse_error(self, error: ld_eventsource.actions.Fault):
|
|
142
|
+
logger.warning(f"DevCycle: Received SSE error: {error}")
|
|
143
|
+
|
|
144
|
+
def sse_state(self, state: ld_eventsource.actions.Start):
|
|
145
|
+
self._sse_connected = True
|
|
146
|
+
logger.info("DevCycle: Connected to SSE stream")
|
|
97
147
|
|
|
98
148
|
def close(self):
|
|
99
149
|
self._polling_enabled = False
|
|
@@ -31,6 +31,7 @@ class EventQueueManager(threading.Thread):
|
|
|
31
31
|
def __init__(
|
|
32
32
|
self,
|
|
33
33
|
sdk_key: str,
|
|
34
|
+
client_uuid: str,
|
|
34
35
|
options: DevCycleLocalOptions,
|
|
35
36
|
local_bucketing: LocalBucketing,
|
|
36
37
|
):
|
|
@@ -49,7 +50,7 @@ class EventQueueManager(threading.Thread):
|
|
|
49
50
|
|
|
50
51
|
# Setup the event queue inside the WASM module
|
|
51
52
|
event_options_json = json.dumps(self._options.event_queue_options())
|
|
52
|
-
self._local_bucketing.init_event_queue(event_options_json)
|
|
53
|
+
self._local_bucketing.init_event_queue(client_uuid, event_options_json)
|
|
53
54
|
|
|
54
55
|
# Only start event processing thread if event logging is enabled
|
|
55
56
|
if not (
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
import ld_eventsource
|
|
4
|
+
import ld_eventsource.actions
|
|
5
|
+
import ld_eventsource.config
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SSEManager:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
handlestate: Callable[[ld_eventsource.actions.Start], None],
|
|
13
|
+
handleerror: Callable[[ld_eventsource.actions.Fault], None],
|
|
14
|
+
handlemessage: Callable[[ld_eventsource.actions.Event], None],
|
|
15
|
+
):
|
|
16
|
+
self.client: ld_eventsource.SSEClient = None
|
|
17
|
+
self.url = ""
|
|
18
|
+
self.handlestate = handlestate
|
|
19
|
+
self.handleerror = handleerror
|
|
20
|
+
self.handlemessage = handlemessage
|
|
21
|
+
|
|
22
|
+
self.read_thread = threading.Thread(
|
|
23
|
+
target=self.read_events,
|
|
24
|
+
args=(self.handlestate, self.handleerror, self.handlemessage),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def read_events(
|
|
28
|
+
self,
|
|
29
|
+
handlestate: Callable[[ld_eventsource.actions.Start], None],
|
|
30
|
+
handleerror: Callable[[ld_eventsource.actions.Fault], None],
|
|
31
|
+
handlemessage: Callable[[ld_eventsource.actions.Event], None],
|
|
32
|
+
):
|
|
33
|
+
self.client.start()
|
|
34
|
+
for event in self.client.all:
|
|
35
|
+
if isinstance(event, ld_eventsource.actions.Start):
|
|
36
|
+
handlestate(event)
|
|
37
|
+
elif isinstance(event, ld_eventsource.actions.Fault):
|
|
38
|
+
handleerror(event)
|
|
39
|
+
elif isinstance(event, ld_eventsource.actions.Event):
|
|
40
|
+
handlemessage(event)
|
|
41
|
+
|
|
42
|
+
def update(self, config: dict):
|
|
43
|
+
if self.use_new_config(config["sse"]):
|
|
44
|
+
self.url = config["sse"]["hostname"] + config["sse"]["path"]
|
|
45
|
+
if self.client is not None:
|
|
46
|
+
self.client.close()
|
|
47
|
+
if self.read_thread.is_alive():
|
|
48
|
+
self.read_thread.join()
|
|
49
|
+
self.client = ld_eventsource.SSEClient(
|
|
50
|
+
connect=ld_eventsource.config.ConnectStrategy.http(self.url),
|
|
51
|
+
error_strategy=ld_eventsource.config.ErrorStrategy.CONTINUE,
|
|
52
|
+
)
|
|
53
|
+
self.read_thread = threading.Thread(
|
|
54
|
+
target=self.read_events,
|
|
55
|
+
args=(self.handlestate, self.handleerror, self.handlemessage),
|
|
56
|
+
)
|
|
57
|
+
self.read_thread.start()
|
|
58
|
+
|
|
59
|
+
def use_new_config(self, config: dict) -> bool:
|
|
60
|
+
new_url = config["hostname"] + config["path"]
|
|
61
|
+
if self.url == "" or self.url is None and new_url != "":
|
|
62
|
+
return True
|
|
63
|
+
return self.url != new_url
|
|
@@ -60,9 +60,9 @@ class ProjectSettings:
|
|
|
60
60
|
@classmethod
|
|
61
61
|
def from_json(cls, data: dict) -> "ProjectSettings":
|
|
62
62
|
return cls(
|
|
63
|
-
edge_db=
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
edge_db=(
|
|
64
|
+
EdgeDBSettings.from_json(data["edgeDB"]) if "edgeDB" in data else None
|
|
65
|
+
),
|
|
66
66
|
opt_in=OptInSettings.from_json(data["optIn"]) if "optIn" in data else None,
|
|
67
67
|
disable_passthrough_rollouts=data.get("disablePassthroughRollouts", False),
|
|
68
68
|
)
|
|
@@ -81,9 +81,11 @@ class Project:
|
|
|
81
81
|
id=data["_id"],
|
|
82
82
|
key=data["key"],
|
|
83
83
|
a0_organization=data["a0_organization"],
|
|
84
|
-
settings=
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
settings=(
|
|
85
|
+
ProjectSettings.from_json(data["settings"])
|
|
86
|
+
if "settings" in data
|
|
87
|
+
else None
|
|
88
|
+
),
|
|
87
89
|
)
|
|
88
90
|
|
|
89
91
|
|
|
@@ -58,9 +58,11 @@ class DevCycleProvider(AbstractProvider):
|
|
|
58
58
|
else:
|
|
59
59
|
return FlagResolutionDetails(
|
|
60
60
|
value=variable.value,
|
|
61
|
-
reason=
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
reason=(
|
|
62
|
+
Reason.DEFAULT
|
|
63
|
+
if variable.isDefaulted
|
|
64
|
+
else Reason.TARGETING_MATCH
|
|
65
|
+
),
|
|
64
66
|
)
|
|
65
67
|
except ValueError as e:
|
|
66
68
|
# occurs if the key or default value is None
|
devcycle_python_sdk/options.py
CHANGED
|
@@ -45,6 +45,7 @@ class DevCycleLocalOptions:
|
|
|
45
45
|
event_retry_delay_ms: int = 200, # milliseconds
|
|
46
46
|
disable_automatic_event_logging: bool = False,
|
|
47
47
|
disable_custom_event_logging: bool = False,
|
|
48
|
+
enable_beta_realtime_updates: bool = False,
|
|
48
49
|
):
|
|
49
50
|
self.events_api_uri = events_api_uri
|
|
50
51
|
self.config_cdn_uri = config_cdn_uri
|
|
@@ -60,6 +61,7 @@ class DevCycleLocalOptions:
|
|
|
60
61
|
self.on_client_initialized = on_client_initialized
|
|
61
62
|
self.event_request_timeout_ms = event_request_timeout_ms
|
|
62
63
|
self.event_retry_delay_ms = event_retry_delay_ms
|
|
64
|
+
self.enable_beta_realtime_updates = enable_beta_realtime_updates
|
|
63
65
|
|
|
64
66
|
if self.flush_event_queue_size >= self.max_event_queue_size:
|
|
65
67
|
logger.warning(
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from google.protobuf.internal import containers as _containers
|
|
2
|
+
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
|
3
|
+
from google.protobuf import descriptor as _descriptor
|
|
4
|
+
from google.protobuf import message as _message
|
|
5
|
+
from typing import ClassVar as _ClassVar, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
|
6
|
+
|
|
7
|
+
Bool: CustomDataType
|
|
8
|
+
Boolean: VariableType_PB
|
|
9
|
+
DESCRIPTOR: _descriptor.FileDescriptor
|
|
10
|
+
JSON: VariableType_PB
|
|
11
|
+
Null: CustomDataType
|
|
12
|
+
Num: CustomDataType
|
|
13
|
+
Number: VariableType_PB
|
|
14
|
+
Str: CustomDataType
|
|
15
|
+
String: VariableType_PB
|
|
16
|
+
|
|
17
|
+
class CustomDataValue(_message.Message):
|
|
18
|
+
__slots__ = ["boolValue", "doubleValue", "stringValue", "type"]
|
|
19
|
+
BOOLVALUE_FIELD_NUMBER: _ClassVar[int]
|
|
20
|
+
DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int]
|
|
21
|
+
STRINGVALUE_FIELD_NUMBER: _ClassVar[int]
|
|
22
|
+
TYPE_FIELD_NUMBER: _ClassVar[int]
|
|
23
|
+
boolValue: bool
|
|
24
|
+
doubleValue: float
|
|
25
|
+
stringValue: str
|
|
26
|
+
type: CustomDataType
|
|
27
|
+
def __init__(self, type: _Optional[_Union[CustomDataType, str]] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ...) -> None: ...
|
|
28
|
+
|
|
29
|
+
class DVCUser_PB(_message.Message):
|
|
30
|
+
__slots__ = ["appBuild", "appVersion", "country", "customData", "deviceModel", "email", "language", "name", "privateCustomData", "user_id"]
|
|
31
|
+
APPBUILD_FIELD_NUMBER: _ClassVar[int]
|
|
32
|
+
APPVERSION_FIELD_NUMBER: _ClassVar[int]
|
|
33
|
+
COUNTRY_FIELD_NUMBER: _ClassVar[int]
|
|
34
|
+
CUSTOMDATA_FIELD_NUMBER: _ClassVar[int]
|
|
35
|
+
DEVICEMODEL_FIELD_NUMBER: _ClassVar[int]
|
|
36
|
+
EMAIL_FIELD_NUMBER: _ClassVar[int]
|
|
37
|
+
LANGUAGE_FIELD_NUMBER: _ClassVar[int]
|
|
38
|
+
NAME_FIELD_NUMBER: _ClassVar[int]
|
|
39
|
+
PRIVATECUSTOMDATA_FIELD_NUMBER: _ClassVar[int]
|
|
40
|
+
USER_ID_FIELD_NUMBER: _ClassVar[int]
|
|
41
|
+
appBuild: NullableDouble
|
|
42
|
+
appVersion: NullableString
|
|
43
|
+
country: NullableString
|
|
44
|
+
customData: NullableCustomData
|
|
45
|
+
deviceModel: NullableString
|
|
46
|
+
email: NullableString
|
|
47
|
+
language: NullableString
|
|
48
|
+
name: NullableString
|
|
49
|
+
privateCustomData: NullableCustomData
|
|
50
|
+
user_id: str
|
|
51
|
+
def __init__(self, user_id: _Optional[str] = ..., email: _Optional[_Union[NullableString, _Mapping]] = ..., name: _Optional[_Union[NullableString, _Mapping]] = ..., language: _Optional[_Union[NullableString, _Mapping]] = ..., country: _Optional[_Union[NullableString, _Mapping]] = ..., appBuild: _Optional[_Union[NullableDouble, _Mapping]] = ..., appVersion: _Optional[_Union[NullableString, _Mapping]] = ..., deviceModel: _Optional[_Union[NullableString, _Mapping]] = ..., customData: _Optional[_Union[NullableCustomData, _Mapping]] = ..., privateCustomData: _Optional[_Union[NullableCustomData, _Mapping]] = ...) -> None: ...
|
|
52
|
+
|
|
53
|
+
class NullableCustomData(_message.Message):
|
|
54
|
+
__slots__ = ["isNull", "value"]
|
|
55
|
+
class ValueEntry(_message.Message):
|
|
56
|
+
__slots__ = ["key", "value"]
|
|
57
|
+
KEY_FIELD_NUMBER: _ClassVar[int]
|
|
58
|
+
VALUE_FIELD_NUMBER: _ClassVar[int]
|
|
59
|
+
key: str
|
|
60
|
+
value: CustomDataValue
|
|
61
|
+
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[CustomDataValue, _Mapping]] = ...) -> None: ...
|
|
62
|
+
ISNULL_FIELD_NUMBER: _ClassVar[int]
|
|
63
|
+
VALUE_FIELD_NUMBER: _ClassVar[int]
|
|
64
|
+
isNull: bool
|
|
65
|
+
value: _containers.MessageMap[str, CustomDataValue]
|
|
66
|
+
def __init__(self, value: _Optional[_Mapping[str, CustomDataValue]] = ..., isNull: bool = ...) -> None: ...
|
|
67
|
+
|
|
68
|
+
class NullableDouble(_message.Message):
|
|
69
|
+
__slots__ = ["isNull", "value"]
|
|
70
|
+
ISNULL_FIELD_NUMBER: _ClassVar[int]
|
|
71
|
+
VALUE_FIELD_NUMBER: _ClassVar[int]
|
|
72
|
+
isNull: bool
|
|
73
|
+
value: float
|
|
74
|
+
def __init__(self, value: _Optional[float] = ..., isNull: bool = ...) -> None: ...
|
|
75
|
+
|
|
76
|
+
class NullableString(_message.Message):
|
|
77
|
+
__slots__ = ["isNull", "value"]
|
|
78
|
+
ISNULL_FIELD_NUMBER: _ClassVar[int]
|
|
79
|
+
VALUE_FIELD_NUMBER: _ClassVar[int]
|
|
80
|
+
isNull: bool
|
|
81
|
+
value: str
|
|
82
|
+
def __init__(self, value: _Optional[str] = ..., isNull: bool = ...) -> None: ...
|
|
83
|
+
|
|
84
|
+
class SDKVariable_PB(_message.Message):
|
|
85
|
+
__slots__ = ["_id", "boolValue", "doubleValue", "evalReason", "key", "stringValue", "type"]
|
|
86
|
+
BOOLVALUE_FIELD_NUMBER: _ClassVar[int]
|
|
87
|
+
DOUBLEVALUE_FIELD_NUMBER: _ClassVar[int]
|
|
88
|
+
EVALREASON_FIELD_NUMBER: _ClassVar[int]
|
|
89
|
+
KEY_FIELD_NUMBER: _ClassVar[int]
|
|
90
|
+
STRINGVALUE_FIELD_NUMBER: _ClassVar[int]
|
|
91
|
+
TYPE_FIELD_NUMBER: _ClassVar[int]
|
|
92
|
+
_ID_FIELD_NUMBER: _ClassVar[int]
|
|
93
|
+
_id: str
|
|
94
|
+
boolValue: bool
|
|
95
|
+
doubleValue: float
|
|
96
|
+
evalReason: NullableString
|
|
97
|
+
key: str
|
|
98
|
+
stringValue: str
|
|
99
|
+
type: VariableType_PB
|
|
100
|
+
def __init__(self, _id: _Optional[str] = ..., type: _Optional[_Union[VariableType_PB, str]] = ..., key: _Optional[str] = ..., boolValue: bool = ..., doubleValue: _Optional[float] = ..., stringValue: _Optional[str] = ..., evalReason: _Optional[_Union[NullableString, _Mapping]] = ...) -> None: ...
|
|
101
|
+
|
|
102
|
+
class VariableForUserParams_PB(_message.Message):
|
|
103
|
+
__slots__ = ["sdkKey", "shouldTrackEvent", "user", "variableKey", "variableType"]
|
|
104
|
+
SDKKEY_FIELD_NUMBER: _ClassVar[int]
|
|
105
|
+
SHOULDTRACKEVENT_FIELD_NUMBER: _ClassVar[int]
|
|
106
|
+
USER_FIELD_NUMBER: _ClassVar[int]
|
|
107
|
+
VARIABLEKEY_FIELD_NUMBER: _ClassVar[int]
|
|
108
|
+
VARIABLETYPE_FIELD_NUMBER: _ClassVar[int]
|
|
109
|
+
sdkKey: str
|
|
110
|
+
shouldTrackEvent: bool
|
|
111
|
+
user: DVCUser_PB
|
|
112
|
+
variableKey: str
|
|
113
|
+
variableType: VariableType_PB
|
|
114
|
+
def __init__(self, sdkKey: _Optional[str] = ..., variableKey: _Optional[str] = ..., variableType: _Optional[_Union[VariableType_PB, str]] = ..., user: _Optional[_Union[DVCUser_PB, _Mapping]] = ..., shouldTrackEvent: bool = ...) -> None: ...
|
|
115
|
+
|
|
116
|
+
class VariableType_PB(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
117
|
+
__slots__ = []
|
|
118
|
+
|
|
119
|
+
class CustomDataType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
120
|
+
__slots__ = []
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: devcycle-python-server-sdk
|
|
3
|
+
Version: 3.7.1
|
|
4
|
+
Summary: DevCycle Python SDK
|
|
5
|
+
Home-page: https://github.com/devcycleHQ/python-server-sdk
|
|
6
|
+
Author-email:
|
|
7
|
+
Keywords: DevCycle
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: setuptools>=21.0.0
|
|
10
|
+
Requires-Dist: urllib3>=1.15.1
|
|
11
|
+
Requires-Dist: requests~=2.31
|
|
12
|
+
Requires-Dist: wasmtime==23.0.0
|
|
13
|
+
Requires-Dist: protobuf>=4.23.3
|
|
14
|
+
Requires-Dist: openfeature-sdk>=0.7.0
|
|
15
|
+
Requires-Dist: launchdarkly-eventsource>=1.2.0
|
|
16
|
+
Requires-Dist: responses~=0.23.1
|
|
17
|
+
Requires-Dist: dataclasses~=0.6
|
|
18
|
+
|
|
19
|
+
The DevCycle Python SDK used for feature management.
|
|
20
|
+
|
{devcycle_python_server_sdk-3.5.0.dist-info → devcycle_python_server_sdk-3.7.1.dist-info}/RECORD
RENAMED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
devcycle_python_sdk/VERSION.txt,sha256=
|
|
1
|
+
devcycle_python_sdk/VERSION.txt,sha256=Tpnn6sIcK3GpcTnKzKaV2YyT-9fdJlBfBPUnGAmsS5Q,6
|
|
2
2
|
devcycle_python_sdk/__init__.py,sha256=30TcYb9PbvVi97gwk16omCGG1UoUI9lmmU7xpvxNMvc,336
|
|
3
|
-
devcycle_python_sdk/bucketing-lib.release.wasm,sha256=
|
|
3
|
+
devcycle_python_sdk/bucketing-lib.release.wasm,sha256=AJyYTuQs-PRjAiH9yzSgn-iESa9W7Vu0AQHLJHC5QS0,214842
|
|
4
4
|
devcycle_python_sdk/cloud_client.py,sha256=UMfXvXwO4v3pZW9ZA52ivxPH1Lp4Goy5gpV_s0qCpxQ,7352
|
|
5
5
|
devcycle_python_sdk/devcycle_client.py,sha256=W5MeOslkXjTqZtoSy_Bbbug8gVoicjxshpKjDteEr6E,917
|
|
6
6
|
devcycle_python_sdk/exceptions.py,sha256=Et7fS8pEFbHkFY86T2pJoEUstwjh4_rzhJxVH8fzAns,1053
|
|
7
|
-
devcycle_python_sdk/local_client.py,sha256=
|
|
8
|
-
devcycle_python_sdk/options.py,sha256=
|
|
7
|
+
devcycle_python_sdk/local_client.py,sha256=WeXt-gWhFsaPjAp2UEmygQhzx_ygx2Jaq0z0yRHaah0,9448
|
|
8
|
+
devcycle_python_sdk/options.py,sha256=TRofo42DvtWlVlGiUfOygIBzidei5oaCZRWpZgDgr0Q,4700
|
|
9
9
|
devcycle_python_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
devcycle_python_sdk/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
11
|
devcycle_python_sdk/api/backoff.py,sha256=F6WYCloaZD9CjNu6mKBMq7K20S8-i2WVU6X05ZRoawE,303
|
|
12
12
|
devcycle_python_sdk/api/bucketing_client.py,sha256=-S0y9uAffippCTfjYgxI5j4j7YNfMxQJYUba1219nps,5111
|
|
13
|
-
devcycle_python_sdk/api/config_client.py,sha256=
|
|
13
|
+
devcycle_python_sdk/api/config_client.py,sha256=73aQAq130rTHpQrVDX6n_DwFKz0WuKxRoLDiE3vSoAM,5077
|
|
14
14
|
devcycle_python_sdk/api/event_client.py,sha256=WxkuV6geEC9MgG4FN36HeEQQa22ilq2mkOVMZkpiEk4,3356
|
|
15
|
-
devcycle_python_sdk/api/local_bucketing.py,sha256
|
|
15
|
+
devcycle_python_sdk/api/local_bucketing.py,sha256=_MAMCHviIS1n4GiU-CnnLBeEVX8BUFRqwNbA1IQi84U,16470
|
|
16
16
|
devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
devcycle_python_sdk/managers/config_manager.py,sha256=
|
|
18
|
-
devcycle_python_sdk/managers/event_queue_manager.py,sha256=
|
|
17
|
+
devcycle_python_sdk/managers/config_manager.py,sha256=U8ok13HjEi5to-GxH6hDuHEq09O5e7h51Aeui05XbdQ,5627
|
|
18
|
+
devcycle_python_sdk/managers/event_queue_manager.py,sha256=uPy1vO6OAisZjUNAhWELE_g17alTiOcPv42z-WwkGck,8110
|
|
19
|
+
devcycle_python_sdk/managers/sse_manager.py,sha256=aJKHPmvv8SR4DU9IOi-KrUSCxQoHdMwoemb2lSVfmVQ,2369
|
|
19
20
|
devcycle_python_sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
-
devcycle_python_sdk/models/bucketed_config.py,sha256=
|
|
21
|
+
devcycle_python_sdk/models/bucketed_config.py,sha256=NsdT-6RbEuDy5QuK5qlr6UqNKQQnwjoXVYN-uQp0P-E,3999
|
|
21
22
|
devcycle_python_sdk/models/error_response.py,sha256=r_dRUDAbral9UGaMYcnBsZOr9Hmkyi1SQbhGmZilgpA,442
|
|
22
23
|
devcycle_python_sdk/models/event.py,sha256=-6bhKp72mAgaoSxvhS__G4vvfQ9Psj83fc0Igelzw3o,3713
|
|
23
24
|
devcycle_python_sdk/models/feature.py,sha256=m3wxxIjC5jxhKpPLI4RcsBaNLuy-zsMqoAgc8ySPe0I,852
|
|
@@ -25,42 +26,43 @@ devcycle_python_sdk/models/platform_data.py,sha256=kIFevH5trsxI32qp7BW6glUCpipgp
|
|
|
25
26
|
devcycle_python_sdk/models/user.py,sha256=57d_n8Yy8g-zsGzVF0hmSf4McPtFPJJ0LLY_ql75FWk,6266
|
|
26
27
|
devcycle_python_sdk/models/variable.py,sha256=qp00SyCNyfYiMlTwnDSDJ4ZetRF35JHTbYZQOBxFIqY,1711
|
|
27
28
|
devcycle_python_sdk/open_feature_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
devcycle_python_sdk/open_feature_provider/provider.py,sha256=
|
|
29
|
+
devcycle_python_sdk/open_feature_provider/provider.py,sha256=VjUMhCYiMLixXUSSIjQ7kVCQrls1QD_Nltooe5EjGks,4539
|
|
29
30
|
devcycle_python_sdk/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
31
|
devcycle_python_sdk/protobuf/utils.py,sha256=zi-kVx6yvgS2c04cUobLd2IDc_ZqKkWduay9YcpjoGc,4684
|
|
31
32
|
devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=Zf4-8n67UnulC0Sz90aqLYsXCFEgywOqfiSHuXZomLk,4244
|
|
33
|
+
devcycle_python_sdk/protobuf/variableForUserParams_pb2.pyi,sha256=gjTD1KkBe5NaXixSq78eNTNbjdDjBQ0h5L4dUKKgDWQ,5659
|
|
32
34
|
devcycle_python_sdk/util/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
33
35
|
devcycle_python_sdk/util/strings.py,sha256=gH0Xm3gIXNqn6hczKPk10Jn5_9knuD7OB5MrB7bqztw,252
|
|
34
36
|
devcycle_python_sdk/util/version.py,sha256=Cz7Us_EuXP-T8GI47g2WHYdBJj_zKo-5y545IDHJAjI,253
|
|
35
37
|
example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
38
|
example/cloud_client_example.py,sha256=YD8ZsTv3dPYfbFUKraRpRT0VsJGrPwjm8ZCuA5H54yI,2463
|
|
37
|
-
example/local_bucketing_client_example.py,sha256=
|
|
39
|
+
example/local_bucketing_client_example.py,sha256=4BKNp7KE_Co9xxRTbJOfG2jQFjtFagPYsvytu93Q5_A,2829
|
|
38
40
|
example/openfeature_example.py,sha256=kO6F-Xk20l6CPiMfIxSRreshEGmHMFHRg3zbeLXa3AI,2410
|
|
39
41
|
test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
42
|
test/test_cloud_client.py,sha256=zjk302m7qKMCQryolkc1WKLFmbGWUxA0VtHlZk0rY-0,11442
|
|
41
|
-
test/test_local_client.py,sha256=
|
|
43
|
+
test/test_local_client.py,sha256=kDB5-cPJGyQtVdHV4MUM-CBBeOpN-oScr6I0OuyGPmE,13022
|
|
42
44
|
test/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
45
|
test/api/test_bucketing_client.py,sha256=M2mjACowLvbsIjXdP-w8-fwY02xqv1X1r_h9v-pXa7A,7739
|
|
44
|
-
test/api/test_config_client.py,sha256=
|
|
46
|
+
test/api/test_config_client.py,sha256=3BwMm4y-4NQcdW2DDGppoJLIF-ffgjlUJti2Qjw1D1M,5129
|
|
45
47
|
test/api/test_event_client.py,sha256=v4Xoum-PdywfczrfMV1llM5vrIJoqHybzxPLVokh09g,3310
|
|
46
|
-
test/api/test_local_bucketing.py,sha256=
|
|
48
|
+
test/api/test_local_bucketing.py,sha256=0mReZ4bsFUoJWPAIcQrWXDZ9UoC5mF9MuI-9BVB6qjM,15561
|
|
47
49
|
test/fixture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
50
|
test/fixture/data.py,sha256=6ReLxWPMhCDbJ5Ex24_iR0PJNjKzPFV1I9j_v0FVbBU,1462
|
|
49
51
|
test/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
-
test/managers/test_config_manager.py,sha256=
|
|
51
|
-
test/managers/test_event_queue_manager.py,sha256=
|
|
52
|
+
test/managers/test_config_manager.py,sha256=X0DxzWdjkvkUmarJSgzTDvO2Ma0TU6RNwL0WUwX2uA8,5963
|
|
53
|
+
test/managers/test_event_queue_manager.py,sha256=z850atk8zAlD7sYaA1OJwgCFcfC0ps4NEk4XUnNCq1U,11629
|
|
52
54
|
test/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
55
|
test/models/test_bucketed_config.py,sha256=8wCEyxaHxY21nl7ZbiFzp-IzZQBZIPIVPPzeREjumQk,858
|
|
54
56
|
test/models/test_user.py,sha256=ZNTBSKLDrqrsqZuaw5AHtc_FW4empSTaeNPZHZaFlI4,6736
|
|
55
57
|
test/openfeature_test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
58
|
test/openfeature_test/test_provider.py,sha256=UPxAD5e7NvpIhmqNFCrTY_yrKrMyYxBc2xuMAgdSGvw,7653
|
|
57
|
-
test/openfeature_test/test_provider_local_sdk.py,sha256=
|
|
59
|
+
test/openfeature_test/test_provider_local_sdk.py,sha256=ad0QizKSS-lbqlIjiQvwtKHP7PCFfYam3OKHtvsFBkU,4426
|
|
58
60
|
test/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
61
|
test/util/test_strings.py,sha256=T5H5U0af7_ld1yJC8IdDmay_Mx-iJQ5Fi5fbvKcAc7A,862
|
|
60
62
|
test/util/test_utils.py,sha256=tVBJm_qnqg-4PssLpXIxqlk0JehuiAIPs4zpi2oNVnY,6671
|
|
61
63
|
test/util/test_version.py,sha256=3Xg0_84m0bdHXz6pxd2Z85Oz9DoUC1T0Ol_WqEH_jac,335
|
|
62
|
-
devcycle_python_server_sdk-3.
|
|
63
|
-
devcycle_python_server_sdk-3.
|
|
64
|
-
devcycle_python_server_sdk-3.
|
|
65
|
-
devcycle_python_server_sdk-3.
|
|
66
|
-
devcycle_python_server_sdk-3.
|
|
64
|
+
devcycle_python_server_sdk-3.7.1.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
|
|
65
|
+
devcycle_python_server_sdk-3.7.1.dist-info/METADATA,sha256=7iRcy6f-15UNPk8GalpC-RJM-Ls_qJOKtBxjZ0tSD9o,586
|
|
66
|
+
devcycle_python_server_sdk-3.7.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
67
|
+
devcycle_python_server_sdk-3.7.1.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
|
|
68
|
+
devcycle_python_server_sdk-3.7.1.dist-info/RECORD,,
|
|
@@ -21,7 +21,7 @@ def main():
|
|
|
21
21
|
|
|
22
22
|
# create an instance of the DevCycle Client object
|
|
23
23
|
server_sdk_key = os.environ["DEVCYCLE_SERVER_SDK_KEY"]
|
|
24
|
-
options = DevCycleLocalOptions()
|
|
24
|
+
options = DevCycleLocalOptions(enable_beta_realtime_updates=True)
|
|
25
25
|
client = DevCycleLocalClient(server_sdk_key, options)
|
|
26
26
|
|
|
27
27
|
# Wait for DevCycle to initialize and load the configuration
|
test/api/test_config_client.py
CHANGED
|
@@ -2,6 +2,9 @@ import logging
|
|
|
2
2
|
import unittest
|
|
3
3
|
import uuid
|
|
4
4
|
from http import HTTPStatus
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from email.utils import formatdate
|
|
7
|
+
from time import mktime
|
|
5
8
|
|
|
6
9
|
import requests
|
|
7
10
|
import responses
|
|
@@ -27,7 +30,7 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
27
30
|
slash_join(
|
|
28
31
|
"https://config-cdn.devcycle.com/",
|
|
29
32
|
"config",
|
|
30
|
-
"
|
|
33
|
+
"v2",
|
|
31
34
|
"server",
|
|
32
35
|
self.sdk_key,
|
|
33
36
|
)
|
|
@@ -36,6 +39,9 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
36
39
|
|
|
37
40
|
options = DevCycleLocalOptions(config_retry_delay_ms=0)
|
|
38
41
|
self.test_client = ConfigAPIClient(self.sdk_key, options)
|
|
42
|
+
now = datetime.now()
|
|
43
|
+
stamp = mktime(now.timetuple())
|
|
44
|
+
self.test_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
|
|
39
45
|
self.test_etag = str(uuid.uuid4())
|
|
40
46
|
self.test_config_json: dict = small_config_json()
|
|
41
47
|
|
|
@@ -45,14 +51,20 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
45
51
|
@responses.activate
|
|
46
52
|
def test_get_config(self):
|
|
47
53
|
new_etag = str(uuid.uuid4())
|
|
54
|
+
now = datetime.now()
|
|
55
|
+
stamp = mktime(now.timetuple())
|
|
56
|
+
new_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
|
|
48
57
|
responses.add(
|
|
49
58
|
responses.GET,
|
|
50
59
|
self.config_url,
|
|
51
|
-
headers={"ETag": new_etag},
|
|
60
|
+
headers={"ETag": new_etag, "Last-Modified": new_lastmodified},
|
|
52
61
|
json=self.test_config_json,
|
|
53
62
|
)
|
|
54
|
-
result, etag = self.test_client.get_config(
|
|
63
|
+
result, etag, lastmodified = self.test_client.get_config(
|
|
64
|
+
config_etag=self.test_etag, last_modified=self.test_lastmodified
|
|
65
|
+
)
|
|
55
66
|
self.assertDictEqual(result, self.test_config_json)
|
|
67
|
+
self.assertEqual(lastmodified, new_lastmodified)
|
|
56
68
|
self.assertEqual(etag, new_etag)
|
|
57
69
|
|
|
58
70
|
@responses.activate(registry=OrderedRegistry)
|
|
@@ -68,7 +80,9 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
68
80
|
headers={"ETag": self.test_etag},
|
|
69
81
|
json=self.test_config_json,
|
|
70
82
|
)
|
|
71
|
-
result, etag = self.test_client.get_config(
|
|
83
|
+
result, etag, lastmodified = self.test_client.get_config(
|
|
84
|
+
config_etag=self.test_etag
|
|
85
|
+
)
|
|
72
86
|
|
|
73
87
|
self.assertDictEqual(result, self.test_config_json)
|
|
74
88
|
self.assertEqual(etag, self.test_etag)
|
|
@@ -83,10 +97,12 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
83
97
|
responses.add(
|
|
84
98
|
responses.GET,
|
|
85
99
|
self.config_url,
|
|
86
|
-
headers={"ETag": self.test_etag},
|
|
100
|
+
headers={"ETag": self.test_etag, "Last-Modified": self.test_lastmodified},
|
|
87
101
|
json=self.test_config_json,
|
|
88
102
|
)
|
|
89
|
-
result, etag = self.test_client.get_config(
|
|
103
|
+
result, etag, last_modified = self.test_client.get_config(
|
|
104
|
+
config_etag=self.test_etag
|
|
105
|
+
)
|
|
90
106
|
self.assertDictEqual(result, self.test_config_json)
|
|
91
107
|
self.assertEqual(etag, self.test_etag)
|
|
92
108
|
|
|
@@ -131,6 +147,8 @@ class ConfigAPIClientTest(unittest.TestCase):
|
|
|
131
147
|
status=HTTPStatus.NOT_MODIFIED,
|
|
132
148
|
)
|
|
133
149
|
|
|
134
|
-
new_config, new_etag = self.test_client.get_config(
|
|
150
|
+
new_config, new_etag, last_modified = self.test_client.get_config(
|
|
151
|
+
config_etag=self.test_etag
|
|
152
|
+
)
|
|
135
153
|
self.assertIsNone(new_config)
|
|
136
154
|
self.assertEqual(new_etag, self.test_etag)
|
test/api/test_local_bucketing.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import unittest
|
|
4
|
+
import uuid
|
|
4
5
|
|
|
5
6
|
from devcycle_python_sdk.api.local_bucketing import LocalBucketing, WASMAbortError
|
|
6
7
|
from devcycle_python_sdk.models.bucketed_config import (
|
|
@@ -24,6 +25,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
24
25
|
def setUp(self) -> None:
|
|
25
26
|
self.test_sdk_key = "dvc_server_testkey"
|
|
26
27
|
self.local_bucketing = LocalBucketing(self.test_sdk_key)
|
|
28
|
+
self.client_uuid = str(uuid.uuid4())
|
|
27
29
|
|
|
28
30
|
def test_init(self):
|
|
29
31
|
local_bucketing = LocalBucketing(self.test_sdk_key)
|
|
@@ -93,7 +95,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
93
95
|
self.local_bucketing.store_config(small_config())
|
|
94
96
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
95
97
|
self.local_bucketing.set_platform_data(platform_json)
|
|
96
|
-
self.local_bucketing.init_event_queue("{}")
|
|
98
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
97
99
|
user = DevCycleUser(user_id="test_user_id")
|
|
98
100
|
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
99
101
|
user=user, key="string-var", default_value="default value"
|
|
@@ -108,7 +110,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
108
110
|
self.local_bucketing.store_config(special_character_config())
|
|
109
111
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
110
112
|
self.local_bucketing.set_platform_data(platform_json)
|
|
111
|
-
self.local_bucketing.init_event_queue("{}")
|
|
113
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
112
114
|
user = DevCycleUser(user_id="test_user_id")
|
|
113
115
|
result = self.local_bucketing.get_variable_for_user_protobuf(
|
|
114
116
|
user=user, key="string-var", default_value="default value"
|
|
@@ -123,7 +125,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
123
125
|
self.local_bucketing.store_config(small_config())
|
|
124
126
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
125
127
|
self.local_bucketing.set_platform_data(platform_json)
|
|
126
|
-
self.local_bucketing.init_event_queue("{}")
|
|
128
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
127
129
|
user = DevCycleUser(user_id="test_user_id")
|
|
128
130
|
|
|
129
131
|
# type mismatch is handled inside the WASM and will return
|
|
@@ -137,7 +139,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
137
139
|
self.local_bucketing.store_config(small_config())
|
|
138
140
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
139
141
|
self.local_bucketing.set_platform_data(platform_json)
|
|
140
|
-
self.local_bucketing.init_event_queue("{}")
|
|
142
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
141
143
|
user = DevCycleUser(user_id="test_user_id")
|
|
142
144
|
|
|
143
145
|
result = self.local_bucketing.generate_bucketed_config(user=user)
|
|
@@ -265,7 +267,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
265
267
|
self.local_bucketing.store_config(small_config())
|
|
266
268
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
267
269
|
self.local_bucketing.set_platform_data(platform_json)
|
|
268
|
-
self.local_bucketing.init_event_queue("{}")
|
|
270
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
269
271
|
|
|
270
272
|
result = self.local_bucketing.get_event_queue_size()
|
|
271
273
|
self.assertEqual(result, 0)
|
|
@@ -276,13 +278,14 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
276
278
|
self.local_bucketing.set_platform_data(platform_json)
|
|
277
279
|
|
|
278
280
|
self.local_bucketing.init_event_queue(
|
|
281
|
+
self.client_uuid,
|
|
279
282
|
json.dumps(
|
|
280
283
|
{
|
|
281
284
|
"disableAutomaticEventLogging": False,
|
|
282
285
|
"disableCustomEventLogging": False,
|
|
283
286
|
"minEventsPerFlush": 1,
|
|
284
287
|
}
|
|
285
|
-
)
|
|
288
|
+
),
|
|
286
289
|
)
|
|
287
290
|
results = self.local_bucketing.flush_event_queue()
|
|
288
291
|
self.assertIsNotNone(results)
|
|
@@ -294,13 +297,14 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
294
297
|
self.local_bucketing.set_platform_data(platform_json)
|
|
295
298
|
|
|
296
299
|
self.local_bucketing.init_event_queue(
|
|
300
|
+
self.client_uuid,
|
|
297
301
|
json.dumps(
|
|
298
302
|
{
|
|
299
303
|
"disableAutomaticEventLogging": False,
|
|
300
304
|
"disableCustomEventLogging": False,
|
|
301
305
|
"minEventsPerFlush": 1,
|
|
302
306
|
}
|
|
303
|
-
)
|
|
307
|
+
),
|
|
304
308
|
)
|
|
305
309
|
|
|
306
310
|
# trigger two events for a single user
|
|
@@ -326,13 +330,14 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
326
330
|
self.local_bucketing.set_platform_data(platform_json)
|
|
327
331
|
|
|
328
332
|
self.local_bucketing.init_event_queue(
|
|
333
|
+
self.client_uuid,
|
|
329
334
|
json.dumps(
|
|
330
335
|
{
|
|
331
336
|
"disableAutomaticEventLogging": False,
|
|
332
337
|
"disableCustomEventLogging": False,
|
|
333
338
|
"minEventsPerFlush": 1,
|
|
334
339
|
}
|
|
335
|
-
)
|
|
340
|
+
),
|
|
336
341
|
)
|
|
337
342
|
|
|
338
343
|
with self.assertRaises(WASMAbortError):
|
|
@@ -344,13 +349,14 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
344
349
|
self.local_bucketing.set_platform_data(platform_json)
|
|
345
350
|
|
|
346
351
|
self.local_bucketing.init_event_queue(
|
|
352
|
+
self.client_uuid,
|
|
347
353
|
json.dumps(
|
|
348
354
|
{
|
|
349
355
|
"disableAutomaticEventLogging": False,
|
|
350
356
|
"disableCustomEventLogging": False,
|
|
351
357
|
"minEventsPerFlush": 1,
|
|
352
358
|
}
|
|
353
|
-
)
|
|
359
|
+
),
|
|
354
360
|
)
|
|
355
361
|
|
|
356
362
|
with self.assertRaises(WASMAbortError):
|
|
@@ -360,7 +366,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
360
366
|
self.local_bucketing.store_config(small_config())
|
|
361
367
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
362
368
|
self.local_bucketing.set_platform_data(platform_json)
|
|
363
|
-
self.local_bucketing.init_event_queue("{}")
|
|
369
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
364
370
|
user = DevCycleUser(user_id="test_user_id")
|
|
365
371
|
event = DevCycleEvent(
|
|
366
372
|
type=EventType.CustomEvent,
|
|
@@ -376,7 +382,7 @@ class LocalBucketingTest(unittest.TestCase):
|
|
|
376
382
|
self.local_bucketing.store_config(small_config())
|
|
377
383
|
platform_json = json.dumps(default_platform_data().to_json())
|
|
378
384
|
self.local_bucketing.set_platform_data(platform_json)
|
|
379
|
-
self.local_bucketing.init_event_queue("{}")
|
|
385
|
+
self.local_bucketing.init_event_queue(self.client_uuid, "{}")
|
|
380
386
|
|
|
381
387
|
event = DevCycleEvent(
|
|
382
388
|
type=EventType.AggVariableDefaulted,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import json
|
|
2
|
+
import logging
|
|
3
3
|
import time
|
|
4
|
-
import uuid
|
|
5
4
|
import unittest
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from email.utils import formatdate
|
|
8
|
+
from time import mktime
|
|
6
9
|
from unittest.mock import patch, MagicMock
|
|
7
10
|
|
|
8
11
|
from devcycle_python_sdk import DevCycleLocalOptions
|
|
@@ -18,20 +21,27 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
18
21
|
self.test_local_bucketing = MagicMock()
|
|
19
22
|
self.test_options = DevCycleLocalOptions(config_polling_interval_ms=500)
|
|
20
23
|
|
|
24
|
+
now = datetime.now()
|
|
25
|
+
stamp = mktime(now.timetuple())
|
|
26
|
+
self.test_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
|
|
21
27
|
self.test_etag = str(uuid.uuid4())
|
|
22
28
|
self.test_config_json = small_config_json()
|
|
23
29
|
self.test_config_string = json.dumps(self.test_config_json)
|
|
24
30
|
|
|
25
31
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
26
32
|
def test_init(self, mock_get_config):
|
|
27
|
-
mock_get_config.return_value = (
|
|
33
|
+
mock_get_config.return_value = (
|
|
34
|
+
self.test_config_json,
|
|
35
|
+
self.test_etag,
|
|
36
|
+
self.test_lastmodified,
|
|
37
|
+
)
|
|
28
38
|
config_manager = EnvironmentConfigManager(
|
|
29
39
|
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
30
40
|
)
|
|
31
41
|
|
|
32
42
|
# sleep to allow polling thread to load the config
|
|
33
43
|
time.sleep(0.1)
|
|
34
|
-
mock_get_config.assert_called_once_with(config_etag=None)
|
|
44
|
+
mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
|
|
35
45
|
|
|
36
46
|
self.assertTrue(config_manager._polling_enabled)
|
|
37
47
|
self.assertTrue(config_manager.is_alive())
|
|
@@ -45,7 +55,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
45
55
|
|
|
46
56
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
47
57
|
def test_init_with_client_callback(self, mock_get_config):
|
|
48
|
-
mock_get_config.return_value = (
|
|
58
|
+
mock_get_config.return_value = (
|
|
59
|
+
self.test_config_json,
|
|
60
|
+
self.test_etag,
|
|
61
|
+
self.test_lastmodified,
|
|
62
|
+
)
|
|
49
63
|
|
|
50
64
|
mock_callback = MagicMock()
|
|
51
65
|
|
|
@@ -55,7 +69,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
55
69
|
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
56
70
|
)
|
|
57
71
|
time.sleep(0.1)
|
|
58
|
-
mock_get_config.assert_called_once_with(config_etag=None)
|
|
72
|
+
mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
|
|
59
73
|
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
60
74
|
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
61
75
|
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
@@ -66,7 +80,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
66
80
|
|
|
67
81
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
68
82
|
def test_init_with_client_callback_with_error(self, mock_get_config):
|
|
69
|
-
mock_get_config.return_value = (
|
|
83
|
+
mock_get_config.return_value = (
|
|
84
|
+
self.test_config_json,
|
|
85
|
+
self.test_etag,
|
|
86
|
+
self.test_lastmodified,
|
|
87
|
+
)
|
|
70
88
|
mock_callback = MagicMock()
|
|
71
89
|
mock_callback.side_effect = Exception(
|
|
72
90
|
"Badly written callback generates an exception"
|
|
@@ -79,7 +97,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
79
97
|
)
|
|
80
98
|
# the callback error should not negatively impact initialization of the config manager
|
|
81
99
|
time.sleep(0.1)
|
|
82
|
-
mock_get_config.assert_called_once_with(config_etag=None)
|
|
100
|
+
mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
|
|
83
101
|
self.assertEqual(config_manager._config_etag, self.test_etag)
|
|
84
102
|
self.assertDictEqual(config_manager._config, self.test_config_json)
|
|
85
103
|
self.test_local_bucketing.store_config.assert_called_once_with(
|
|
@@ -105,7 +123,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
105
123
|
|
|
106
124
|
@patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
|
|
107
125
|
def test_get_config_unchanged(self, mock_get_config):
|
|
108
|
-
mock_get_config.return_value = (
|
|
126
|
+
mock_get_config.return_value = (
|
|
127
|
+
self.test_config_json,
|
|
128
|
+
self.test_etag,
|
|
129
|
+
self.test_lastmodified,
|
|
130
|
+
)
|
|
109
131
|
|
|
110
132
|
self.test_options.config_polling_interval_ms = 200
|
|
111
133
|
config_manager = EnvironmentConfigManager(
|
|
@@ -117,7 +139,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
|
|
|
117
139
|
config_manager.close()
|
|
118
140
|
|
|
119
141
|
self.test_local_bucketing.store_config.reset_mock()
|
|
120
|
-
mock_get_config.return_value = (None, config_manager._config_etag)
|
|
142
|
+
mock_get_config.return_value = (None, config_manager._config_etag, None)
|
|
121
143
|
|
|
122
144
|
# trigger refresh of the config directly
|
|
123
145
|
config_manager._get_config()
|
|
@@ -26,6 +26,7 @@ logger = logging.getLogger(__name__)
|
|
|
26
26
|
class EventQueueManagerTest(unittest.TestCase):
|
|
27
27
|
def setUp(self) -> None:
|
|
28
28
|
self.sdk_key = "dvc_server_" + str(uuid.uuid4())
|
|
29
|
+
self.client_uuid = str(uuid.uuid4())
|
|
29
30
|
|
|
30
31
|
# mock out the local bucketing to test event queue logic directly
|
|
31
32
|
self.test_local_bucketing = MagicMock()
|
|
@@ -84,7 +85,7 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
84
85
|
self.test_local_bucketing.flush_event_queue.return_value = []
|
|
85
86
|
self.test_options.event_flush_interval_ms = 100
|
|
86
87
|
manager = EventQueueManager(
|
|
87
|
-
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
88
|
+
self.sdk_key, self.client_uuid, self.test_options, self.test_local_bucketing
|
|
88
89
|
)
|
|
89
90
|
self.assertTrue(manager._should_run())
|
|
90
91
|
self.assertTrue(manager.is_alive())
|
|
@@ -94,7 +95,7 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
94
95
|
self.test_local_bucketing.flush_event_queue.return_value = []
|
|
95
96
|
self.test_options.event_flush_interval_ms = 100
|
|
96
97
|
manager = EventQueueManager(
|
|
97
|
-
self.sdk_key, self.test_options, self.test_local_bucketing
|
|
98
|
+
self.sdk_key, self.client_uuid, self.test_options, self.test_local_bucketing
|
|
98
99
|
)
|
|
99
100
|
|
|
100
101
|
manager.close()
|
|
@@ -111,7 +112,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
111
112
|
mock_publish_events.side_effect = APIClientError("Some retryable error")
|
|
112
113
|
|
|
113
114
|
manager = EventQueueManager(
|
|
114
|
-
self.sdk_key,
|
|
115
|
+
self.sdk_key,
|
|
116
|
+
self.client_uuid,
|
|
117
|
+
self.test_options_no_thread,
|
|
118
|
+
self.test_local_bucketing,
|
|
115
119
|
)
|
|
116
120
|
manager._publish_event_payload(self.test_payload)
|
|
117
121
|
|
|
@@ -129,7 +133,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
129
133
|
)
|
|
130
134
|
|
|
131
135
|
manager = EventQueueManager(
|
|
132
|
-
self.sdk_key,
|
|
136
|
+
self.sdk_key,
|
|
137
|
+
self.client_uuid,
|
|
138
|
+
self.test_options_no_thread,
|
|
139
|
+
self.test_local_bucketing,
|
|
133
140
|
)
|
|
134
141
|
manager._publish_event_payload(self.test_payload)
|
|
135
142
|
|
|
@@ -146,7 +153,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
146
153
|
self.test_local_bucketing.flush_event_queue.return_value = []
|
|
147
154
|
|
|
148
155
|
manager = EventQueueManager(
|
|
149
|
-
self.sdk_key,
|
|
156
|
+
self.sdk_key,
|
|
157
|
+
self.client_uuid,
|
|
158
|
+
self.test_options_no_thread,
|
|
159
|
+
self.test_local_bucketing,
|
|
150
160
|
)
|
|
151
161
|
manager._flush_events()
|
|
152
162
|
|
|
@@ -158,7 +168,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
158
168
|
self.test_local_bucketing.flush_event_queue.return_value = [self.test_payload]
|
|
159
169
|
|
|
160
170
|
manager = EventQueueManager(
|
|
161
|
-
self.sdk_key,
|
|
171
|
+
self.sdk_key,
|
|
172
|
+
self.client_uuid,
|
|
173
|
+
self.test_options_no_thread,
|
|
174
|
+
self.test_local_bucketing,
|
|
162
175
|
)
|
|
163
176
|
result = manager._flush_events()
|
|
164
177
|
|
|
@@ -169,7 +182,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
169
182
|
|
|
170
183
|
def test_queue_event_bad_data(self):
|
|
171
184
|
manager = EventQueueManager(
|
|
172
|
-
self.sdk_key,
|
|
185
|
+
self.sdk_key,
|
|
186
|
+
self.client_uuid,
|
|
187
|
+
self.test_options_no_thread,
|
|
188
|
+
self.test_local_bucketing,
|
|
173
189
|
)
|
|
174
190
|
with self.assertRaises(ValueError):
|
|
175
191
|
manager.queue_event(user=None, event=None)
|
|
@@ -188,7 +204,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
188
204
|
self.test_local_bucketing.get_event_queue_size.return_value = 1
|
|
189
205
|
|
|
190
206
|
manager = EventQueueManager(
|
|
191
|
-
self.sdk_key,
|
|
207
|
+
self.sdk_key,
|
|
208
|
+
self.client_uuid,
|
|
209
|
+
self.test_options_no_thread,
|
|
210
|
+
self.test_local_bucketing,
|
|
192
211
|
)
|
|
193
212
|
DevCycleUser(user_id="test_user_id")
|
|
194
213
|
event = DevCycleEvent(
|
|
@@ -207,7 +226,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
207
226
|
self.test_options_no_thread.max_event_queue_size = 100
|
|
208
227
|
|
|
209
228
|
manager = EventQueueManager(
|
|
210
|
-
self.sdk_key,
|
|
229
|
+
self.sdk_key,
|
|
230
|
+
self.client_uuid,
|
|
231
|
+
self.test_options_no_thread,
|
|
232
|
+
self.test_local_bucketing,
|
|
211
233
|
)
|
|
212
234
|
|
|
213
235
|
# queue empty, no issues
|
|
@@ -231,7 +253,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
231
253
|
|
|
232
254
|
def test_queue_aggregate_event_bad_data(self):
|
|
233
255
|
manager = EventQueueManager(
|
|
234
|
-
self.sdk_key,
|
|
256
|
+
self.sdk_key,
|
|
257
|
+
self.client_uuid,
|
|
258
|
+
self.test_options_no_thread,
|
|
259
|
+
self.test_local_bucketing,
|
|
235
260
|
)
|
|
236
261
|
with self.assertRaises(ValueError):
|
|
237
262
|
manager.queue_aggregate_event(event=None, bucketed_config=None)
|
|
@@ -252,7 +277,10 @@ class EventQueueManagerTest(unittest.TestCase):
|
|
|
252
277
|
self.test_local_bucketing.get_event_queue_size.return_value = 1
|
|
253
278
|
|
|
254
279
|
manager = EventQueueManager(
|
|
255
|
-
self.sdk_key,
|
|
280
|
+
self.sdk_key,
|
|
281
|
+
self.client_uuid,
|
|
282
|
+
self.test_options_no_thread,
|
|
283
|
+
self.test_local_bucketing,
|
|
256
284
|
)
|
|
257
285
|
event = DevCycleEvent(
|
|
258
286
|
type=EventType.AggVariableDefaulted,
|
|
@@ -23,7 +23,7 @@ class DevCycleProviderWithLocalSDKTest(unittest.TestCase):
|
|
|
23
23
|
self.test_config_json = small_config_json()
|
|
24
24
|
self.test_etag = "2f71454e-3279-4ca7-a8e7-802ce97bef43"
|
|
25
25
|
|
|
26
|
-
config_url = "http://localhost/config/
|
|
26
|
+
config_url = "http://localhost/config/v2/server/" + self.sdk_key + ".json"
|
|
27
27
|
|
|
28
28
|
responses.add(
|
|
29
29
|
responses.GET,
|
test/test_local_client.py
CHANGED
|
@@ -25,7 +25,7 @@ class DevCycleLocalClientTest(unittest.TestCase):
|
|
|
25
25
|
self.test_config_json = small_config_json()
|
|
26
26
|
self.test_etag = "2f71454e-3279-4ca7-a8e7-802ce97bef43"
|
|
27
27
|
|
|
28
|
-
config_url = "http://localhost/config/
|
|
28
|
+
config_url = "http://localhost/config/v2/server/" + self.sdk_key + ".json"
|
|
29
29
|
|
|
30
30
|
responses.add(
|
|
31
31
|
responses.GET,
|
|
@@ -371,7 +371,7 @@ def test_benchmark_variable_call(benchmark):
|
|
|
371
371
|
test_config_json = small_config_json()
|
|
372
372
|
test_etag = "2f71454e-3279-4ca7-a8e7-802ce97bef43"
|
|
373
373
|
|
|
374
|
-
config_url = "http://localhost/config/
|
|
374
|
+
config_url = "http://localhost/config/v2/server/" + sdk_key + ".json"
|
|
375
375
|
|
|
376
376
|
responses.add(
|
|
377
377
|
responses.GET,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: devcycle-python-server-sdk
|
|
3
|
-
Version: 3.5.0
|
|
4
|
-
Summary: DevCycle Python SDK
|
|
5
|
-
Home-page: https://github.com/devcycleHQ/python-server-sdk
|
|
6
|
-
Author: UNKNOWN
|
|
7
|
-
Author-email: UNKNOWN
|
|
8
|
-
License: UNKNOWN
|
|
9
|
-
Keywords: DevCycle
|
|
10
|
-
Platform: UNKNOWN
|
|
11
|
-
Requires-Dist: setuptools >=21.0.0
|
|
12
|
-
Requires-Dist: urllib3 >=1.15.1
|
|
13
|
-
Requires-Dist: requests ~=2.31.0
|
|
14
|
-
Requires-Dist: wasmtime ==9.0.0
|
|
15
|
-
Requires-Dist: protobuf >=4.23.3
|
|
16
|
-
Requires-Dist: openfeature-sdk >=0.7.0
|
|
17
|
-
|
|
18
|
-
The DevCycle Python SDK used for feature management.
|
|
19
|
-
|
|
20
|
-
|
{devcycle_python_server_sdk-3.5.0.dist-info → devcycle_python_server_sdk-3.7.1.dist-info}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|