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.
Files changed (50) hide show
  1. devcycle_python_sdk/VERSION.txt +1 -1
  2. devcycle_python_sdk/__init__.py +3 -3
  3. devcycle_python_sdk/api/backoff.py +11 -0
  4. devcycle_python_sdk/api/bucketing_client.py +17 -25
  5. devcycle_python_sdk/api/config_client.py +116 -0
  6. devcycle_python_sdk/api/event_client.py +95 -0
  7. devcycle_python_sdk/api/local_bucketing.py +429 -2
  8. devcycle_python_sdk/{dvc_cloud_client.py → cloud_client.py} +69 -41
  9. devcycle_python_sdk/exceptions.py +25 -3
  10. devcycle_python_sdk/local_client.py +220 -0
  11. devcycle_python_sdk/managers/config_manager.py +83 -15
  12. devcycle_python_sdk/managers/event_queue_manager.py +213 -8
  13. devcycle_python_sdk/models/bucketed_config.py +150 -0
  14. devcycle_python_sdk/models/event.py +111 -8
  15. devcycle_python_sdk/models/feature.py +12 -0
  16. devcycle_python_sdk/models/platform_data.py +33 -0
  17. devcycle_python_sdk/models/user.py +52 -7
  18. devcycle_python_sdk/models/variable.py +17 -5
  19. devcycle_python_sdk/options.py +106 -0
  20. devcycle_python_sdk/protobuf/__init__.py +0 -0
  21. devcycle_python_sdk/protobuf/utils.py +129 -0
  22. devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +46 -0
  23. devcycle_python_sdk/util/version.py +1 -1
  24. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/METADATA +3 -1
  25. devcycle_python_server_sdk-3.0.0.dist-info/RECORD +55 -0
  26. example/{example.py → cloud_client_example.py} +24 -20
  27. example/local_bucketing_client_example.py +74 -0
  28. test/api/test_bucketing_client.py +10 -5
  29. test/api/test_config_client.py +133 -0
  30. test/api/test_event_client.py +97 -0
  31. test/api/test_local_bucketing.py +379 -4
  32. test/fixture/__init__.py +0 -0
  33. test/fixture/data.py +57 -0
  34. test/managers/test_config_manager.py +120 -5
  35. test/managers/test_event_queue_manager.py +259 -5
  36. test/models/__init__.py +0 -0
  37. test/models/test_bucketed_config.py +26 -0
  38. test/{test_dvc_cloud_client.py → test_cloud_client.py} +11 -17
  39. test/test_local_client.py +397 -0
  40. test/util/test_utils.py +183 -0
  41. test/util/test_version.py +2 -2
  42. devcycle_python_sdk/dvc_local_client.py +0 -152
  43. devcycle_python_sdk/dvc_options.py +0 -45
  44. devcycle_python_server_sdk-2.0.1.dist-info/RECORD +0 -41
  45. test/test_bucketing_client.py +0 -228
  46. test/test_dvc_local_client.py +0 -143
  47. test/test_version.py +0 -18
  48. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/LICENSE +0 -0
  49. {devcycle_python_server_sdk-2.0.1.dist-info → devcycle_python_server_sdk-3.0.0.dist-info}/WHEEL +0 -0
  50. {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 time import time
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 Event:
18
+ class DevCycleEvent:
9
19
  type: Optional[str] = None
10
20
  target: Optional[str] = None
11
- date: int = field(default_factory=lambda: int(time()))
21
+ date: datetime = field(default_factory=lambda: datetime.utcnow())
12
22
  value: Optional[int] = None
13
- metaData: Dict[str, str] = field(default_factory=dict)
23
+ metaData: Optional[Dict[str, str]] = None
14
24
 
15
- def to_json(self):
16
- return {
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 time import time
3
+ from datetime import datetime, timezone
4
4
  from typing import Dict, Optional
5
5
 
6
6
 
7
7
  @dataclass(order=False)
8
- class User:
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] = field(default_factory=dict)
17
- privateCustomData: Dict[str, str] = field(default_factory=dict)
18
- createdDate: int = field(default_factory=lambda: int(time()))
19
- lastSeenDate: Optional[int] = None # TODO: accept datetime
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
- return {
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: Optional[str]
30
- type: Optional[str]
31
- value: Optional[Any] = None
29
+ key: str
30
+ type: str
31
+ value: Any = None
32
32
  isDefaulted: Optional[bool] = False
33
- defaultValue: Optional[Any] = None
34
- # evalReason: Optional[str] = None
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)