Skip to content

Commit 793ced1

Browse files
federicobondbeeme1mrtoddbaert
authored
refactor!: simplify namespaces to make public API more pythonic (#172)
* refactor!: simplify namespaces to make public API more pythonic Signed-off-by: Federico Bond <[email protected]> Co-authored-by: Michael Beemer <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 6f7cdb8 commit 793ced1

35 files changed

+286
-178
lines changed

open_feature/hooks/hook_support.py renamed to open_feature/_internal/hook_support.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
import typing
33
from functools import reduce
44

5-
from open_feature.evaluation_context.evaluation_context import EvaluationContext
6-
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
7-
from open_feature.flag_evaluation.flag_type import FlagType
8-
from open_feature.hooks.hook import Hook
9-
from open_feature.hooks.hook_context import HookContext
10-
from open_feature.hooks.hook_type import HookType
5+
from open_feature.evaluation_context import EvaluationContext
6+
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
7+
from open_feature.hook import Hook, HookContext, HookType
118

129

1310
def error_hooks(

open_feature/open_feature_api.py renamed to open_feature/api.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import typing
22

3-
from open_feature.evaluation_context.evaluation_context import EvaluationContext
4-
from open_feature.exception.exceptions import GeneralError
5-
from open_feature.hooks.hook import Hook
6-
from open_feature.open_feature_client import OpenFeatureClient
3+
from open_feature.client import OpenFeatureClient
4+
from open_feature.evaluation_context import EvaluationContext
5+
from open_feature.exception import GeneralError
6+
from open_feature.hook import Hook
77
from open_feature.provider.metadata import Metadata
88
from open_feature.provider.no_op_provider import NoOpProvider
99
from open_feature.provider.provider import AbstractProvider

open_feature/open_feature_client.py renamed to open_feature/client.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@
22
import typing
33
from dataclasses import dataclass
44

5-
from open_feature import open_feature_api as api
6-
from open_feature.evaluation_context.evaluation_context import EvaluationContext
7-
from open_feature.exception.error_code import ErrorCode
8-
from open_feature.exception.exceptions import (
5+
from open_feature import api
6+
from open_feature.evaluation_context import EvaluationContext
7+
from open_feature.exception import (
8+
ErrorCode,
99
GeneralError,
1010
OpenFeatureError,
1111
TypeMismatchError,
1212
)
13-
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
14-
from open_feature.flag_evaluation.flag_evaluation_options import FlagEvaluationOptions
15-
from open_feature.flag_evaluation.flag_type import FlagType
16-
from open_feature.flag_evaluation.reason import Reason
17-
from open_feature.flag_evaluation.resolution_details import FlagResolutionDetails
18-
from open_feature.hooks.hook import Hook
19-
from open_feature.hooks.hook_context import HookContext
20-
from open_feature.hooks.hook_support import (
13+
from open_feature.flag_evaluation import (
14+
FlagEvaluationDetails,
15+
FlagEvaluationOptions,
16+
FlagType,
17+
Reason,
18+
FlagResolutionDetails,
19+
)
20+
from open_feature.hook import Hook, HookContext
21+
from open_feature.hook.hook_support import (
2122
after_all_hooks,
2223
after_hooks,
2324
before_hooks,

open_feature/exception/exceptions.py renamed to open_feature/exception.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import typing
2+
from enum import Enum
23

3-
from open_feature.exception.error_code import ErrorCode
4+
5+
class ErrorCode(Enum):
6+
PROVIDER_NOT_READY = "PROVIDER_NOT_READY"
7+
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
8+
PARSE_ERROR = "PARSE_ERROR"
9+
TYPE_MISMATCH = "TYPE_MISMATCH"
10+
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
11+
INVALID_CONTEXT = "INVALID_CONTEXT"
12+
GENERAL = "GENERAL"
413

514

615
class OpenFeatureError(Exception):

open_feature/exception/__init__.py

Whitespace-only changes.

open_feature/exception/error_code.py

-11
This file was deleted.

open_feature/flag_evaluation.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
import typing
3+
from dataclasses import dataclass, field
4+
5+
from open_feature._backports.strenum import StrEnum
6+
from open_feature.exception import ErrorCode
7+
8+
if typing.TYPE_CHECKING: # resolves a circular dependency in type annotations
9+
from open_feature.hook import Hook
10+
11+
12+
class FlagType(StrEnum):
13+
BOOLEAN = "BOOLEAN"
14+
STRING = "STRING"
15+
OBJECT = "OBJECT"
16+
FLOAT = "FLOAT"
17+
INTEGER = "INTEGER"
18+
19+
20+
class Reason(StrEnum):
21+
CACHED = "CACHED"
22+
DEFAULT = "DEFAULT"
23+
DISABLED = "DISABLED"
24+
ERROR = "ERROR"
25+
STATIC = "STATIC"
26+
SPLIT = "SPLIT"
27+
TARGETING_MATCH = "TARGETING_MATCH"
28+
UNKNOWN = "UNKNOWN"
29+
30+
31+
T = typing.TypeVar("T", covariant=True)
32+
33+
34+
@dataclass
35+
class FlagEvaluationDetails(typing.Generic[T]):
36+
flag_key: str
37+
value: T
38+
variant: typing.Optional[str] = None
39+
reason: typing.Optional[Reason] = None
40+
error_code: typing.Optional[ErrorCode] = None
41+
error_message: typing.Optional[str] = None
42+
43+
44+
@dataclass
45+
class FlagEvaluationOptions:
46+
hooks: typing.List[Hook] = field(default_factory=list)
47+
hook_hints: dict = field(default_factory=dict)
48+
49+
50+
U = typing.TypeVar("U", covariant=True)
51+
52+
53+
@dataclass
54+
class FlagResolutionDetails(typing.Generic[U]):
55+
value: U
56+
error_code: typing.Optional[ErrorCode] = None
57+
error_message: typing.Optional[str] = None
58+
reason: typing.Optional[Reason] = None
59+
variant: typing.Optional[str] = None
60+
flag_metadata: typing.Optional[str] = None

open_feature/flag_evaluation/__init__.py

Whitespace-only changes.

open_feature/flag_evaluation/flag_evaluation_details.py

-17
This file was deleted.

open_feature/flag_evaluation/flag_evaluation_options.py

-10
This file was deleted.

open_feature/flag_evaluation/flag_type.py

-9
This file was deleted.

open_feature/flag_evaluation/reason.py

-12
This file was deleted.

open_feature/flag_evaluation/resolution_details.py

-17
This file was deleted.

open_feature/hooks/hook.py renamed to open_feature/hook/__init__.py

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
from __future__ import annotations
2+
import typing
13
from abc import abstractmethod
4+
from dataclasses import dataclass
5+
from enum import Enum
26

3-
from open_feature.evaluation_context.evaluation_context import EvaluationContext
4-
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
5-
from open_feature.flag_evaluation.flag_type import FlagType
6-
from open_feature.hooks.hook_context import HookContext
7+
from open_feature.evaluation_context import EvaluationContext
8+
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
9+
10+
11+
class HookType(Enum):
12+
BEFORE = "before"
13+
AFTER = "after"
14+
FINALLY_AFTER = "finally_after"
15+
ERROR = "error"
16+
17+
18+
@dataclass
19+
class HookContext:
20+
flag_key: str
21+
flag_type: FlagType
22+
default_value: typing.Any
23+
evaluation_context: EvaluationContext
24+
client_metadata: typing.Optional[dict] = None
25+
provider_metadata: typing.Optional[dict] = None
726

827

928
class Hook:

open_feature/hook/hook_support.py

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import logging
2+
import typing
3+
from functools import reduce
4+
5+
from open_feature.evaluation_context import EvaluationContext
6+
from open_feature.flag_evaluation import FlagEvaluationDetails, FlagType
7+
from open_feature.hook import Hook, HookContext, HookType
8+
9+
10+
def error_hooks(
11+
flag_type: FlagType,
12+
hook_context: HookContext,
13+
exception: Exception,
14+
hooks: typing.List[Hook],
15+
hints: typing.Optional[typing.Mapping] = None,
16+
):
17+
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
18+
_execute_hooks(
19+
flag_type=flag_type, hooks=hooks, hook_method=HookType.ERROR, **kwargs
20+
)
21+
22+
23+
def after_all_hooks(
24+
flag_type: FlagType,
25+
hook_context: HookContext,
26+
hooks: typing.List[Hook],
27+
hints: typing.Optional[typing.Mapping] = None,
28+
):
29+
kwargs = {"hook_context": hook_context, "hints": hints}
30+
_execute_hooks(
31+
flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs
32+
)
33+
34+
35+
def after_hooks(
36+
flag_type: FlagType,
37+
hook_context: HookContext,
38+
details: FlagEvaluationDetails,
39+
hooks: typing.List[Hook],
40+
hints: typing.Optional[typing.Mapping] = None,
41+
):
42+
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
43+
_execute_hooks_unchecked(
44+
flag_type=flag_type, hooks=hooks, hook_method=HookType.AFTER, **kwargs
45+
)
46+
47+
48+
def before_hooks(
49+
flag_type: FlagType,
50+
hook_context: HookContext,
51+
hooks: typing.List[Hook],
52+
hints: typing.Optional[typing.Mapping] = None,
53+
) -> EvaluationContext:
54+
kwargs = {"hook_context": hook_context, "hints": hints}
55+
executed_hooks = _execute_hooks_unchecked(
56+
flag_type=flag_type, hooks=hooks, hook_method=HookType.BEFORE, **kwargs
57+
)
58+
filtered_hooks = list(filter(lambda hook: hook is not None, executed_hooks))
59+
60+
if filtered_hooks:
61+
return reduce(lambda a, b: a.merge(b), filtered_hooks)
62+
63+
return EvaluationContext()
64+
65+
66+
def _execute_hooks(
67+
flag_type: FlagType, hooks: typing.List[Hook], hook_method: HookType, **kwargs
68+
) -> list:
69+
"""
70+
Run multiple hooks of any hook type. All of these hooks will be run through an
71+
exception check.
72+
73+
:param flag_type: particular type of flag
74+
:param hooks: a list of hooks
75+
:param hook_method: the type of hook that is being run
76+
:param kwargs: arguments that need to be provided to the hook method
77+
:return: a list of results from the applied hook methods
78+
"""
79+
if hooks:
80+
filtered_hooks = list(
81+
filter(
82+
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
83+
)
84+
)
85+
return [
86+
_execute_hook_checked(hook, hook_method, **kwargs)
87+
for hook in filtered_hooks
88+
]
89+
return []
90+
91+
92+
def _execute_hooks_unchecked(
93+
flag_type: FlagType, hooks, hook_method: HookType, **kwargs
94+
) -> list:
95+
"""
96+
Execute a single hook without checking whether an exception is thrown. This is
97+
used in the before and after hooks since any exception will be caught in the
98+
client.
99+
100+
:param flag_type: particular type of flag
101+
:param hooks: a list of hooks
102+
:param hook_method: the type of hook that is being run
103+
:param kwargs: arguments that need to be provided to the hook method
104+
:return: a list of results from the applied hook methods
105+
"""
106+
if hooks:
107+
filtered_hooks = list(
108+
filter(
109+
lambda hook: hook.supports_flag_value_type(flag_type=flag_type), hooks
110+
)
111+
)
112+
return [getattr(hook, hook_method.value)(**kwargs) for hook in filtered_hooks]
113+
114+
return []
115+
116+
117+
def _execute_hook_checked(hook: Hook, hook_method: HookType, **kwargs):
118+
"""
119+
Try and run a single hook and catch any exception thrown. This is used in the
120+
after all and error hooks since any error thrown at this point needs to be caught.
121+
122+
:param hook: a list of hooks
123+
:param hook_method: the type of hook that is being run
124+
:param kwargs: arguments that need to be provided to the hook method
125+
:return: the result of the hook method
126+
"""
127+
try:
128+
return getattr(hook, hook_method.value)(**kwargs)
129+
except Exception: # noqa
130+
logging.error(f"Exception when running {hook_method.value} hooks")

open_feature/hooks/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)