Skip to content

Commit 92f5da4

Browse files
authored
feat: add hook data (#533)
* add hook data Signed-off-by: gruebel <[email protected]> * remove redundant reduce Signed-off-by: gruebel <[email protected]> --------- Signed-off-by: gruebel <[email protected]>
1 parent 32fdec1 commit 92f5da4

File tree

6 files changed

+205
-100
lines changed

6 files changed

+205
-100
lines changed

openfeature/client.py

Lines changed: 81 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import typing
33
from collections.abc import Awaitable, Sequence
44
from dataclasses import dataclass
5+
from itertools import chain
56

67
from openfeature import _event_support
78
from openfeature.evaluation_context import EvaluationContext, get_evaluation_context
@@ -420,10 +421,10 @@ def _establish_hooks_and_provider(
420421
flag_evaluation_options: typing.Optional[FlagEvaluationOptions],
421422
) -> tuple[
422423
FeatureProvider,
423-
HookContext,
424424
HookHints,
425-
list[Hook],
426-
list[Hook],
425+
list[tuple[Hook, HookContext]],
426+
list[tuple[Hook, HookContext]],
427+
EvaluationContext,
427428
]:
428429
if evaluation_context is None:
429430
evaluation_context = EvaluationContext()
@@ -444,25 +445,43 @@ def _establish_hooks_and_provider(
444445
.merge(evaluation_context)
445446
)
446447

447-
hook_context = HookContext(
448-
flag_key=flag_key,
449-
flag_type=flag_type,
450-
default_value=default_value,
451-
evaluation_context=merged_eval_context,
452-
client_metadata=self.get_metadata(),
453-
provider_metadata=provider.get_metadata(),
454-
)
448+
client_metadata = self.get_metadata()
449+
provider_metadata = provider.get_metadata()
450+
455451
# Hooks need to be handled in different orders at different stages
456452
# in the flag evaluation
457453
# before: API, Client, Invocation, Provider
458-
merged_hooks = (
459-
get_hooks() + self.hooks + evaluation_hooks + provider.get_provider_hooks()
460-
)
454+
merged_hooks_and_context = [
455+
(
456+
hook,
457+
HookContext(
458+
flag_key=flag_key,
459+
flag_type=flag_type,
460+
default_value=default_value,
461+
evaluation_context=merged_eval_context,
462+
client_metadata=client_metadata,
463+
provider_metadata=provider_metadata,
464+
hook_data={},
465+
),
466+
)
467+
for hook in chain(
468+
get_hooks(),
469+
self.hooks,
470+
evaluation_hooks,
471+
provider.get_provider_hooks(),
472+
)
473+
]
461474
# after, error, finally: Provider, Invocation, Client, API
462-
reversed_merged_hooks = merged_hooks[:]
463-
reversed_merged_hooks.reverse()
464-
465-
return provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks
475+
reversed_merged_hooks_and_context = merged_hooks_and_context[:]
476+
reversed_merged_hooks_and_context.reverse()
477+
478+
return (
479+
provider,
480+
hook_hints,
481+
merged_hooks_and_context,
482+
reversed_merged_hooks_and_context,
483+
merged_eval_context,
484+
)
466485

467486
def _assert_provider_status(
468487
self,
@@ -477,24 +496,21 @@ def _assert_provider_status(
477496
def _run_before_hooks_and_update_context(
478497
self,
479498
flag_type: FlagType,
480-
hook_context: HookContext,
481-
merged_hooks: list[Hook],
499+
merged_hooks_and_context: list[tuple[Hook, HookContext]],
482500
hook_hints: HookHints,
483-
evaluation_context: typing.Optional[EvaluationContext],
501+
evaluation_context: EvaluationContext,
484502
) -> EvaluationContext:
485503
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
486504
# Any resulting evaluation context from a before hook will overwrite
487505
# duplicate fields defined globally, on the client, or in the invocation.
488506
# Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context
489507
before_hooks_context = before_hooks(
490-
flag_type, hook_context, merged_hooks, hook_hints
508+
flag_type, merged_hooks_and_context, hook_hints
491509
)
492510

493511
# The hook_context.evaluation_context already contains the merged context from
494512
# _establish_hooks_and_provider, so we just need to merge with the before hooks result
495-
merged_context = hook_context.evaluation_context.merge(before_hooks_context)
496-
497-
return merged_context
513+
return evaluation_context.merge(before_hooks_context)
498514

499515
@typing.overload
500516
async def evaluate_flag_details_async(
@@ -575,23 +591,26 @@ async def evaluate_flag_details_async(
575591
:return: a typing.Awaitable[FlagEvaluationDetails] object with the fully evaluated flag from a
576592
provider
577593
"""
578-
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
579-
self._establish_hooks_and_provider(
580-
flag_type,
581-
flag_key,
582-
default_value,
583-
evaluation_context,
584-
flag_evaluation_options,
585-
)
594+
(
595+
provider,
596+
hook_hints,
597+
merged_hooks_and_context,
598+
reversed_merged_hooks_and_context,
599+
merged_eval_context,
600+
) = self._establish_hooks_and_provider(
601+
flag_type,
602+
flag_key,
603+
default_value,
604+
evaluation_context,
605+
flag_evaluation_options,
586606
)
587607

588608
try:
589609
if provider_err := self._assert_provider_status():
590610
error_hooks(
591611
flag_type,
592-
hook_context,
593612
provider_err,
594-
reversed_merged_hooks,
613+
reversed_merged_hooks_and_context,
595614
hook_hints,
596615
)
597616
flag_evaluation = FlagEvaluationDetails(
@@ -605,10 +624,9 @@ async def evaluate_flag_details_async(
605624

606625
merged_context = self._run_before_hooks_and_update_context(
607626
flag_type,
608-
hook_context,
609-
merged_hooks,
627+
merged_hooks_and_context,
610628
hook_hints,
611-
evaluation_context,
629+
merged_eval_context,
612630
)
613631

614632
flag_evaluation = await self._create_provider_evaluation_async(
@@ -620,22 +638,21 @@ async def evaluate_flag_details_async(
620638
)
621639
if err := flag_evaluation.get_exception():
622640
error_hooks(
623-
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
641+
flag_type, err, reversed_merged_hooks_and_context, hook_hints
624642
)
625643
return flag_evaluation
626644

627645
after_hooks(
628646
flag_type,
629-
hook_context,
630647
flag_evaluation,
631-
reversed_merged_hooks,
648+
reversed_merged_hooks_and_context,
632649
hook_hints,
633650
)
634651

635652
return flag_evaluation
636653

637654
except OpenFeatureError as err:
638-
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
655+
error_hooks(flag_type, err, reversed_merged_hooks_and_context, hook_hints)
639656
flag_evaluation = FlagEvaluationDetails(
640657
flag_key=flag_key,
641658
value=default_value,
@@ -651,7 +668,7 @@ async def evaluate_flag_details_async(
651668
"Unable to correctly evaluate flag with key: '%s'", flag_key
652669
)
653670

654-
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
671+
error_hooks(flag_type, err, reversed_merged_hooks_and_context, hook_hints)
655672

656673
error_message = getattr(err, "error_message", str(err))
657674
flag_evaluation = FlagEvaluationDetails(
@@ -666,9 +683,8 @@ async def evaluate_flag_details_async(
666683
finally:
667684
after_all_hooks(
668685
flag_type,
669-
hook_context,
670686
flag_evaluation,
671-
reversed_merged_hooks,
687+
reversed_merged_hooks_and_context,
672688
hook_hints,
673689
)
674690

@@ -751,23 +767,26 @@ def evaluate_flag_details(
751767
:return: a FlagEvaluationDetails object with the fully evaluated flag from a
752768
provider
753769
"""
754-
provider, hook_context, hook_hints, merged_hooks, reversed_merged_hooks = (
755-
self._establish_hooks_and_provider(
756-
flag_type,
757-
flag_key,
758-
default_value,
759-
evaluation_context,
760-
flag_evaluation_options,
761-
)
770+
(
771+
provider,
772+
hook_hints,
773+
merged_hooks_and_context,
774+
reversed_merged_hooks_and_context,
775+
merged_eval_context,
776+
) = self._establish_hooks_and_provider(
777+
flag_type,
778+
flag_key,
779+
default_value,
780+
evaluation_context,
781+
flag_evaluation_options,
762782
)
763783

764784
try:
765785
if provider_err := self._assert_provider_status():
766786
error_hooks(
767787
flag_type,
768-
hook_context,
769788
provider_err,
770-
reversed_merged_hooks,
789+
reversed_merged_hooks_and_context,
771790
hook_hints,
772791
)
773792
flag_evaluation = FlagEvaluationDetails(
@@ -781,10 +800,9 @@ def evaluate_flag_details(
781800

782801
merged_context = self._run_before_hooks_and_update_context(
783802
flag_type,
784-
hook_context,
785-
merged_hooks,
803+
merged_hooks_and_context,
786804
hook_hints,
787-
evaluation_context,
805+
merged_eval_context,
788806
)
789807

790808
flag_evaluation = self._create_provider_evaluation(
@@ -796,23 +814,22 @@ def evaluate_flag_details(
796814
)
797815
if err := flag_evaluation.get_exception():
798816
error_hooks(
799-
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
817+
flag_type, err, reversed_merged_hooks_and_context, hook_hints
800818
)
801819
flag_evaluation.value = default_value
802820
return flag_evaluation
803821

804822
after_hooks(
805823
flag_type,
806-
hook_context,
807824
flag_evaluation,
808-
reversed_merged_hooks,
825+
reversed_merged_hooks_and_context,
809826
hook_hints,
810827
)
811828

812829
return flag_evaluation
813830

814831
except OpenFeatureError as err:
815-
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
832+
error_hooks(flag_type, err, reversed_merged_hooks_and_context, hook_hints)
816833

817834
flag_evaluation = FlagEvaluationDetails(
818835
flag_key=flag_key,
@@ -829,7 +846,7 @@ def evaluate_flag_details(
829846
"Unable to correctly evaluate flag with key: '%s'", flag_key
830847
)
831848

832-
error_hooks(flag_type, hook_context, err, reversed_merged_hooks, hook_hints)
849+
error_hooks(flag_type, err, reversed_merged_hooks_and_context, hook_hints)
833850

834851
error_message = getattr(err, "error_message", str(err))
835852
flag_evaluation = FlagEvaluationDetails(
@@ -844,9 +861,8 @@ def evaluate_flag_details(
844861
finally:
845862
after_all_hooks(
846863
flag_type,
847-
hook_context,
848864
flag_evaluation,
849-
reversed_merged_hooks,
865+
reversed_merged_hooks_and_context,
850866
hook_hints,
851867
)
852868

openfeature/hook/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import typing
4-
from collections.abc import Sequence
4+
from collections.abc import MutableMapping, Sequence
55
from datetime import datetime
66
from enum import Enum
77
from typing import TYPE_CHECKING
@@ -16,6 +16,7 @@
1616
__all__ = [
1717
"Hook",
1818
"HookContext",
19+
"HookData",
1920
"HookHints",
2021
"HookType",
2122
"add_hooks",
@@ -26,6 +27,10 @@
2627
_hooks: list[Hook] = []
2728

2829

30+
# https://openfeature.dev/specification/sections/hooks/#requirement-461
31+
HookData = MutableMapping[str, typing.Any]
32+
33+
2934
class HookType(Enum):
3035
BEFORE = "before"
3136
AFTER = "after"
@@ -34,21 +39,23 @@ class HookType(Enum):
3439

3540

3641
class HookContext:
37-
def __init__(
42+
def __init__( # noqa: PLR0913
3843
self,
3944
flag_key: str,
4045
flag_type: FlagType,
4146
default_value: FlagValueType,
4247
evaluation_context: EvaluationContext,
4348
client_metadata: typing.Optional[ClientMetadata] = None,
4449
provider_metadata: typing.Optional[Metadata] = None,
50+
hook_data: typing.Optional[HookData] = None,
4551
):
4652
self.flag_key = flag_key
4753
self.flag_type = flag_type
4854
self.default_value = default_value
4955
self.evaluation_context = evaluation_context
5056
self.client_metadata = client_metadata
5157
self.provider_metadata = provider_metadata
58+
self.hook_data = hook_data or {}
5259

5360
def __setattr__(self, key: str, value: typing.Any) -> None:
5461
if hasattr(self, key) and key in (

0 commit comments

Comments
 (0)