Skip to content

Commit 8e27529

Browse files
authored
FFM-10393 Add get_flag_type and int_or_float_variation to Client API (#87)
1 parent 351834e commit 8e27529

File tree

9 files changed

+158
-5
lines changed

9 files changed

+158
-5
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
import time
3+
4+
from featureflags.evaluations.auth_target import Target
5+
from featureflags.client import CfClient, FeatureFlagType
6+
from featureflags.util import log
7+
8+
9+
def main():
10+
log.setLevel(logging.INFO)
11+
log.info("Starting example")
12+
api_key = "your_api_key"
13+
client = CfClient(api_key)
14+
# Don't continue until all flags and groups have been loaded into the
15+
# cache.
16+
log.info("Waiting to load all flags and groups before proceeding")
17+
client.wait_for_initialization()
18+
19+
target = Target(identifier='harness')
20+
21+
flag_identifier = 'anyflag'
22+
23+
# Get the flag type
24+
flag_type = client.get_flag_type(flag_identifier)
25+
log.info("Flag '%s' is of type '%s'", flag_identifier, flag_type)
26+
27+
# Ensure the right variation method is called based on the flag type
28+
variation_methods = {
29+
FeatureFlagType.BOOLEAN: lambda: client.bool_variation(flag_identifier, target, False),
30+
31+
FeatureFlagType.STRING: lambda: client.string_variation(flag_identifier, target, "default"),
32+
33+
FeatureFlagType.INT_OR_FLOAT: lambda: client.int_or_float_variation(flag_identifier, target, 3.2),
34+
35+
FeatureFlagType.JSON: lambda: client.json_variation(flag_identifier, target, {}),
36+
37+
# If the flag cannot be found, log an error
38+
FeatureFlagType.FLAG_NOT_FOUND: lambda: log.error("Flag %s was not found", flag_identifier)
39+
}
40+
41+
while True:
42+
result = variation_methods[flag_type]()
43+
log.info("Result for flag type '%s' is %s", flag_type, result)
44+
time.sleep(10)
45+
46+
47+
if __name__ == "__main__":
48+
main()

featureflags/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
__author__ = """Enver Bisevac"""
44
__email__ = "[email protected]"
5-
__version__ = '1.3.0'
5+
__version__ = '1.4.0'

featureflags/analytics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
VARIATION_VALUE_ATTRIBUTE = 'variationValue'
3434
TARGET_ATTRIBUTE = 'target'
3535
SDK_VERSION_ATTRIBUTE = 'SDK_VERSION'
36-
SDK_VERSION = '1.3.0'
36+
SDK_VERSION = '1.4.0'
3737
SDK_TYPE_ATTRIBUTE = 'SDK_TYPE'
3838
SDK_TYPE = 'server'
3939
SDK_LANGUAGE_ATTRIBUTE = 'SDK_LANGUAGE'

featureflags/client.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Client for interacting with Harness FF server"""
22

33
import threading
4-
from typing import Any, Callable, Dict, Optional
4+
from enum import Enum
5+
from typing import Any, Callable, Dict, Optional, Union
56

67
from tenacity import RetryError
78
from jwt import decode
@@ -33,6 +34,14 @@ def __str__(self):
3334
return f"MissingOrEmptyAPIKeyException: {self.message}"
3435

3536

37+
class FeatureFlagType(str, Enum):
38+
BOOLEAN = "boolean"
39+
INT_OR_FLOAT = "int"
40+
STRING = "string"
41+
JSON = "json"
42+
FLAG_NOT_FOUND = "flag_not_found"
43+
44+
3645
class CfClient(object):
3746
def __init__(
3847
self, sdk_key: str,
@@ -197,6 +206,21 @@ def authenticate(self):
197206
additional_headers["Harness-AccountID"] = decoded["accountID"]
198207
self._client = self._client.with_headers(additional_headers)
199208

209+
def get_flag_type(self, identifier) -> Optional[FeatureFlagType]:
210+
if self._initialised_failed_reason[True] is not None:
211+
log.warning(
212+
"Failed to check flag type for flag '%s', reason: Client is "
213+
"not initialized",
214+
identifier)
215+
return FeatureFlagType("flag_not_found")
216+
kind = self._evaluator.get_kind(identifier)
217+
if not kind:
218+
log.warning(
219+
"Failed to check flag kind for flag '%s', reason: flag not "
220+
"found", identifier)
221+
return FeatureFlagType("flag_not_found")
222+
return FeatureFlagType(kind)
223+
200224
def bool_variation(self, identifier: str, target: Target,
201225
default: bool) -> bool:
202226
# If initialization has failed, then return the default variation
@@ -291,6 +315,40 @@ def number_variation(self, identifier: str, target: Target,
291315
"Reason: '%s'", identifier, default, str(ex))
292316
return default
293317

318+
def int_or_float_variation(self, identifier: str, target: Target,
319+
default: Union[float, int]) -> Union[
320+
float, int]:
321+
322+
# If initialization has failed, then return the default variation
323+
# immediately
324+
if self._initialised_failed_reason[True] is not None:
325+
log.error(
326+
"SDKCODE:6001: Failed to evaluate int_or_float variation for "
327+
"flag '%s' and the default variation '%s' is being returned. "
328+
"Reason: `Client is not initialized: %s'",
329+
identifier, default, self._initialised_failed_reason[True])
330+
return default
331+
332+
try:
333+
variation = self._evaluator.evaluate(
334+
identifier, target, "int")
335+
336+
# Only register metrics if analytics is enabled,
337+
# and sometimes when the SDK starts up we can
338+
# evaluate before the flag is cached which results in
339+
# an empty identifier.
340+
if self._config.enable_analytics and variation.identifier != "":
341+
self._analytics.enqueue(target, identifier, variation)
342+
343+
return variation.int_or_float(target, identifier, default)
344+
345+
except FlagKindMismatchException as ex:
346+
log.error(
347+
"SDKCODE:6001: Failed to evaluate int_or_float variation for "
348+
"flag '%s' and the default variation '%s' is being returned. "
349+
"Reason: '%s'", identifier, default, str(ex))
350+
return default
351+
294352
def string_variation(self, identifier: str, target: Target,
295353
default: str) -> str:
296354

featureflags/evaluations/evaluator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ class Evaluator(object):
3838
def __init__(self, provider: QueryInterface):
3939
self.provider = provider
4040

41+
def get_kind(self, identifier) -> Optional[str]:
42+
fc = self.provider.get_flag(identifier)
43+
if not fc:
44+
return None
45+
return fc.kind
46+
4147
def _find_variation(self, variations: List[Variation],
4248
identifier: Optional[str]) -> Variation:
4349
if not identifier:

featureflags/evaluations/variation.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,39 @@ def int(self, target: Target, flag_identifier: str,
7979
{"target": target, "flag": flag_identifier}, default)
8080
return default
8181

82+
def int_or_float(self, target: Target, flag_identifier: str,
83+
default: Union[int, float]) -> Union[int, float]:
84+
if self.value:
85+
try:
86+
result = int(self.value)
87+
except ValueError:
88+
try:
89+
# If int conversion fails, try converting to float
90+
result = float(self.value)
91+
except ValueError:
92+
# If both conversions fail, log an error and return the
93+
# default
94+
log.error(
95+
"SDKCODE:6001: Invalid number format for %s. "
96+
"Expected a number but got '%s'",
97+
{"flag": flag_identifier, "value": self.value}
98+
)
99+
return default
100+
101+
log.debug(
102+
"SDKCODE:6000: Evaluated number variation successfully: %s",
103+
{"result": result, "flag identifier": flag_identifier,
104+
"target": target}
105+
)
106+
return result
107+
108+
log.error(
109+
"SDKCODE:6001: Failed to evaluate int_or_float variation for %s "
110+
"and the "
111+
"default variation '%s' is being returned",
112+
{"target": target, "flag": flag_identifier}, default)
113+
return default
114+
82115
def json(self, target: Target, flag_identifier: str,
83116
default: dict) -> dict:
84117
if self.value:

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 1.3.0
2+
current_version = 1.4.0
33
commit = True
44
tag = True
55

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@
5757
test_suite="tests",
5858
tests_require=test_requirements,
5959
url="https://github.com/harness/ff-python-server-sdk",
60-
version='1.3.0',
60+
version='1.4.0',
6161
zip_safe=False,
6262
)

tests/unit/test_evaluator.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,11 @@ def test_evaluate_flag_on(data_provider, feature, target, true_variation):
274274
got = evaluator._evaluate_flag(feature, target)
275275

276276
assert got == true_variation
277+
278+
279+
def test_get_flag_kind(data_provider, feature, target, true_variation):
280+
evaluator = Evaluator(data_provider)
281+
282+
got = evaluator.get_kind(feature.feature)
283+
284+
assert got == FeatureConfigKind.BOOLEAN

0 commit comments

Comments
 (0)