-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathprovider.py
175 lines (144 loc) · 7.91 KB
/
provider.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import threading
from typing import Any, List, Optional, Union
from ldclient import LDClient, Config
from ldclient.interfaces import DataSourceStatus, FlagChange, DataSourceState
from openfeature.evaluation_context import EvaluationContext
from openfeature.exception import ErrorCode, ProviderFatalError
from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, Reason
from openfeature.hook import Hook
from openfeature.provider.metadata import Metadata
from openfeature.provider import AbstractProvider
from openfeature.event import ProviderEventDetails
from ld_openfeature.impl.context_converter import EvaluationContextConverter
from ld_openfeature.impl.details_converter import ResolutionDetailsConverter
class LaunchDarklyProvider(AbstractProvider):
def __init__(self, config: Config):
self.__client = LDClient(config)
self.__context_converter = EvaluationContextConverter()
self.__details_converter = ResolutionDetailsConverter()
def __handle_data_source_status(self, status: DataSourceStatus):
state = status.state
if state == DataSourceState.INITIALIZING:
return
elif state == DataSourceState.VALID:
self.emit_provider_ready(ProviderEventDetails())
elif state == DataSourceState.OFF:
error_message = self.__get_message(status,
"the provider has encountered a permanent error or has been shutdown")
self.emit_provider_error(ProviderEventDetails(error_code=ErrorCode.PROVIDER_FATAL,
message=error_message))
elif state == DataSourceState.INTERRUPTED:
error_message = self.__get_message(status, "encountered an unknown error")
self.emit_provider_stale(ProviderEventDetails(message=error_message))
# For now treat an unknown state as no change.
def __handle_flag_change(self, change: FlagChange):
self.emit_provider_configuration_changed(ProviderEventDetails(flags_changed=[change.key]))
pass
def initialize(self, evaluation_context: EvaluationContext):
ready_event = threading.Event()
def ready_handler(status: DataSourceStatus):
if status.state == DataSourceState.VALID:
ready_event.set()
elif status.state == DataSourceState.OFF:
ready_event.set()
# We listen just to handle the ready event. We do not emit events because the client emits them for us.
self.__client.data_source_status_provider.add_listener(ready_handler)
# Check for conditions that may have happened before we added the listener.
if self.__client.data_source_status_provider.status.state == DataSourceState.OFF:
ready_event.set()
if self.__client.is_initialized():
ready_event.set()
ready_event.wait()
self.__client.data_source_status_provider.remove_listener(ready_handler)
if not self.__client.is_initialized():
raise ProviderFatalError(error_message="launchdarkly client initialization failed")
# Listen to new status events and emit them.
self.__client.data_source_status_provider.add_listener(self.__handle_data_source_status)
self.__client.flag_tracker.add_listener(self.__handle_flag_change)
def shutdown(self):
self.__client.data_source_status_provider.remove_listener(self.__handle_data_source_status)
self.__client.flag_tracker.remove_listener(self.__handle_flag_change)
self.__client.close()
def get_metadata(self) -> Metadata:
return Metadata("launchdarkly-openfeature-server")
def get_provider_hooks(self) -> List[Hook]:
return []
def resolve_boolean_details(
self,
flag_key: str,
default_value: bool,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]:
"""Resolves the flag value for the provided flag key as a boolean"""
return self.__resolve_value(FlagType(FlagType.BOOLEAN), flag_key, default_value, evaluation_context)
def resolve_string_details(
self,
flag_key: str,
default_value: str,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]:
"""Resolves the flag value for the provided flag key as a string"""
return self.__resolve_value(FlagType(FlagType.STRING), flag_key, default_value, evaluation_context)
def resolve_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
"""Resolves the flag value for the provided flag key as a integer"""
return self.__resolve_value(FlagType(FlagType.INTEGER), flag_key, default_value, evaluation_context)
def resolve_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
"""Resolves the flag value for the provided flag key as a float"""
return self.__resolve_value(FlagType(FlagType.FLOAT), flag_key, default_value, evaluation_context)
def resolve_object_details(
self,
flag_key: str,
default_value: Union[dict, list],
evaluation_context: Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[Union[dict, list]]:
"""Resolves the flag value for the provided flag key as a list or dictionary"""
return self.__resolve_value(FlagType(FlagType.OBJECT), flag_key, default_value, evaluation_context)
def __resolve_value(self, flag_type: FlagType, flag_key: str, default_value: Any,
evaluation_context: Optional[EvaluationContext] = None) -> FlagResolutionDetails:
if evaluation_context is None:
return FlagResolutionDetails(
value=default_value,
reason=Reason(Reason.ERROR),
error_code=ErrorCode.TARGETING_KEY_MISSING
)
ld_context = self.__context_converter.to_ld_context(evaluation_context)
result = self.__client.variation_detail(flag_key, ld_context, default_value)
if flag_type == FlagType.BOOLEAN and not isinstance(result.value, bool):
return self.__mismatched_type_details(default_value)
elif flag_type == FlagType.STRING and not isinstance(result.value, str):
return self.__mismatched_type_details(default_value)
elif flag_type == FlagType.INTEGER and isinstance(result.value, bool):
# Python treats boolean values as instances of int
return self.__mismatched_type_details(default_value)
elif flag_type == FlagType.FLOAT and isinstance(result.value, bool):
# Python treats boolean values as instances of int
return self.__mismatched_type_details(default_value)
elif flag_type == FlagType.INTEGER and not isinstance(result.value, int):
return self.__mismatched_type_details(default_value)
elif flag_type == FlagType.FLOAT and not isinstance(result.value, float) and not isinstance(result.value, int):
return self.__mismatched_type_details(default_value)
elif flag_type == FlagType.OBJECT and not isinstance(result.value, dict) and not isinstance(result.value, list):
return self.__mismatched_type_details(default_value)
return self.__details_converter.to_resolution_details(result)
@staticmethod
def __mismatched_type_details(default_value: Any) -> FlagResolutionDetails:
return FlagResolutionDetails(
value=default_value,
reason=Reason(Reason.ERROR),
error_code=ErrorCode.TYPE_MISMATCH
)
@staticmethod
def __get_message(status: DataSourceStatus, default: str):
if status.error and status.error.message:
return status.error.message
return default