Skip to content

Commit 19adda5

Browse files
author
Miloš Vasić
authored
Merge pull request #29 from harness/FFM-2616
[FFM-2616] new eval engine integrated in client class
2 parents a2ec971 + ca8f0c8 commit 19adda5

File tree

9 files changed

+134
-101
lines changed

9 files changed

+134
-101
lines changed

featureflags/analytics.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from .api.client import AuthenticatedClient
1111
from .api.default.post_metrics import sync_detailed as post_metrics
1212
from .config import Config
13-
from .evaluations.feature import FeatureConfig
1413
from .evaluations.target import Target
1514
from .evaluations.variation import Variation
1615
from .models.key_value import KeyValue
@@ -38,7 +37,7 @@
3837
@attr.s(auto_attribs=True)
3938
class AnalyticsEvent(object):
4039
target: Target
41-
feature_config: FeatureConfig
40+
flag_identifier: str
4241
variation: Variation
4342
count: int = 0
4443

@@ -58,11 +57,11 @@ def __init__(self, config: Config, client: AuthenticatedClient,
5857
self._runner.daemon = True
5958
self._runner.start()
6059

61-
def enqueue(self, target: Target, feature_config: FeatureConfig,
60+
def enqueue(self, target: Target, identifier: str,
6261
variation: Variation):
6362
event: AnalyticsEvent = AnalyticsEvent(
6463
target=target,
65-
feature_config=feature_config,
64+
flag_identifier=identifier,
6665
variation=variation
6766
)
6867

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

8079
def get_key(self, event: AnalyticsEvent) -> str:
8180
return '{feature}-{variation}-{value}-{target}'.format(
82-
feature=event.feature_config.feature,
81+
feature=event.flag_identifier,
8382
variation=event.variation.identifier,
8483
value=event.variation.value,
8584
target=GLOBAL_TARGET,
@@ -122,9 +121,9 @@ def _send_data(self) -> None:
122121

123122
metric_attributes: List[KeyValue] = [
124123
KeyValue(FEATURE_IDENTIFIER_ATTRIBUTE,
125-
event.feature_config.feature),
124+
event.flag_identifier),
126125
KeyValue(FEATURE_NAME_ATTRIBUTE,
127-
event.feature_config.feature),
126+
event.flag_identifier),
128127
KeyValue(VARIATION_IDENTIFIER_ATTRIBUTE,
129128
event.variation.identifier),
130129
KeyValue(VARIATION_VALUE_ATTRIBUTE, event.variation.value),

featureflags/client.py

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

88
from featureflags.analytics import AnalyticsService
9+
from featureflags.evaluations.evaluator import Evaluator
10+
from featureflags.repository import Repository
911

1012
from .api.client import AuthenticatedClient, Client
1113
from .api.default.authenticate import AuthenticationRequest
1214
from .api.default.authenticate import sync as authenticate
1315
from .config import Config, default_config
14-
from .evaluations.feature import FeatureConfig
15-
from .evaluations.segment import Segments
16-
from .evaluations.target import Target
16+
from .evaluations.auth_target import Target
1717
from .polling import PollingProcessor
1818
from .streaming import StreamProcessor
1919
from .util import log
@@ -39,6 +39,12 @@ def __init__(
3939
if callable(option):
4040
option(self._config)
4141

42+
if self._config.cache is None:
43+
raise Exception("cache cannot be none")
44+
45+
self._repository = Repository(self._config.cache)
46+
self._evaluator = Evaluator(self._repository)
47+
4248
log.debug("CfClient initialized")
4349
self.run()
4450

@@ -53,20 +59,21 @@ def run(self):
5359
config=self._config,
5460
environment_id=self._environment_id,
5561
ready=polling_event,
56-
stream_ready=streaming_event
62+
stream_ready=streaming_event,
63+
repository=self._repository
5764
)
5865
self._polling_processor.start()
5966

6067
if self._config.enable_stream:
6168
self._stream = StreamProcessor(
62-
cache=self._config.cache,
69+
repository=self._repository,
6370
client=self._client,
6471
environment_id=self._environment_id,
6572
api_key=self._sdk_key,
6673
token=self._auth_token,
6774
config=self._config,
6875
ready=streaming_event,
69-
cluster=self._cluster
76+
cluster=self._cluster,
7077
)
7178
self._stream.start()
7279

@@ -105,58 +112,37 @@ def authenticate(self):
105112
)
106113
self._client.with_headers({"User-Agent": "PythonSDK/" + VERSION})
107114

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-
141115
def bool_variation(self, identifier: str, target: Target,
142116
default: bool) -> bool:
143-
return self._variation('bool', identifier, target, default)
117+
variation = self._evaluator.evaluate(identifier, target)
118+
self._analytics.enqueue(target, identifier, variation)
119+
return variation.bool(default)
144120

145121
def int_variation(self, identifier: str, target: Target,
146122
default: int) -> int:
147-
return self._variation('int', identifier, target, default)
123+
variation = self._evaluator.evaluate(identifier, target)
124+
self._analytics.enqueue(target, identifier, variation)
125+
return variation.int(default)
148126

149127
def number_variation(self, identifier: str, target: Target,
150128
default: float) -> float:
151-
return self._variation('number', identifier, target, default)
129+
variation = self._evaluator.evaluate(
130+
identifier, target)
131+
self._analytics.enqueue(target, identifier, variation)
132+
return variation.number(default)
152133

153134
def string_variation(self, identifier: str, target: Target,
154135
default: str) -> str:
155-
return self._variation('string', identifier, target, default)
136+
variation = self._evaluator.evaluate(
137+
identifier, target)
138+
self._analytics.enqueue(target, identifier, variation)
139+
return variation.string(default)
156140

157141
def json_variation(self, identifier: str, target: Target,
158142
default: Dict[str, Any]) -> Dict[str, Any]:
159-
return self._variation('json', identifier, target, default)
143+
variation = self._evaluator.evaluate(identifier, target)
144+
self._analytics.enqueue(target, identifier, variation)
145+
return variation.json(default)
160146

161147
def close(self):
162148
log.info('closing sdk client')

featureflags/evaluations/evaluator.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
STARTS_WITH_OPERATOR)
1515
from featureflags.evaluations.distribution import Distribution
1616
from featureflags.evaluations.enum import FeatureState
17-
from featureflags.evaluations.feature import FeatureConfig, FeatureConfigKind
17+
from featureflags.evaluations.feature import FeatureConfig
1818
from featureflags.evaluations.serving_rule import ServingRule, ServingRules
1919
from featureflags.evaluations.variation import Variation
2020
from featureflags.evaluations.variation_map import VariationMap
@@ -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: 8 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,8 @@ 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,
18+
repository: DataProviderInterface) -> None:
1719
Thread.__init__(self)
1820
self.daemon = True
1921
self.__environment_id = environment_id
@@ -22,6 +24,7 @@ def __init__(self, client: AuthenticatedClient, config: Config,
2224
self.__running = False
2325
self.__ready = ready
2426
self.__stream_ready = stream_ready
27+
self.__repository = repository
2528

2629
def run(self):
2730
if not self.__running:
@@ -66,8 +69,8 @@ def __retrieve_flags(self):
6669
)
6770
log.debug("Feature flags loaded")
6871
for flag in flags:
69-
log.debug("Setting the cache value %s", flag.feature)
70-
self.__config.cache.set(f"flags/{flag.feature}", flag)
72+
log.debug("Put flag %s into repository", flag.feature)
73+
self.__repository.set_flag(flag)
7174

7275
def __retrieve_segments(self):
7376
log.debug("Loading target segments")
@@ -76,5 +79,5 @@ def __retrieve_segments(self):
7679
)
7780
log.debug("Target segments loaded")
7881
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)
82+
log.debug("Put %s segment into repository", segment.identifier)
83+
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)