Skip to content

refactor!: simplify namespaces to make public API more pythonic #172

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Sep 8, 2023
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