Skip to content

Commit ef98a50

Browse files
committed
new eval engine integrated into client
1 parent 22c00c8 commit ef98a50

File tree

9 files changed

+131
-97
lines changed

9 files changed

+131
-97
lines changed

featureflags/analytics.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
@attr.s(auto_attribs=True)
3939
class AnalyticsEvent(object):
4040
target: Target
41-
feature_config: FeatureConfig
41+
flag_identifier: str
4242
variation: Variation
4343
count: int = 0
4444

@@ -58,11 +58,11 @@ def __init__(self, config: Config, client: AuthenticatedClient,
5858
self._runner.daemon = True
5959
self._runner.start()
6060

61-
def enqueue(self, target: Target, feature_config: FeatureConfig,
61+
def enqueue(self, target: Target, identifier: str,
6262
variation: Variation):
6363
event: AnalyticsEvent = AnalyticsEvent(
6464
target=target,
65-
feature_config=feature_config,
65+
flag_identifier=identifier,
6666
variation=variation
6767
)
6868

@@ -79,7 +79,7 @@ def enqueue(self, target: Target, feature_config: FeatureConfig,
7979

8080
def get_key(self, event: AnalyticsEvent) -> str:
8181
return '{feature}-{variation}-{value}-{target}'.format(
82-
feature=event.feature_config.feature,
82+
feature=event.flag_identifier,
8383
variation=event.variation.identifier,
8484
value=event.variation.value,
8585
target=GLOBAL_TARGET,
@@ -122,9 +122,9 @@ def _send_data(self) -> None:
122122

123123
metric_attributes: List[KeyValue] = [
124124
KeyValue(FEATURE_IDENTIFIER_ATTRIBUTE,
125-
event.feature_config.feature),
125+
event.flag_identifier),
126126
KeyValue(FEATURE_NAME_ATTRIBUTE,
127-
event.feature_config.feature),
127+
event.flag_identifier),
128128
KeyValue(VARIATION_IDENTIFIER_ATTRIBUTE,
129129
event.variation.identifier),
130130
KeyValue(VARIATION_VALUE_ATTRIBUTE, event.variation.value),

featureflags/client.py

Lines changed: 32 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@
66
from jwt import decode
77

88
from featureflags.analytics import AnalyticsService
9+
from featureflags.evaluations.evaluator import Evaluator
10+
from featureflags.lru_cache import LRUCache
11+
from featureflags.repository import Repository
912

1013
from .api.client import AuthenticatedClient, Client
1114
from .api.default.authenticate import AuthenticationRequest
1215
from .api.default.authenticate import sync as authenticate
1316
from .config import Config, default_config
14-
from .evaluations.feature import FeatureConfig
17+
from .evaluations.feature import FeatureConfig, FeatureConfigKind
1518
from .evaluations.segment import Segments
16-
from .evaluations.target import Target
19+
from .evaluations.auth_target import Target
1720
from .polling import PollingProcessor
1821
from .streaming import StreamProcessor
1922
from .util import log
@@ -39,6 +42,12 @@ def __init__(
3942
if callable(option):
4043
option(self._config)
4144

45+
if self._config.cache is None:
46+
raise Exception("cache cannot be none")
47+
48+
self._repository = Repository(self._config.cache)
49+
self._evaluator = Evaluator(self._repository)
50+
4251
log.debug("CfClient initialized")
4352
self.run()
4453

@@ -53,20 +62,21 @@ def run(self):
5362
config=self._config,
5463
environment_id=self._environment_id,
5564
ready=polling_event,
56-
stream_ready=streaming_event
65+
stream_ready=streaming_event,
66+
repository=self._repository
5767
)
5868
self._polling_processor.start()
5969

6070
if self._config.enable_stream:
6171
self._stream = StreamProcessor(
62-
cache=self._config.cache,
72+
repository=self._repository,
6373
client=self._client,
6474
environment_id=self._environment_id,
6575
api_key=self._sdk_key,
6676
token=self._auth_token,
6777
config=self._config,
6878
ready=streaming_event,
69-
cluster=self._cluster
79+
cluster=self._cluster,
7080
)
7181
self._stream.start()
7282

@@ -105,58 +115,37 @@ def authenticate(self):
105115
)
106116
self._client.with_headers({"User-Agent": "PythonSDK/" + VERSION})
107117

108-
def map_segments_from_cache(self, fc: FeatureConfig) -> None:
109-
if self._config.cache:
110-
segments = fc.get_segment_identifiers()
111-
for identifier in segments:
112-
try:
113-
segment = self._config.cache.get(f'segments/{identifier}')
114-
if fc.segments is None:
115-
fc.segments = Segments({})
116-
fc.segments[identifier] = segment
117-
except KeyError:
118-
log.warning("segment %s not found in cache", identifier)
119-
120-
def _variation(self, fn: str, identifier: str, target: Target,
121-
default: Any) -> Any:
122-
if self._config.cache:
123-
try:
124-
fc = self._config.cache.get(f'flags/{identifier}')
125-
if fc:
126-
self.map_segments_from_cache(fc)
127-
method = getattr(fc, f'{fn}_variation', None)
128-
if method:
129-
variation = method(target)
130-
if variation is None:
131-
log.debug('No variation found')
132-
return default
133-
self._analytics.enqueue(target, fc, variation)
134-
return getattr(variation, fn)(default)
135-
else:
136-
log.error("Wrong method name %s", fn)
137-
except KeyError:
138-
log.warning("flag %s not found in cache", identifier)
139-
return default
140-
141118
def bool_variation(self, identifier: str, target: Target,
142119
default: bool) -> bool:
143-
return self._variation('bool', identifier, target, default)
120+
variation = self._evaluator.evaluate(identifier, target)
121+
self._analytics.enqueue(target, identifier, variation)
122+
return variation.bool(default)
144123

145124
def int_variation(self, identifier: str, target: Target,
146125
default: int) -> int:
147-
return self._variation('int', identifier, target, default)
126+
variation = self._evaluator.evaluate(identifier, target)
127+
self._analytics.enqueue(target, identifier, variation)
128+
return variation.int(default)
148129

149130
def number_variation(self, identifier: str, target: Target,
150131
default: float) -> float:
151-
return self._variation('number', identifier, target, default)
132+
variation = self._evaluator.evaluate(
133+
identifier, target)
134+
self._analytics.enqueue(target, identifier, variation)
135+
return variation.number(default)
152136

153137
def string_variation(self, identifier: str, target: Target,
154138
default: str) -> str:
155-
return self._variation('string', identifier, target, default)
139+
variation = self._evaluator.evaluate(
140+
identifier, target)
141+
self._analytics.enqueue(target, identifier, variation)
142+
return variation.string(default)
156143

157144
def json_variation(self, identifier: str, target: Target,
158145
default: Dict[str, Any]) -> Dict[str, Any]:
159-
return self._variation('json', identifier, target, default)
146+
variation = self._evaluator.evaluate(identifier, target)
147+
self._analytics.enqueue(target, identifier, variation)
148+
return variation.json(default)
160149

161150
def close(self):
162151
log.info('closing sdk client')

featureflags/evaluations/evaluator.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,23 @@
2323
from featureflags.util import log
2424

2525

26+
EMPTY_VARIATION = Variation(identifier="", value=None)
27+
28+
2629
class Evaluator(object):
2730

2831
def __init__(self, provider: QueryInterface):
2932
self.provider = provider
3033

3134
def _find_variation(self, variations: List[Variation],
32-
identifier: Optional[str]) -> Optional[Variation]:
35+
identifier: Optional[str]) -> Variation:
3336
if not identifier:
3437
log.debug("Empty identifier %s or variations %s occurred",
3538
identifier, variations)
36-
return None
39+
return EMPTY_VARIATION
3740
variation = next(
3841
(val for val in variations if val.identifier == identifier),
39-
None
42+
EMPTY_VARIATION
4043
)
4144
log.debug("Variation %s found in variations %s",
4245
identifier, variations)
@@ -236,7 +239,7 @@ def _evaluate_variation_map(self, var_target_map: List[VariationMap],
236239
return None
237240

238241
def _evaluate_flag(self, fc: FeatureConfig,
239-
target: Target) -> Optional[Variation]:
242+
target: Target) -> Variation:
240243
variation: Optional[str] = fc.off_variation
241244
log.debug("feature %s state is %s", fc.feature, fc.state)
242245
if fc.state == FeatureState.ON:
@@ -297,11 +300,10 @@ def _check_prerequisite(self, parent: FeatureConfig,
297300
return self._check_prerequisite(config, target)
298301
return True
299302

300-
def evaluate(self, identifier: str, target: Target,
301-
expected: FeatureConfigKind) -> Optional[Variation]:
303+
def evaluate(self, identifier: str, target: Target) -> Variation:
302304
fc = self.provider.get_flag(identifier)
303-
if not fc or fc.kind != expected:
304-
return None
305+
if not fc:
306+
return Variation(identifier="", value=None)
305307

306308
if fc.prerequisites:
307309
prereq = self._check_prerequisite(fc, target)

featureflags/evaluations/variation.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from typing import Any, Dict, List, Type, TypeVar, Union
3+
from xmlrpc.client import boolean
34

45
import attr
56

@@ -11,14 +12,14 @@
1112
@attr.s(auto_attribs=True)
1213
class Variation(object):
1314
identifier: str
14-
value: str
15+
value: Union[str, None]
1516
name: Union[Unset, str] = UNSET
1617
description: Union[Unset, str] = UNSET
1718
additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)
1819

1920
def bool(self, default: bool = False) -> bool:
2021
if self.value:
21-
return self.value == "true"
22+
return self.value.lower() == "true"
2223
return default
2324

2425
def string(self, default: str) -> str:
@@ -94,5 +95,5 @@ def __setitem__(self, key: str, value: Any) -> None:
9495
def __delitem__(self, key: str) -> None:
9596
del self.additional_properties[key]
9697

97-
def __contains__(self, key: str) -> bool:
98+
def __contains__(self, key: str) -> boolean:
9899
return key in self.additional_properties

featureflags/polling.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from threading import Event, Thread
33

44
from featureflags.api.client import AuthenticatedClient
5+
from featureflags.repository import DataProviderInterface
56

67
from .api.default.get_all_segments import sync as retrieve_segments
78
from .api.default.get_feature_config import sync as retrieve_flags
@@ -13,7 +14,7 @@ class PollingProcessor(Thread):
1314

1415
def __init__(self, client: AuthenticatedClient, config: Config,
1516
environment_id: str, ready: Event,
16-
stream_ready: Event) -> None:
17+
stream_ready: Event, repository: DataProviderInterface) -> None:
1718
Thread.__init__(self)
1819
self.daemon = True
1920
self.__environment_id = environment_id
@@ -22,6 +23,7 @@ def __init__(self, client: AuthenticatedClient, config: Config,
2223
self.__running = False
2324
self.__ready = ready
2425
self.__stream_ready = stream_ready
26+
self.__repository = repository
2527

2628
def run(self):
2729
if not self.__running:
@@ -66,8 +68,8 @@ def __retrieve_flags(self):
6668
)
6769
log.debug("Feature flags loaded")
6870
for flag in flags:
69-
log.debug("Setting the cache value %s", flag.feature)
70-
self.__config.cache.set(f"flags/{flag.feature}", flag)
71+
log.debug("Put flag %s into repository", flag.feature)
72+
self.__repository.set_flag(flag)
7173

7274
def __retrieve_segments(self):
7375
log.debug("Loading target segments")
@@ -76,5 +78,5 @@ def __retrieve_segments(self):
7678
)
7779
log.debug("Target segments loaded")
7880
for segment in segments:
79-
log.debug("Setting the cache segment value %s", segment.identifier)
80-
self.__config.cache.set(f"segments/{segment.identifier}", segment)
81+
log.debug("Put %s segment into repository", segment.identifier)
82+
self.__repository.set_segment(segment)

featureflags/repository.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import abc
22
from typing import List, Optional
3+
from featureflags.evaluations.constants import SEGMENT_MATCH_OPERATOR
34

45
from featureflags.evaluations.feature import FeatureConfig
56
from featureflags.evaluations.segment import Segment
@@ -58,6 +59,16 @@ def set_segment(self, group: Segment) -> None:
5859
"""Put Target group to the repository"""
5960
raise NotImplementedError
6061

62+
@abc.abstractmethod
63+
def remove_flag(self, identifier: str) -> None:
64+
"""Remove Flag from the repository"""
65+
raise NotImplementedError
66+
67+
@abc.abstractmethod
68+
def remove_segment(self, identifier: str) -> None:
69+
"""Remove Target group from the repository"""
70+
raise NotImplementedError
71+
6172
@abc.abstractmethod
6273
def close(self) -> None:
6374
"""Put Target group to the repository"""
@@ -140,8 +151,45 @@ def set_segment(self, segment: Segment) -> None:
140151
self.cache.set(segment_key, segment)
141152
log.debug("Segment %s successfully cached", segment.identifier)
142153

143-
def find_flags_by_segment(self, identifier: str) -> List[str]:
144-
return []
154+
def find_flags_by_segment(self, segment: str) -> List[str]:
155+
result = []
156+
keys = self.cache.keys()
157+
if self.store:
158+
keys = self.store.keys()
159+
for key in keys:
160+
flag = self.get_flag(key, cacheable=False)
161+
if not flag:
162+
continue
163+
for serving_rule in flag.rules:
164+
for clause in serving_rule.clauses:
165+
if clause.op == SEGMENT_MATCH_OPERATOR and not next(
166+
(val for val in clause.values if val == segment),
167+
None
168+
):
169+
log.debug("Flag %s evaluated in segments",
170+
flag.feature)
171+
result.append(flag.feature)
172+
return result
173+
174+
def remove_flag(self, identifier: str) -> None:
175+
"""Remove Flag from the repository"""
176+
flag_key = format_flag_key(identifier)
177+
if self.store:
178+
self.store.remove([flag_key])
179+
log.debug("Flag %s successfully deleted from store", identifier)
180+
181+
self.cache.remove([flag_key])
182+
log.debug("Flag %s successfully deleted from cache", identifier)
183+
184+
def remove_segment(self, identifier: str) -> None:
185+
"""Remove Target group from the repository"""
186+
segment_key = format_segment_key(identifier)
187+
if self.store:
188+
self.store.remove([segment_key])
189+
log.debug("Segment %s successfully deleted from store", identifier)
190+
191+
self.cache.remove([segment_key])
192+
log.debug("Segment %s successfully deleted from cache", identifier)
145193

146194
def close(self) -> None:
147195
if self.store:

0 commit comments

Comments
 (0)