Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@
import typing
from functools import reduce

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
from open_feature.flag_evaluation.flag_type import FlagType
from open_feature.hooks.hook import Hook
from open_feature.hooks.hook_context import HookContext
from open_feature.hooks.hook_type import HookType
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
from open_feature.hook import Hook, HookContext, HookType


def error_hooks(
Expand Down
8 changes: 4 additions & 4 deletions open_feature/open_feature_api.py → open_feature/api.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import typing

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.exception.exceptions import GeneralError
from open_feature.hooks.hook import Hook
from open_feature.open_feature_client import OpenFeatureClient
from open_feature.client import OpenFeatureClient
from open_feature.evaluation_context import EvaluationContext
from open_feature.exception import GeneralError
from open_feature.hook import Hook
from open_feature.provider.metadata import Metadata
from open_feature.provider.no_op_provider import NoOpProvider
from open_feature.provider.provider import AbstractProvider
Expand Down
25 changes: 13 additions & 12 deletions open_feature/open_feature_client.py → open_feature/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@
import typing
from dataclasses import dataclass

from open_feature import open_feature_api as api
from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.exception.error_code import ErrorCode
from open_feature.exception.exceptions import (
from open_feature import api
from open_feature.evaluation_context import EvaluationContext
from open_feature.exception import (
ErrorCode,
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.flag_evaluation.resolution_details import FlagResolutionDetails
from open_feature.hooks.hook import Hook
from open_feature.hooks.hook_context import HookContext
from open_feature.hooks.hook_support import (
from open_feature.flag_evaluation import (
FlagEvaluationDetails,
FlagEvaluationOptions,
FlagType,
Reason,
FlagResolutionDetails,
)
from open_feature.hook import Hook, HookContext
from open_feature.hook.hook_support import (
after_all_hooks,
after_hooks,
before_hooks,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import typing
from enum import Enum

from open_feature.exception.error_code import ErrorCode

class ErrorCode(Enum):
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
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"


class OpenFeatureError(Exception):
Expand Down
Empty file.
11 changes: 0 additions & 11 deletions open_feature/exception/error_code.py

This file was deleted.

60 changes: 60 additions & 0 deletions open_feature/flag_evaluation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations
import typing
from dataclasses import dataclass, field

from open_feature._backports.strenum import StrEnum
from open_feature.exception import ErrorCode

if typing.TYPE_CHECKING: # resolves a circular dependency in type annotations
from open_feature.hook import Hook


class FlagType(StrEnum):
BOOLEAN = "BOOLEAN"
STRING = "STRING"
OBJECT = "OBJECT"
FLOAT = "FLOAT"
INTEGER = "INTEGER"


class Reason(StrEnum):
CACHED = "CACHED"
DEFAULT = "DEFAULT"
DISABLED = "DISABLED"
ERROR = "ERROR"
STATIC = "STATIC"
SPLIT = "SPLIT"
TARGETING_MATCH = "TARGETING_MATCH"
UNKNOWN = "UNKNOWN"


T = typing.TypeVar("T", covariant=True)


@dataclass
class FlagEvaluationDetails(typing.Generic[T]):
flag_key: str
value: T
variant: typing.Optional[str] = None
reason: typing.Optional[Reason] = None
error_code: typing.Optional[ErrorCode] = None
error_message: typing.Optional[str] = None


@dataclass
class FlagEvaluationOptions:
hooks: typing.List[Hook] = field(default_factory=list)
hook_hints: dict = field(default_factory=dict)


U = typing.TypeVar("U", covariant=True)


@dataclass
class FlagResolutionDetails(typing.Generic[U]):
value: U
error_code: typing.Optional[ErrorCode] = None
error_message: typing.Optional[str] = None
reason: typing.Optional[Reason] = None
variant: typing.Optional[str] = None
flag_metadata: typing.Optional[str] = None
Empty file.
17 changes: 0 additions & 17 deletions open_feature/flag_evaluation/flag_evaluation_details.py

This file was deleted.

10 changes: 0 additions & 10 deletions open_feature/flag_evaluation/flag_evaluation_options.py

This file was deleted.

9 changes: 0 additions & 9 deletions open_feature/flag_evaluation/flag_type.py

This file was deleted.

12 changes: 0 additions & 12 deletions open_feature/flag_evaluation/reason.py

This file was deleted.

17 changes: 0 additions & 17 deletions open_feature/flag_evaluation/resolution_details.py

This file was deleted.

27 changes: 23 additions & 4 deletions open_feature/hooks/hook.py → open_feature/hook/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
from __future__ import annotations
import typing
from abc import abstractmethod
from dataclasses import dataclass
from enum import Enum

from open_feature.evaluation_context.evaluation_context import EvaluationContext
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
from open_feature.flag_evaluation.flag_type import FlagType
from open_feature.hooks.hook_context import HookContext
from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType


class HookType(Enum):
BEFORE = "before"
AFTER = "after"
FINALLY_AFTER = "finally_after"
ERROR = "error"


@dataclass
class HookContext:
flag_key: str
flag_type: FlagType
default_value: typing.Any
evaluation_context: EvaluationContext
client_metadata: typing.Optional[dict] = None
provider_metadata: typing.Optional[dict] = None


class Hook:
Expand Down
130 changes: 130 additions & 0 deletions open_feature/hook/hook_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import logging
import typing
from functools import reduce

from open_feature.evaluation_context import EvaluationContext
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
from open_feature.hook import Hook, HookContext, HookType


def error_hooks(
flag_type: FlagType,
hook_context: HookContext,
exception: Exception,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
_execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.ERROR, **kwargs
)


def after_all_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "hints": hints}
_execute_hooks(
flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs
)


def after_hooks(
flag_type: FlagType,
hook_context: HookContext,
details: FlagEvaluationDetails,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
):
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
_execute_hooks_unchecked(
flag_type=flag_type, hooks=hooks, hook_method=HookType.AFTER, **kwargs
)


def before_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
) -> EvaluationContext:
kwargs = {"hook_context": hook_context, "hints": hints}
executed_hooks = _execute_hooks_unchecked(
flag_type=flag_type, hooks=hooks, hook_method=HookType.BEFORE, **kwargs
)
filtered_hooks = list(filter(lambda hook: hook is not None, executed_hooks))

if filtered_hooks:
return reduce(lambda a, b: a.merge(b), filtered_hooks)

return EvaluationContext()


def _execute_hooks(
flag_type: FlagType, hooks: typing.List[Hook], hook_method: HookType, **kwargs
) -> list:
"""
Run multiple hooks of any hook type. All of these hooks will be run through an
exception check.

:param flag_type: particular type of flag
:param hooks: a list of hooks
:param hook_method: the type of hook that is being run
:param kwargs: arguments that need to be provided to the hook method
:return: a list of results from the applied hook methods
"""
if hooks:
filtered_hooks = list(
filter(
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
)
)
return [
_execute_hook_checked(hook, hook_method, **kwargs)
for hook in filtered_hooks
]
return []


def _execute_hooks_unchecked(
flag_type: FlagType, hooks, hook_method: HookType, **kwargs
) -> list:
"""
Execute a single hook without checking whether an exception is thrown. This is
used in the before and after hooks since any exception will be caught in the
client.

:param flag_type: particular type of flag
:param hooks: a list of hooks
:param hook_method: the type of hook that is being run
:param kwargs: arguments that need to be provided to the hook method
:return: a list of results from the applied hook methods
"""
if hooks:
filtered_hooks = list(
filter(
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
)
)
return [getattr(hook, hook_method.value)(**kwargs) for hook in filtered_hooks]

return []


def _execute_hook_checked(hook: Hook, hook_method: HookType, **kwargs):
"""
Try and run a single hook and catch any exception thrown. This is used in the
after all and error hooks since any error thrown at this point needs to be caught.

:param hook: a list of hooks
:param hook_method: the type of hook that is being run
:param kwargs: arguments that need to be provided to the hook method
:return: the result of the hook method
"""
try:
return getattr(hook, hook_method.value)(**kwargs)
except Exception: # noqa
logging.error(f"Exception when running {hook_method.value} hooks")
Empty file removed open_feature/hooks/__init__.py
Empty file.
Loading