Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ class ErrorCode(Enum):
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
PARSE_ERROR = "PARSE_ERROR"
TYPE_MISMATCH = "TYPE_MISMATCH"
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
INVALID_CONTEXT = "INVALID_CONTEXT"
GENERAL = "GENERAL"
41 changes: 20 additions & 21 deletions open_feature/exception/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from open_feature.flag_evaluation.error_code import ErrorCode
import typing

from open_feature.exception.error_code import ErrorCode


class OpenFeatureError(Exception):
Expand All @@ -7,13 +9,14 @@ class OpenFeatureError(Exception):
the more specific exceptions extending this one should be used.
"""

def __init__(self, error_message: str = None, error_code: ErrorCode = None):
def __init__(
self, error_message: typing.Optional[str] = None, error_code: ErrorCode = None
):
"""
Constructor for the generic OpenFeatureError.
@param error_message: a string message representing why the error has been
raised
@param error_message: an optional string message representing why the
error has been raised
@param error_code: the ErrorCode string enum value for the type of error
@return: the generic OpenFeatureError exception
"""
self.error_message = error_message
self.error_code = error_code
Expand All @@ -25,13 +28,12 @@ class FlagNotFoundError(OpenFeatureError):
key provided by the user.
"""

def __init__(self, error_message: str = None):
def __init__(self, error_message: typing.Optional[str] = None):
"""
Constructor for the FlagNotFoundError. The error code for
this type of exception is ErrorCode.FLAG_NOT_FOUND.
@param error_message: a string message representing why the error has been
raised
@return: the generic FlagNotFoundError exception
@param error_message: an optional string message representing
why the error has been raised
"""
super().__init__(error_message, ErrorCode.FLAG_NOT_FOUND)

Expand All @@ -42,13 +44,12 @@ class GeneralError(OpenFeatureError):
feature python sdk.
"""

def __init__(self, error_message: str = None):
def __init__(self, error_message: typing.Optional[str] = None):
"""
Constructor for the GeneralError. The error code for this type of exception
is ErrorCode.GENERAL.
@param error_message: a string message representing why the error has been
raised
@return: the generic GeneralError exception
@param error_message: an optional string message representing why the error
has been raised
"""
super().__init__(error_message, ErrorCode.GENERAL)

Expand All @@ -59,13 +60,12 @@ class ParseError(OpenFeatureError):
be parsed into a FlagEvaluationDetails object.
"""

def __init__(self, error_message: str = None):
def __init__(self, error_message: typing.Optional[str] = None):
"""
Constructor for the ParseError. The error code for this type of exception
is ErrorCode.PARSE_ERROR.
@param error_message: a string message representing why the error has been
raised
@return: the generic ParseError exception
@param error_message: an optional string message representing why the
error has been raised
"""
super().__init__(error_message, ErrorCode.PARSE_ERROR)

Expand All @@ -76,12 +76,11 @@ class TypeMismatchError(OpenFeatureError):
not match the type requested by the user.
"""

def __init__(self, error_message: str = None):
def __init__(self, error_message: typing.Optional[str] = None):
"""
Constructor for the TypeMismatchError. The error code for this type of
exception is ErrorCode.TYPE_MISMATCH.
@param error_message: a string message representing why the error has been
raised
@return: the generic TypeMismatchError exception
@param error_message: an optional string message representing why the
error has been raised
"""
super().__init__(error_message, ErrorCode.TYPE_MISMATCH)
4 changes: 2 additions & 2 deletions open_feature/flag_evaluation/flag_evaluation_details.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing
from dataclasses import dataclass

from open_feature.flag_evaluation.error_code import ErrorCode
from open_feature.exception.error_code import ErrorCode
from open_feature.flag_evaluation.reason import Reason


Expand All @@ -12,4 +12,4 @@ class FlagEvaluationDetails:
variant: str = None
reason: Reason = None
error_code: ErrorCode = None
error_message: str = None
error_message: typing.Optional[str] = None
10 changes: 10 additions & 0 deletions open_feature/flag_evaluation/flag_evaluation_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import typing
from dataclasses import dataclass, field

from open_feature.hooks.hook import Hook


@dataclass
class FlagEvaluationOptions:
hooks: typing.List[Hook] = field(default_factory=list)
hook_hints: dict = field(default_factory=dict)
2 changes: 2 additions & 0 deletions open_feature/flag_evaluation/flag_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ class FlagType(Enum):
STRING = 2
NUMBER = 3
OBJECT = 4
FLOAT = 5
INTEGER = 6
106 changes: 98 additions & 8 deletions open_feature/open_feature_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
from numbers import Number

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.exception.exceptions import GeneralError, OpenFeatureError
from open_feature.flag_evaluation.error_code import ErrorCode
from open_feature.exception.error_code import ErrorCode
from open_feature.exception.exceptions import (
GeneralError,
OpenFeatureError,
TypeMismatchError,
)
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
from open_feature.flag_evaluation.flag_evaluation_options import FlagEvaluationOptions
from open_feature.flag_evaluation.flag_type import FlagType
from open_feature.flag_evaluation.reason import Reason
from open_feature.hooks.hook import Hook
Expand All @@ -20,6 +25,8 @@
from open_feature.provider.no_op_provider import NoOpProvider
from open_feature.provider.provider import AbstractProvider

NUMERIC_TYPES = [FlagType.FLOAT, FlagType.INTEGER]


class OpenFeatureClient:
def __init__(
Expand Down Expand Up @@ -129,6 +136,64 @@ def get_number_details(
flag_evaluation_options,
)

def get_integer_value(
self,
flag_key: str,
default_value: int,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: typing.Any = None,
) -> int:
return self.get_integer_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value

def get_integer_details(
self,
flag_key: str,
default_value: int,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: typing.Any = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.INTEGER,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)

def get_float_value(
self,
flag_key: str,
default_value: float,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: typing.Any = None,
) -> float:
return self.get_float_details(
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
).value

def get_float_details(
self,
flag_key: str,
default_value: float,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: typing.Any = None,
) -> FlagEvaluationDetails:
return self.evaluate_flag_details(
FlagType.FLOAT,
flag_key,
default_value,
evaluation_context,
flag_evaluation_options,
)

def get_object_value(
self,
flag_key: str,
Expand Down Expand Up @@ -165,7 +230,7 @@ def evaluate_flag_details(
flag_key: str,
default_value: typing.Any,
evaluation_context: EvaluationContext = None,
flag_evaluation_options: typing.Any = None,
flag_evaluation_options: FlagEvaluationOptions = None,
) -> FlagEvaluationDetails:
"""
Evaluate the flag requested by the user from the clients provider.
Expand All @@ -182,6 +247,9 @@ def evaluate_flag_details(
if evaluation_context is None:
evaluation_context = EvaluationContext()

if flag_evaluation_options is None:
flag_evaluation_options = FlagEvaluationOptions()

hook_context = HookContext(
flag_key=flag_key,
flag_type=flag_type,
Expand All @@ -190,18 +258,21 @@ def evaluate_flag_details(
client_metadata=None,
provider_metadata=None,
)
merged_hooks = self.hooks
merged_hooks = (
self.provider.get_provider_hooks()
+ flag_evaluation_options.hooks
+ self.hooks
)

try:
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
# Any resulting evaluation context from a before hook will overwrite
# duplicate fields defined globally, on the client, or in the invocation.
# Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context
invocation_context = before_hooks(
flag_type, hook_context, merged_hooks, None
)
invocation_context.merge(ctx2=evaluation_context)

# merge of: API.context, client.context, invocation.context
merged_context = (
api_evaluation_context().merge(self.context).merge(invocation_context)
)
Expand All @@ -213,7 +284,7 @@ def evaluate_flag_details(
merged_context,
)

after_hooks(type, hook_context, flag_evaluation, merged_hooks, None)
after_hooks(flag_type, hook_context, flag_evaluation, merged_hooks, None)

return flag_evaluation

Expand Down Expand Up @@ -272,11 +343,30 @@ def _create_provider_evaluation(
get_details_callable = {
FlagType.BOOLEAN: self.provider.get_boolean_details,
FlagType.NUMBER: self.provider.get_number_details,
FlagType.INTEGER: self.provider.get_number_details,
FlagType.FLOAT: self.provider.get_number_details,
FlagType.OBJECT: self.provider.get_object_details,
FlagType.STRING: self.provider.get_string_details,
}.get(flag_type)

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

return get_details_callable(*args)
value = get_details_callable(*args)

if flag_type in NUMERIC_TYPES:
value.value = self._convert_numeric_types(flag_type, value.value)

return value

@staticmethod
def _convert_numeric_types(flag_type: FlagType, current_value: Number):
converter = {
FlagType.FLOAT: float,
FlagType.INTEGER: int,
}.get(flag_type)

try:
return converter(current_value)
except ValueError:
raise TypeMismatchError()
5 changes: 5 additions & 0 deletions open_feature/provider/no_op_provider.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import typing
from numbers import Number

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
from open_feature.flag_evaluation.reason import Reason
from open_feature.hooks.hook import Hook
from open_feature.provider.metadata import Metadata
from open_feature.provider.no_op_metadata import NoOpMetadata
from open_feature.provider.provider import AbstractProvider
Expand All @@ -14,6 +16,9 @@ class NoOpProvider(AbstractProvider):
def get_metadata(self) -> Metadata:
return NoOpMetadata()

def get_provider_hooks(self) -> typing.List[Hook]:
return []

def get_boolean_details(
self,
flag_key: str,
Expand Down
6 changes: 6 additions & 0 deletions open_feature/provider/provider.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import typing
from abc import abstractmethod
from numbers import Number

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.hooks.hook import Hook
from open_feature.provider.metadata import Metadata


Expand All @@ -10,6 +12,10 @@ class AbstractProvider:
def get_metadata(self) -> Metadata:
pass

@abstractmethod
def get_provider_hooks(self) -> typing.List[Hook]:
return []

@abstractmethod
def get_boolean_details(
self,
Expand Down
2 changes: 1 addition & 1 deletion tests/test_open_feature_api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest

from open_feature.exception.error_code import ErrorCode
from open_feature.exception.exceptions import GeneralError
from open_feature.flag_evaluation.error_code import ErrorCode
from open_feature.open_feature_api import get_client, get_provider, set_provider
from open_feature.provider.no_op_provider import NoOpProvider

Expand Down
6 changes: 5 additions & 1 deletion tests/test_open_feature_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import pytest

from open_feature.exception.error_code import ErrorCode
from open_feature.exception.exceptions import OpenFeatureError
from open_feature.flag_evaluation.error_code import ErrorCode
from open_feature.flag_evaluation.reason import Reason
from open_feature.hooks.hook import Hook

Expand All @@ -15,6 +15,8 @@
(bool, True, "get_boolean_value"),
(str, "String", "get_string_value"),
(Number, 100, "get_number_value"),
(int, 100, "get_integer_value"),
(float, 10.23, "get_float_value"),
(
dict,
{
Expand Down Expand Up @@ -46,6 +48,8 @@ def test_should_get_flag_value_based_on_method_type(
(bool, True, "get_boolean_details"),
(str, "String", "get_string_details"),
(Number, 100, "get_number_details"),
(int, 100, "get_integer_details"),
(float, 10.23, "get_float_details"),
(
dict,
{
Expand Down
2 changes: 1 addition & 1 deletion tests/test_open_feature_evaluation_context.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.exception.error_code import ErrorCode
from open_feature.exception.exceptions import GeneralError
from open_feature.flag_evaluation.error_code import ErrorCode
from open_feature.open_feature_evaluation_context import (
api_evaluation_context,
set_api_evaluation_context,
Expand Down