Skip to content

Commit 311b8ee

Browse files
feat: spec-0.2.0 (#38)
* fix/unit-tests: Add float and int flag methods Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * fix/unit-tests: Add the ability for a provider to have hooks Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * fix/unit-tests: Flag evaluation options added for hook merging Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * fix/unit-tests: Move numeric type methods to a private method within the client Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Add tests for new numeric methods Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Overwrite init on FlagEvaluationOptions Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Overwrite init on FlagEvaluationOptions Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Remove init on FlagEvaluationOptions Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Ensure before_hooks are evaluated in the opposite order Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Remove number flag evaluation in favour of strongly typed counterparts Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Check flag type after provider response Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Remove unnecessary static method Signed-off-by: Matthew Elwell <[email protected]> * feature/spec-0.2.0: Update docstring parameter name Signed-off-by: Matthew Elwell <[email protected]> * feature/spec-0.2.0: Fix typing of int and float methods Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> * feature/spec-0.2.0: Change provider methods to resolves rather than gets Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> Signed-off-by: Andrew Helsby <[email protected]> Co-authored-by: Matthew Elwell <[email protected]>
1 parent 06d0494 commit 311b8ee

File tree

8 files changed

+174
-58
lines changed

8 files changed

+174
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import typing
2+
from dataclasses import dataclass, field
3+
4+
from open_feature.hooks.hook import Hook
5+
6+
7+
@dataclass
8+
class FlagEvaluationOptions:
9+
hooks: typing.List[Hook] = field(default_factory=list)
10+
hook_hints: dict = field(default_factory=dict)

open_feature/flag_evaluation/flag_type.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33

44
class FlagType(Enum):
5-
BOOLEAN = 1
6-
STRING = 2
7-
NUMBER = 3
8-
OBJECT = 4
5+
BOOLEAN = bool
6+
STRING = str
7+
OBJECT = dict
8+
FLOAT = float
9+
INTEGER = int

open_feature/open_feature_client.py

+90-30
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import logging
22
import typing
3-
from numbers import Number
43

54
from open_feature.evaluation_context.evaluation_context import EvaluationContext
6-
from open_feature.exception.exceptions import GeneralError, OpenFeatureError
5+
from open_feature.exception.exceptions import (
6+
GeneralError,
7+
OpenFeatureError,
8+
TypeMismatchError,
9+
)
710
from open_feature.flag_evaluation.error_code import ErrorCode
811
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
12+
from open_feature.flag_evaluation.flag_evaluation_options import FlagEvaluationOptions
913
from open_feature.flag_evaluation.flag_type import FlagType
1014
from open_feature.flag_evaluation.reason import Reason
1115
from open_feature.hooks.hook import Hook
@@ -20,6 +24,8 @@
2024
from open_feature.provider.no_op_provider import NoOpProvider
2125
from open_feature.provider.provider import AbstractProvider
2226

27+
NUMERIC_TYPES = [FlagType.FLOAT, FlagType.INTEGER]
28+
2329

2430
class OpenFeatureClient:
2531
def __init__(
@@ -44,7 +50,7 @@ def get_boolean_value(
4450
flag_key: str,
4551
default_value: bool,
4652
evaluation_context: EvaluationContext = None,
47-
flag_evaluation_options: typing.Any = None,
53+
flag_evaluation_options: FlagEvaluationOptions = None,
4854
) -> bool:
4955
return self.evaluate_flag_details(
5056
FlagType.BOOLEAN,
@@ -59,7 +65,7 @@ def get_boolean_details(
5965
flag_key: str,
6066
default_value: bool,
6167
evaluation_context: EvaluationContext = None,
62-
flag_evaluation_options: typing.Any = None,
68+
flag_evaluation_options: FlagEvaluationOptions = None,
6369
) -> FlagEvaluationDetails:
6470
return self.evaluate_flag_details(
6571
FlagType.BOOLEAN,
@@ -74,7 +80,7 @@ def get_string_value(
7480
flag_key: str,
7581
default_value: str,
7682
evaluation_context: EvaluationContext = None,
77-
flag_evaluation_options: typing.Any = None,
83+
flag_evaluation_options: FlagEvaluationOptions = None,
7884
) -> str:
7985
return self.evaluate_flag_details(
8086
FlagType.STRING,
@@ -89,7 +95,7 @@ def get_string_details(
8995
flag_key: str,
9096
default_value: str,
9197
evaluation_context: EvaluationContext = None,
92-
flag_evaluation_options: typing.Any = None,
98+
flag_evaluation_options: FlagEvaluationOptions = None,
9399
) -> FlagEvaluationDetails:
94100
return self.evaluate_flag_details(
95101
FlagType.STRING,
@@ -99,30 +105,58 @@ def get_string_details(
99105
flag_evaluation_options,
100106
)
101107

102-
def get_number_value(
108+
def get_integer_value(
109+
self,
110+
flag_key: str,
111+
default_value: int,
112+
evaluation_context: EvaluationContext = None,
113+
flag_evaluation_options: FlagEvaluationOptions = None,
114+
) -> int:
115+
return self.get_integer_details(
116+
flag_key,
117+
default_value,
118+
evaluation_context,
119+
flag_evaluation_options,
120+
).value
121+
122+
def get_integer_details(
103123
self,
104124
flag_key: str,
105-
default_value: Number,
125+
default_value: int,
106126
evaluation_context: EvaluationContext = None,
107-
flag_evaluation_options: typing.Any = None,
108-
) -> Number:
127+
flag_evaluation_options: FlagEvaluationOptions = None,
128+
) -> FlagEvaluationDetails:
109129
return self.evaluate_flag_details(
110-
FlagType.NUMBER,
130+
FlagType.INTEGER,
131+
flag_key,
132+
default_value,
133+
evaluation_context,
134+
flag_evaluation_options,
135+
)
136+
137+
def get_float_value(
138+
self,
139+
flag_key: str,
140+
default_value: float,
141+
evaluation_context: EvaluationContext = None,
142+
flag_evaluation_options: FlagEvaluationOptions = None,
143+
) -> float:
144+
return self.get_float_details(
111145
flag_key,
112146
default_value,
113147
evaluation_context,
114148
flag_evaluation_options,
115149
).value
116150

117-
def get_number_details(
151+
def get_float_details(
118152
self,
119153
flag_key: str,
120-
default_value: Number,
154+
default_value: float,
121155
evaluation_context: EvaluationContext = None,
122-
flag_evaluation_options: typing.Any = None,
156+
flag_evaluation_options: FlagEvaluationOptions = None,
123157
) -> FlagEvaluationDetails:
124158
return self.evaluate_flag_details(
125-
FlagType.NUMBER,
159+
FlagType.FLOAT,
126160
flag_key,
127161
default_value,
128162
evaluation_context,
@@ -134,7 +168,7 @@ def get_object_value(
134168
flag_key: str,
135169
default_value: dict,
136170
evaluation_context: EvaluationContext = None,
137-
flag_evaluation_options: typing.Any = None,
171+
flag_evaluation_options: FlagEvaluationOptions = None,
138172
) -> dict:
139173
return self.evaluate_flag_details(
140174
FlagType.OBJECT,
@@ -149,7 +183,7 @@ def get_object_details(
149183
flag_key: str,
150184
default_value: dict,
151185
evaluation_context: EvaluationContext = None,
152-
flag_evaluation_options: typing.Any = None,
186+
flag_evaluation_options: FlagEvaluationOptions = None,
153187
) -> FlagEvaluationDetails:
154188
return self.evaluate_flag_details(
155189
FlagType.OBJECT,
@@ -165,13 +199,13 @@ def evaluate_flag_details(
165199
flag_key: str,
166200
default_value: typing.Any,
167201
evaluation_context: EvaluationContext = None,
168-
flag_evaluation_options: typing.Any = None,
202+
flag_evaluation_options: FlagEvaluationOptions = None,
169203
) -> FlagEvaluationDetails:
170204
"""
171205
Evaluate the flag requested by the user from the clients provider.
172206
173207
:param flag_type: the type of the flag being returned
174-
:param key: the string key of the selected flag
208+
:param flag_key: the string key of the selected flag
175209
:param default_value: backup value returned if no result found by the provider
176210
:param evaluation_context: Information for the purposes of flag evaluation
177211
:param flag_evaluation_options: Additional flag evaluation information
@@ -182,6 +216,9 @@ def evaluate_flag_details(
182216
if evaluation_context is None:
183217
evaluation_context = EvaluationContext()
184218

219+
if flag_evaluation_options is None:
220+
flag_evaluation_options = FlagEvaluationOptions()
221+
185222
hook_context = HookContext(
186223
flag_key=flag_key,
187224
flag_type=flag_type,
@@ -190,7 +227,22 @@ def evaluate_flag_details(
190227
client_metadata=None,
191228
provider_metadata=None,
192229
)
193-
merged_hooks = self.hooks
230+
# Todo add api level hooks
231+
# https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#requirement-442
232+
# Hooks need to be handled in different orders at different stages
233+
# in the flag evaluation
234+
# before: API, Client, Invocation, Provider
235+
merged_hooks = (
236+
self.hooks
237+
+ flag_evaluation_options.hooks
238+
+ self.provider.get_provider_hooks()
239+
)
240+
# after, error, finally: Provider, Invocation, Client, API
241+
reversed_merged_hooks = (
242+
self.provider.get_provider_hooks()
243+
+ flag_evaluation_options.hooks
244+
+ self.hooks
245+
)
194246

195247
try:
196248
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
@@ -201,7 +253,7 @@ def evaluate_flag_details(
201253
)
202254
invocation_context = invocation_context.merge(ctx2=evaluation_context)
203255

204-
# merge of: API.context, client.context, invocation.context
256+
# Requirement 3.2.2 merge: API.context->client.context->invocation.context
205257
merged_context = (
206258
api_evaluation_context().merge(self.context).merge(invocation_context)
207259
)
@@ -213,12 +265,14 @@ def evaluate_flag_details(
213265
merged_context,
214266
)
215267

216-
after_hooks(type, hook_context, flag_evaluation, merged_hooks, None)
268+
after_hooks(
269+
flag_type, hook_context, flag_evaluation, reversed_merged_hooks, None
270+
)
217271

218272
return flag_evaluation
219273

220274
except OpenFeatureError as e:
221-
error_hooks(flag_type, hook_context, e, merged_hooks, None)
275+
error_hooks(flag_type, hook_context, e, reversed_merged_hooks, None)
222276
return FlagEvaluationDetails(
223277
flag_key=flag_key,
224278
value=default_value,
@@ -229,7 +283,7 @@ def evaluate_flag_details(
229283
# Catch any type of exception here since the user can provide any exception
230284
# in the error hooks
231285
except Exception as e: # noqa
232-
error_hooks(flag_type, hook_context, e, merged_hooks, None)
286+
error_hooks(flag_type, hook_context, e, reversed_merged_hooks, None)
233287
error_message = getattr(e, "error_message", str(e))
234288
return FlagEvaluationDetails(
235289
flag_key=flag_key,
@@ -240,7 +294,7 @@ def evaluate_flag_details(
240294
)
241295

242296
finally:
243-
after_all_hooks(flag_type, hook_context, merged_hooks, None)
297+
after_all_hooks(flag_type, hook_context, reversed_merged_hooks, None)
244298

245299
def _create_provider_evaluation(
246300
self,
@@ -270,13 +324,19 @@ def _create_provider_evaluation(
270324
self.provider = NoOpProvider()
271325

272326
get_details_callable = {
273-
FlagType.BOOLEAN: self.provider.get_boolean_details,
274-
FlagType.NUMBER: self.provider.get_number_details,
275-
FlagType.OBJECT: self.provider.get_object_details,
276-
FlagType.STRING: self.provider.get_string_details,
327+
FlagType.BOOLEAN: self.provider.resolve_boolean_details,
328+
FlagType.INTEGER: self.provider.resolve_integer_details,
329+
FlagType.FLOAT: self.provider.resolve_float_details,
330+
FlagType.OBJECT: self.provider.resolve_object_details,
331+
FlagType.STRING: self.provider.resolve_string_details,
277332
}.get(flag_type)
278333

279334
if not get_details_callable:
280335
raise GeneralError(error_message="Unknown flag type")
281336

282-
return get_details_callable(*args)
337+
value = get_details_callable(*args)
338+
339+
if not isinstance(value.value, flag_type.value):
340+
raise TypeMismatchError()
341+
342+
return value

open_feature/provider/no_op_provider.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from numbers import Number
1+
import typing
22

33
from open_feature.evaluation_context.evaluation_context import EvaluationContext
44
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
55
from open_feature.flag_evaluation.reason import Reason
6+
from open_feature.hooks.hook import Hook
67
from open_feature.provider.metadata import Metadata
78
from open_feature.provider.no_op_metadata import NoOpMetadata
89
from open_feature.provider.provider import AbstractProvider
@@ -14,7 +15,10 @@ class NoOpProvider(AbstractProvider):
1415
def get_metadata(self) -> Metadata:
1516
return NoOpMetadata()
1617

17-
def get_boolean_details(
18+
def get_provider_hooks(self) -> typing.List[Hook]:
19+
return []
20+
21+
def resolve_boolean_details(
1822
self,
1923
flag_key: str,
2024
default_value: bool,
@@ -27,7 +31,7 @@ def get_boolean_details(
2731
variant=PASSED_IN_DEFAULT,
2832
)
2933

30-
def get_string_details(
34+
def resolve_string_details(
3135
self,
3236
flag_key: str,
3337
default_value: str,
@@ -40,10 +44,23 @@ def get_string_details(
4044
variant=PASSED_IN_DEFAULT,
4145
)
4246

43-
def get_number_details(
47+
def resolve_integer_details(
48+
self,
49+
flag_key: str,
50+
default_value: int,
51+
evaluation_context: EvaluationContext = None,
52+
):
53+
return FlagEvaluationDetails(
54+
flag_key=flag_key,
55+
value=default_value,
56+
reason=Reason.DEFAULT,
57+
variant=PASSED_IN_DEFAULT,
58+
)
59+
60+
def resolve_float_details(
4461
self,
4562
flag_key: str,
46-
default_value: Number,
63+
default_value: float,
4764
evaluation_context: EvaluationContext = None,
4865
):
4966
return FlagEvaluationDetails(
@@ -53,7 +70,7 @@ def get_number_details(
5370
variant=PASSED_IN_DEFAULT,
5471
)
5572

56-
def get_object_details(
73+
def resolve_object_details(
5774
self,
5875
flag_key: str,
5976
default_value: dict,

0 commit comments

Comments
 (0)