devcycle-python-server-sdk 2.0.1__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devcycle_python_sdk/VERSION.txt +1 -1
- devcycle_python_sdk/__init__.py +3 -3
- devcycle_python_sdk/api/backoff.py +11 -0
- devcycle_python_sdk/api/bucketing_client.py +17 -25
- devcycle_python_sdk/api/config_client.py +116 -0
- devcycle_python_sdk/api/event_client.py +95 -0
- devcycle_python_sdk/api/local_bucketing.py +429 -2
- devcycle_python_sdk/{dvc_cloud_client.py → cloud_client.py} +69 -41
- devcycle_python_sdk/exceptions.py +25 -3
- devcycle_python_sdk/local_client.py +220 -0
- devcycle_python_sdk/managers/config_manager.py +83 -15
- devcycle_python_sdk/managers/event_queue_manager.py +213 -8
- devcycle_python_sdk/models/bucketed_config.py +150 -0
- devcycle_python_sdk/models/event.py +111 -8
- devcycle_python_sdk/models/feature.py +12 -0
- devcycle_python_sdk/models/platform_data.py +33 -0
- devcycle_python_sdk/models/user.py +52 -7
- devcycle_python_sdk/models/variable.py +17 -5
- devcycle_python_sdk/options.py +106 -0
- devcycle_python_sdk/protobuf/__init__.py +0 -0
- devcycle_python_sdk/protobuf/utils.py +129 -0
- devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +46 -0
- devcycle_python_sdk/util/version.py +1 -1
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/METADATA +3 -1
- devcycle_python_server_sdk-3.0.0.dist-info/RECORD +55 -0
- example/{example.py → cloud_client_example.py} +24 -20
- example/local_bucketing_client_example.py +74 -0
- test/api/test_bucketing_client.py +10 -5
- test/api/test_config_client.py +133 -0
- test/api/test_event_client.py +97 -0
- test/api/test_local_bucketing.py +379 -4
- test/fixture/__init__.py +0 -0
- test/fixture/data.py +57 -0
- test/managers/test_config_manager.py +120 -5
- test/managers/test_event_queue_manager.py +259 -5
- test/models/__init__.py +0 -0
- test/models/test_bucketed_config.py +26 -0
- test/{test_dvc_cloud_client.py → test_cloud_client.py} +11 -17
- test/test_local_client.py +397 -0
- test/util/test_utils.py +183 -0
- test/util/test_version.py +2 -2
- devcycle_python_sdk/dvc_local_client.py +0 -152
- devcycle_python_sdk/dvc_options.py +0 -45
- devcycle_python_server_sdk-2.0.1.dist-info/RECORD +0 -41
- test/test_bucketing_client.py +0 -228
- test/test_dvc_local_client.py +0 -143
- test/test_version.py +0 -18
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/LICENSE +0 -0
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/WHEEL +0 -0
- {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# ruff: noqa: N815
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .user import DevCycleUser
|
|
7
|
+
from .variable import Variable
|
|
8
|
+
from .feature import Feature
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class EdgeDBSettings:
|
|
13
|
+
enabled: bool
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_json(cls, data: dict) -> "EdgeDBSettings":
|
|
17
|
+
return cls(
|
|
18
|
+
enabled=data.get("enabled", False),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class OptInColors:
|
|
24
|
+
primary: str
|
|
25
|
+
secondary: str
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_json(cls, data: dict) -> "OptInColors":
|
|
29
|
+
return cls(
|
|
30
|
+
primary=data.get("primary", ""),
|
|
31
|
+
secondary=data.get("secondary", ""),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class OptInSettings:
|
|
37
|
+
enabled: bool
|
|
38
|
+
title: str
|
|
39
|
+
description: str
|
|
40
|
+
image_url: str
|
|
41
|
+
colors: OptInColors
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_json(cls, data: dict) -> "OptInSettings":
|
|
45
|
+
return cls(
|
|
46
|
+
enabled=data.get("enabled", False),
|
|
47
|
+
title=data.get("title", ""),
|
|
48
|
+
description=data.get("description", ""),
|
|
49
|
+
image_url=data.get("imageURL", ""),
|
|
50
|
+
colors=OptInColors.from_json(data.get("colors", "")),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ProjectSettings:
|
|
56
|
+
edge_db: Optional[EdgeDBSettings]
|
|
57
|
+
opt_in: Optional[OptInSettings]
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_json(cls, data: dict) -> "ProjectSettings":
|
|
61
|
+
return cls(
|
|
62
|
+
edge_db=EdgeDBSettings.from_json(data["edgeDB"])
|
|
63
|
+
if "edgeDB" in data
|
|
64
|
+
else None,
|
|
65
|
+
opt_in=OptInSettings.from_json(data["optIn"]) if "optIn" in data else None,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class Project:
|
|
71
|
+
id: str
|
|
72
|
+
key: str
|
|
73
|
+
a0_organization: str
|
|
74
|
+
settings: Optional[ProjectSettings]
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_json(cls, data: dict) -> "Project":
|
|
78
|
+
return cls(
|
|
79
|
+
id=data["_id"],
|
|
80
|
+
key=data["key"],
|
|
81
|
+
a0_organization=data["a0_organization"],
|
|
82
|
+
settings=ProjectSettings.from_json(data["settings"])
|
|
83
|
+
if "settings" in data
|
|
84
|
+
else None,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Environment:
|
|
90
|
+
id: str
|
|
91
|
+
key: str
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def from_json(cls, data: dict) -> "Environment":
|
|
95
|
+
return cls(
|
|
96
|
+
id=data["_id"],
|
|
97
|
+
key=data["key"],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class FeatureVariation:
|
|
103
|
+
feature: str
|
|
104
|
+
variation: str
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_json(cls, data: dict) -> "FeatureVariation":
|
|
108
|
+
return cls(
|
|
109
|
+
feature=data["_feature"],
|
|
110
|
+
variation=data["_variation"],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(order=False)
|
|
115
|
+
class BucketedConfig:
|
|
116
|
+
project: Project
|
|
117
|
+
environment: Environment
|
|
118
|
+
features: Dict[str, Feature]
|
|
119
|
+
feature_variation_map: Dict[str, str]
|
|
120
|
+
variable_variation_map: Dict[str, FeatureVariation]
|
|
121
|
+
variables: Dict[str, Variable]
|
|
122
|
+
known_variable_keys: List[float]
|
|
123
|
+
|
|
124
|
+
user: Optional[DevCycleUser] = None
|
|
125
|
+
|
|
126
|
+
def to_json(self):
|
|
127
|
+
return {
|
|
128
|
+
key: getattr(self, key)
|
|
129
|
+
for key in self.__dataclass_fields__
|
|
130
|
+
if getattr(self, key) is not None
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def from_json(cls, data: dict) -> "BucketedConfig":
|
|
135
|
+
return cls(
|
|
136
|
+
project=Project.from_json(data["project"]),
|
|
137
|
+
environment=Environment.from_json(data["environment"]),
|
|
138
|
+
features={
|
|
139
|
+
k: Feature.from_json(v) for k, v in data.get("features", {}).items()
|
|
140
|
+
},
|
|
141
|
+
feature_variation_map=data.get("featureVariationMap", {}),
|
|
142
|
+
variable_variation_map={
|
|
143
|
+
k: FeatureVariation.from_json(v)
|
|
144
|
+
for k, v in data.get("variableVariationMap", {}).items()
|
|
145
|
+
},
|
|
146
|
+
variables={
|
|
147
|
+
k: Variable.from_json(v) for k, v in data.get("variables", {}).items()
|
|
148
|
+
},
|
|
149
|
+
known_variable_keys=data.get("knownVariableKeys", []),
|
|
150
|
+
)
|
|
@@ -1,20 +1,123 @@
|
|
|
1
1
|
# ruff: noqa: N815
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
from typing import Dict, Optional
|
|
4
|
-
from
|
|
3
|
+
from typing import Dict, Optional, Any, List
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from .user import DevCycleUser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EventType:
|
|
10
|
+
VariableEvaluated = "variableEvaluated"
|
|
11
|
+
AggVariableEvaluated = "aggVariableEvaluated"
|
|
12
|
+
VariableDefaulted = "variableDefaulted"
|
|
13
|
+
AggVariableDefaulted = "aggVariableDefaulted"
|
|
14
|
+
CustomEvent = "customEvent"
|
|
5
15
|
|
|
6
16
|
|
|
7
17
|
@dataclass(order=False)
|
|
8
|
-
class
|
|
18
|
+
class DevCycleEvent:
|
|
9
19
|
type: Optional[str] = None
|
|
10
20
|
target: Optional[str] = None
|
|
11
|
-
date:
|
|
21
|
+
date: datetime = field(default_factory=lambda: datetime.utcnow())
|
|
12
22
|
value: Optional[int] = None
|
|
13
|
-
metaData: Dict[str, str] =
|
|
23
|
+
metaData: Optional[Dict[str, str]] = None
|
|
14
24
|
|
|
15
|
-
def to_json(self):
|
|
16
|
-
|
|
25
|
+
def to_json(self, use_bucketing_api_format: bool = False) -> Dict[str, Any]:
|
|
26
|
+
json_dict = {
|
|
17
27
|
key: getattr(self, key)
|
|
18
28
|
for key in self.__dataclass_fields__
|
|
19
|
-
if getattr(self, key) is not None
|
|
29
|
+
if getattr(self, key) is not None and key != "date"
|
|
20
30
|
}
|
|
31
|
+
if self.date:
|
|
32
|
+
if use_bucketing_api_format:
|
|
33
|
+
# convert to timestamp in milliseconds as required by the bucketing API
|
|
34
|
+
json_dict["date"] = int(self.date.timestamp() * 1000)
|
|
35
|
+
else:
|
|
36
|
+
# convert to UTC and format as ISO string
|
|
37
|
+
json_dict["date"] = self.date.astimezone(tz=timezone.utc).isoformat()
|
|
38
|
+
return json_dict
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(order=False)
|
|
42
|
+
class RequestEvent:
|
|
43
|
+
"""
|
|
44
|
+
An event generated by local bucketing event that can be sent to the Events API
|
|
45
|
+
Not interfaced by developers, purely for internal use
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
type: str
|
|
49
|
+
user_id: str
|
|
50
|
+
# will be a timestamp in iso format
|
|
51
|
+
clientDate: str
|
|
52
|
+
# will be a timestamp in iso format
|
|
53
|
+
date: str
|
|
54
|
+
target: Optional[str] = None
|
|
55
|
+
customType: Optional[str] = None
|
|
56
|
+
value: Optional[float] = 0
|
|
57
|
+
featureVars: Dict[str, str] = field(default_factory=dict)
|
|
58
|
+
metaData: Optional[Dict[str, str]] = None
|
|
59
|
+
|
|
60
|
+
def to_json(self):
|
|
61
|
+
json_obj = {}
|
|
62
|
+
for key in self.__dataclass_fields__:
|
|
63
|
+
json_obj[key] = getattr(self, key)
|
|
64
|
+
|
|
65
|
+
return json_obj
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_json(cls, data: dict) -> "RequestEvent":
|
|
69
|
+
return cls(
|
|
70
|
+
type=data["type"],
|
|
71
|
+
user_id=data["user_id"],
|
|
72
|
+
clientDate=data["clientDate"],
|
|
73
|
+
date=data["date"],
|
|
74
|
+
target=data.get("target"),
|
|
75
|
+
customType=data.get("customType"),
|
|
76
|
+
value=data.get("value", 0),
|
|
77
|
+
featureVars=data.get("featureVars", {}),
|
|
78
|
+
metaData=data.get("metaData"),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass()
|
|
83
|
+
class UserEventsBatchRecord:
|
|
84
|
+
"""
|
|
85
|
+
A collection of events generated by local bucketing library for a single user
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
user: DevCycleUser
|
|
89
|
+
events: List[RequestEvent]
|
|
90
|
+
|
|
91
|
+
def to_json(self) -> Dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"user": self.user.to_json(),
|
|
94
|
+
"events": [event.to_json() for event in self.events],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_json(cls, data: dict) -> "UserEventsBatchRecord":
|
|
99
|
+
return cls(
|
|
100
|
+
user=DevCycleUser.from_json(data["user"]),
|
|
101
|
+
events=[RequestEvent.from_json(element) for element in data["events"]],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass()
|
|
106
|
+
class FlushPayload:
|
|
107
|
+
"""
|
|
108
|
+
A collection of events exported by the local bucketing library that can be sent to the Events API as a batch
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
payloadId: str
|
|
112
|
+
records: List[UserEventsBatchRecord]
|
|
113
|
+
eventCount: int
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_json(cls, data: dict) -> "FlushPayload":
|
|
117
|
+
return cls(
|
|
118
|
+
payloadId=data["payloadId"],
|
|
119
|
+
eventCount=data["eventCount"],
|
|
120
|
+
records=[
|
|
121
|
+
UserEventsBatchRecord.from_json(element) for element in data["records"]
|
|
122
|
+
],
|
|
123
|
+
)
|
|
@@ -19,3 +19,15 @@ class Feature:
|
|
|
19
19
|
for key in self.__dataclass_fields__
|
|
20
20
|
if getattr(self, key) is not None
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_json(cls, data: dict) -> "Feature":
|
|
25
|
+
return cls(
|
|
26
|
+
_id=data["_id"],
|
|
27
|
+
key=data["key"],
|
|
28
|
+
type=data["type"],
|
|
29
|
+
_variation=data["_variation"],
|
|
30
|
+
variationName=data["variationName"],
|
|
31
|
+
variationKey=data["variationKey"],
|
|
32
|
+
evalReason=data.get("evalReason"),
|
|
33
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# ruff: noqa: N815
|
|
2
|
+
import platform
|
|
3
|
+
import socket
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from devcycle_python_sdk.util.version import sdk_version
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(order=False)
|
|
9
|
+
class PlatformData:
|
|
10
|
+
sdkType: str
|
|
11
|
+
sdkVersion: str
|
|
12
|
+
platformVersion: str
|
|
13
|
+
deviceModel: str
|
|
14
|
+
platform: str
|
|
15
|
+
hostname: str
|
|
16
|
+
|
|
17
|
+
def to_json(self):
|
|
18
|
+
return {
|
|
19
|
+
key: getattr(self, key)
|
|
20
|
+
for key in self.__dataclass_fields__
|
|
21
|
+
if getattr(self, key) is not None
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def default_platform_data() -> PlatformData:
|
|
26
|
+
return PlatformData(
|
|
27
|
+
sdkType="server",
|
|
28
|
+
sdkVersion=sdk_version(),
|
|
29
|
+
platformVersion=platform.python_version(),
|
|
30
|
+
deviceModel=platform.platform(),
|
|
31
|
+
platform="Python",
|
|
32
|
+
hostname=socket.gethostname(),
|
|
33
|
+
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# ruff: noqa: N815
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
|
-
from
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from typing import Dict, Optional
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
@dataclass(order=False)
|
|
8
|
-
class
|
|
8
|
+
class DevCycleUser:
|
|
9
9
|
user_id: str
|
|
10
10
|
email: Optional[str] = None
|
|
11
11
|
name: Optional[str] = None
|
|
@@ -13,10 +13,10 @@ class User:
|
|
|
13
13
|
country: Optional[str] = None
|
|
14
14
|
appVersion: Optional[str] = None
|
|
15
15
|
appBuild: Optional[str] = None
|
|
16
|
-
customData: Dict[str, str] =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
lastSeenDate: Optional[
|
|
16
|
+
customData: Optional[Dict[str, str]] = None
|
|
17
|
+
createdDate: datetime = field(default_factory=lambda: datetime.utcnow())
|
|
18
|
+
privateCustomData: Optional[Dict[str, str]] = None
|
|
19
|
+
lastSeenDate: Optional[datetime] = None
|
|
20
20
|
platform: Optional[str] = None
|
|
21
21
|
platformVersion: Optional[str] = None
|
|
22
22
|
deviceModel: Optional[str] = None
|
|
@@ -24,8 +24,53 @@ class User:
|
|
|
24
24
|
sdkVersion: Optional[str] = None
|
|
25
25
|
|
|
26
26
|
def to_json(self):
|
|
27
|
-
|
|
27
|
+
json_dict = {
|
|
28
28
|
key: getattr(self, key)
|
|
29
29
|
for key in self.__dataclass_fields__
|
|
30
30
|
if getattr(self, key) is not None
|
|
31
|
+
and key not in ["createdDate", "lastSeenDate"]
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
if self.createdDate:
|
|
35
|
+
json_dict["createdDate"] = self.createdDate.astimezone(
|
|
36
|
+
tz=timezone.utc
|
|
37
|
+
).isoformat()
|
|
38
|
+
if self.lastSeenDate:
|
|
39
|
+
json_dict["lastSeenDate"] = self.lastSeenDate.astimezone(
|
|
40
|
+
tz=timezone.utc
|
|
41
|
+
).isoformat()
|
|
42
|
+
return json_dict
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_json(cls, data: dict) -> "DevCycleUser":
|
|
46
|
+
if "createdDate" in data:
|
|
47
|
+
created_date = datetime.fromisoformat(
|
|
48
|
+
data["createdDate"].replace("Z", "+00:00")
|
|
49
|
+
)
|
|
50
|
+
else:
|
|
51
|
+
created_date = datetime.utcnow()
|
|
52
|
+
|
|
53
|
+
last_seen_date = None
|
|
54
|
+
if "lastSeenDate" in data:
|
|
55
|
+
last_seen_date = datetime.fromisoformat(
|
|
56
|
+
data["lastSeenDate"].replace("Z", "+00:00")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return cls(
|
|
60
|
+
user_id=data["user_id"],
|
|
61
|
+
email=data.get("email"),
|
|
62
|
+
name=data.get("name"),
|
|
63
|
+
language=data.get("language"),
|
|
64
|
+
country=data.get("country"),
|
|
65
|
+
appVersion=data.get("appVersion"),
|
|
66
|
+
appBuild=data.get("appBuild"),
|
|
67
|
+
customData=data.get("customData"),
|
|
68
|
+
privateCustomData=data.get("privateCustomData"),
|
|
69
|
+
createdDate=created_date,
|
|
70
|
+
lastSeenDate=last_seen_date,
|
|
71
|
+
platform=data.get("platform"),
|
|
72
|
+
platformVersion=data.get("platformVersion"),
|
|
73
|
+
deviceModel=data.get("deviceModel"),
|
|
74
|
+
sdkType=data.get("sdkType"),
|
|
75
|
+
sdkVersion=data.get("sdkVersion"),
|
|
76
|
+
)
|
|
@@ -26,12 +26,12 @@ def determine_variable_type(value: Any) -> str:
|
|
|
26
26
|
@dataclass(order=False)
|
|
27
27
|
class Variable:
|
|
28
28
|
_id: Optional[str]
|
|
29
|
-
key:
|
|
30
|
-
type:
|
|
31
|
-
value:
|
|
29
|
+
key: str
|
|
30
|
+
type: str
|
|
31
|
+
value: Any = None
|
|
32
32
|
isDefaulted: Optional[bool] = False
|
|
33
|
-
defaultValue:
|
|
34
|
-
|
|
33
|
+
defaultValue: Any = None
|
|
34
|
+
evalReason: Optional[str] = None
|
|
35
35
|
|
|
36
36
|
def to_json(self):
|
|
37
37
|
return {
|
|
@@ -40,6 +40,18 @@ class Variable:
|
|
|
40
40
|
if getattr(self, key) is not None
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_json(cls, data: dict) -> "Variable":
|
|
45
|
+
return cls(
|
|
46
|
+
_id=data["_id"],
|
|
47
|
+
key=data["key"],
|
|
48
|
+
type=data["type"],
|
|
49
|
+
value=data["value"],
|
|
50
|
+
isDefaulted=data.get("isDefaulted", None),
|
|
51
|
+
defaultValue=data.get("defaultValue"),
|
|
52
|
+
evalReason=data.get("evalReason"),
|
|
53
|
+
)
|
|
54
|
+
|
|
43
55
|
@staticmethod
|
|
44
56
|
def create_default_variable(key: str, default_value: Any) -> "Variable":
|
|
45
57
|
var_type = determine_variable_type(default_value)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Callable, Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DevCycleCloudOptions:
|
|
8
|
+
"""
|
|
9
|
+
Options for configuring the DevCycle Cloud SDK.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
enable_edge_db: bool = False,
|
|
15
|
+
bucketing_api_uri: str = "https://bucketing-api.devcycle.com/",
|
|
16
|
+
request_timeout: int = 5, # seconds
|
|
17
|
+
request_retries: int = 5,
|
|
18
|
+
retry_delay: int = 200, # milliseconds
|
|
19
|
+
):
|
|
20
|
+
self.enable_edge_db = enable_edge_db
|
|
21
|
+
self.bucketing_api_uri = bucketing_api_uri
|
|
22
|
+
self.request_timeout = request_timeout
|
|
23
|
+
self.request_retries = request_retries
|
|
24
|
+
self.retry_delay = retry_delay
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DevCycleLocalOptions:
|
|
28
|
+
"""
|
|
29
|
+
Options for configuring the DevCycle Local Bucketing SDK.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
config_cdn_uri: str = "https://config-cdn.devcycle.com/",
|
|
35
|
+
config_request_timeout_ms: int = 5000,
|
|
36
|
+
config_polling_interval_ms: int = 1000,
|
|
37
|
+
config_retry_delay_ms: int = 200, # milliseconds
|
|
38
|
+
on_client_initialized: Optional[Callable] = None,
|
|
39
|
+
events_api_uri: str = "https://events.devcycle.com/",
|
|
40
|
+
max_event_queue_size: int = 2000,
|
|
41
|
+
event_flush_interval_ms: int = 10000,
|
|
42
|
+
flush_event_queue_size: int = 1000,
|
|
43
|
+
event_request_chunk_size: int = 100,
|
|
44
|
+
event_request_timeout_ms: int = 10000,
|
|
45
|
+
event_retry_delay_ms: int = 200, # milliseconds
|
|
46
|
+
disable_automatic_event_logging: bool = False,
|
|
47
|
+
disable_custom_event_logging: bool = False,
|
|
48
|
+
):
|
|
49
|
+
self.events_api_uri = events_api_uri
|
|
50
|
+
self.config_cdn_uri = config_cdn_uri
|
|
51
|
+
self.config_request_timeout_ms = config_request_timeout_ms
|
|
52
|
+
self.config_polling_interval_ms = config_polling_interval_ms
|
|
53
|
+
self.max_event_queue_size = max_event_queue_size
|
|
54
|
+
self.event_flush_interval_ms = event_flush_interval_ms
|
|
55
|
+
self.flush_event_queue_size = flush_event_queue_size
|
|
56
|
+
self.event_request_chunk_size = event_request_chunk_size
|
|
57
|
+
self.disable_automatic_event_logging = disable_automatic_event_logging
|
|
58
|
+
self.disable_custom_event_logging = disable_custom_event_logging
|
|
59
|
+
self.config_retry_delay_ms = config_retry_delay_ms
|
|
60
|
+
self.on_client_initialized = on_client_initialized
|
|
61
|
+
self.event_request_timeout_ms = event_request_timeout_ms
|
|
62
|
+
self.event_retry_delay_ms = event_retry_delay_ms
|
|
63
|
+
|
|
64
|
+
if self.flush_event_queue_size >= self.max_event_queue_size:
|
|
65
|
+
logger.warning(
|
|
66
|
+
f"flush_event_queue_size: {self.flush_event_queue_size} must be smaller than max_event_queue_size: {self.max_event_queue_size}"
|
|
67
|
+
)
|
|
68
|
+
self.flush_event_queue_size = self.max_event_queue_size - 1
|
|
69
|
+
|
|
70
|
+
if self.event_request_chunk_size > self.flush_event_queue_size:
|
|
71
|
+
logger.warning(
|
|
72
|
+
f"event_request_chunk_size: {self.event_request_chunk_size} must be smaller than flush_event_queue_size: {self.flush_event_queue_size}"
|
|
73
|
+
)
|
|
74
|
+
self.event_request_chunk_size = 100
|
|
75
|
+
|
|
76
|
+
if self.event_request_chunk_size > self.max_event_queue_size:
|
|
77
|
+
logger.warning(
|
|
78
|
+
f"event_request_chunk_size: {self.event_request_chunk_size} must be smaller than max_event_queue_size: { self.max_event_queue_size}"
|
|
79
|
+
)
|
|
80
|
+
self.event_request_chunk_size = 100
|
|
81
|
+
|
|
82
|
+
if self.flush_event_queue_size > 20000:
|
|
83
|
+
logger.warning(
|
|
84
|
+
f"flush_event_queue_size: {self.flush_event_queue_size} must be smaller than 20,000"
|
|
85
|
+
)
|
|
86
|
+
self.flush_event_queue_size = 20000
|
|
87
|
+
|
|
88
|
+
if self.max_event_queue_size > 20000:
|
|
89
|
+
logger.warning(
|
|
90
|
+
f"max_event_queue_size: {self.max_event_queue_size} must be smaller than 20,000"
|
|
91
|
+
)
|
|
92
|
+
self.max_event_queue_size = 20000
|
|
93
|
+
|
|
94
|
+
def event_queue_options(self) -> Dict[str, Any]:
|
|
95
|
+
"""
|
|
96
|
+
Returns a read-only view of the options that are relevant to the event subsystem
|
|
97
|
+
"""
|
|
98
|
+
return {
|
|
99
|
+
"flushEventsMS": self.event_flush_interval_ms,
|
|
100
|
+
"disableAutomaticEventLogging": self.disable_automatic_event_logging,
|
|
101
|
+
"disableCustomEventLogging": self.disable_custom_event_logging,
|
|
102
|
+
"maxEventsPerFlush": self.max_event_queue_size,
|
|
103
|
+
"minEventsPerFlush": self.flush_event_queue_size,
|
|
104
|
+
"eventRequestChunkSize": self.event_request_chunk_size,
|
|
105
|
+
"eventsAPIBasePath": self.events_api_uri,
|
|
106
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from devcycle_python_sdk.models.variable import TypeEnum, Variable
|
|
8
|
+
from devcycle_python_sdk.models.user import DevCycleUser
|
|
9
|
+
|
|
10
|
+
import devcycle_python_sdk.protobuf.variableForUserParams_pb2 as pb2
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_nullable_double(val: Optional[float]) -> pb2.NullableDouble: # type: ignore
|
|
16
|
+
if val and not math.isnan(val):
|
|
17
|
+
return pb2.NullableDouble(value=val, isNull=False) # type: ignore
|
|
18
|
+
else:
|
|
19
|
+
return pb2.NullableDouble(isNull=True) # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_nullable_string(val: Optional[str]) -> pb2.NullableString: # type: ignore
|
|
23
|
+
if val is None:
|
|
24
|
+
return pb2.NullableString(isNull=True) # type: ignore
|
|
25
|
+
else:
|
|
26
|
+
return pb2.NullableString(value=val, isNull=False) # type: ignore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_nullable_custom_data(val: Optional[dict]) -> pb2.NullableCustomData: # type: ignore
|
|
30
|
+
if val:
|
|
31
|
+
values = dict()
|
|
32
|
+
for key, value in val.items():
|
|
33
|
+
if value is None:
|
|
34
|
+
values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Null) # type: ignore
|
|
35
|
+
elif isinstance(value, bool):
|
|
36
|
+
values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Bool, boolValue=value) # type: ignore
|
|
37
|
+
elif isinstance(value, str):
|
|
38
|
+
values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Str, stringValue=value) # type: ignore
|
|
39
|
+
elif isinstance(value, (int, float)):
|
|
40
|
+
values[key] = pb2.CustomDataValue(type=pb2.CustomDataType.Num, doubleValue=value) # type: ignore
|
|
41
|
+
else:
|
|
42
|
+
logger.warning(
|
|
43
|
+
f"Custom Data contains data type that can't be written, will be ignored. Key: {key}, Type: {str(type(value))}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return pb2.NullableCustomData(value=values, isNull=False) # type: ignore
|
|
47
|
+
else:
|
|
48
|
+
return pb2.NullableCustomData(isNull=True) # type: ignore
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def convert_type_enum_to_variable_type(var_type: str) -> pb2.VariableType_PB:
|
|
52
|
+
if var_type == TypeEnum.BOOLEAN:
|
|
53
|
+
return pb2.VariableType_PB.Boolean
|
|
54
|
+
elif var_type == TypeEnum.STRING:
|
|
55
|
+
return pb2.VariableType_PB.String
|
|
56
|
+
elif var_type == TypeEnum.NUMBER:
|
|
57
|
+
return pb2.VariableType_PB.Number
|
|
58
|
+
elif var_type == TypeEnum.JSON:
|
|
59
|
+
return pb2.VariableType_PB.JSON
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("Unknown type: " + str(var_type))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def create_dvcuser_pb(user: DevCycleUser) -> pb2.DVCUser_PB: # type: ignore
|
|
65
|
+
app_build = float("nan")
|
|
66
|
+
if user.appBuild:
|
|
67
|
+
try:
|
|
68
|
+
app_build = float(user.appBuild)
|
|
69
|
+
except ValueError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
return pb2.DVCUser_PB( # type: ignore
|
|
73
|
+
user_id=user.user_id,
|
|
74
|
+
email=create_nullable_string(user.email),
|
|
75
|
+
name=create_nullable_string(user.name),
|
|
76
|
+
language=create_nullable_string(user.language),
|
|
77
|
+
country=create_nullable_string(user.country),
|
|
78
|
+
appVersion=create_nullable_string(user.appVersion),
|
|
79
|
+
appBuild=create_nullable_double(app_build),
|
|
80
|
+
customData=create_nullable_custom_data(user.customData),
|
|
81
|
+
privateCustomData=create_nullable_custom_data(user.privateCustomData),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def create_variable(sdk_variable: pb2.SDKVariable_PB, default_value: Any) -> Variable: # type: ignore
|
|
86
|
+
if sdk_variable.type == pb2.VariableType_PB.Boolean: # type: ignore
|
|
87
|
+
return Variable(
|
|
88
|
+
_id=None,
|
|
89
|
+
value=sdk_variable.boolValue,
|
|
90
|
+
key=sdk_variable.key,
|
|
91
|
+
type=TypeEnum.BOOLEAN,
|
|
92
|
+
isDefaulted=False,
|
|
93
|
+
defaultValue=default_value,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
elif sdk_variable.type == pb2.VariableType_PB.String: # type: ignore
|
|
97
|
+
return Variable(
|
|
98
|
+
_id=None,
|
|
99
|
+
value=sdk_variable.stringValue,
|
|
100
|
+
key=sdk_variable.key,
|
|
101
|
+
type=TypeEnum.STRING,
|
|
102
|
+
isDefaulted=False,
|
|
103
|
+
defaultValue=default_value,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
elif sdk_variable.type == pb2.VariableType_PB.Number: # type: ignore
|
|
107
|
+
return Variable(
|
|
108
|
+
_id=None,
|
|
109
|
+
value=sdk_variable.doubleValue,
|
|
110
|
+
key=sdk_variable.key,
|
|
111
|
+
type=TypeEnum.NUMBER,
|
|
112
|
+
isDefaulted=False,
|
|
113
|
+
defaultValue=default_value,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
elif sdk_variable.type == pb2.VariableType_PB.JSON: # type: ignore
|
|
117
|
+
json_data = json.loads(sdk_variable.stringValue)
|
|
118
|
+
|
|
119
|
+
return Variable(
|
|
120
|
+
_id=None,
|
|
121
|
+
value=json_data,
|
|
122
|
+
key=sdk_variable.key,
|
|
123
|
+
type=TypeEnum.JSON,
|
|
124
|
+
isDefaulted=False,
|
|
125
|
+
defaultValue=default_value,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
else:
|
|
129
|
+
raise ValueError("Unknown type: " + sdk_variable.type)
|