Skip to content

Commit

Permalink
refactor!: simplify namespaces to make public API more pythonic (#172)
Browse files Browse the repository at this point in the history
* refactor!: simplify namespaces to make public API more pythonic

Signed-off-by: Federico Bond <federicobond@gmail.com>
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
  • Loading branch information
3 people authored Sep 8, 2023
1 parent 6f7cdb8 commit 793ced1
Show file tree
Hide file tree
Showing 35 changed files with 286 additions and 178 deletions.
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
File renamed without changes.
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 removed open_feature/exception/__init__.py
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

0 comments on commit 793ced1

Please sign in to comment.