From b9a9f707a1d3d4eaf183865aaa931f83cf43b0e0 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Wed, 24 Apr 2024 13:38:51 -0400 Subject: [PATCH] chore: Add hook support to contract tests --- contract-tests/client_entity.py | 5 +++- contract-tests/hook.py | 45 +++++++++++++++++++++++++++++++++ contract-tests/service.py | 1 + ldclient/client.py | 7 +++-- ldclient/hook.py | 4 +-- 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 contract-tests/hook.py diff --git a/contract-tests/client_entity.py b/contract-tests/client_entity.py index e940b4e..bc119fc 100644 --- a/contract-tests/client_entity.py +++ b/contract-tests/client_entity.py @@ -3,7 +3,7 @@ import os import sys import requests -from typing import Optional +from hook import PostingHook from big_segment_store_fixture import BigSegmentStoreFixture @@ -52,6 +52,9 @@ def __init__(self, tag, config): else: opts["send_events"] = False + if config.get("hooks") is not None: + opts["hooks"] = [PostingHook(h["name"], h["callbackUri"], h.get("data", {}), h.get("errors", {})) for h in config["hooks"]["hooks"]] + if config.get("bigSegments") is not None: big_params = config["bigSegments"] big_config = { diff --git a/contract-tests/hook.py b/contract-tests/hook.py new file mode 100644 index 0000000..ec2708c --- /dev/null +++ b/contract-tests/hook.py @@ -0,0 +1,45 @@ +from ldclient.hook import Hook, EvaluationSeriesContext +from ldclient.evaluation import EvaluationDetail + +from typing import Any, Optional +import requests + + +class PostingHook(Hook): + def __init__(self, name: str, callback: str, data: dict, errors: dict): + self.__name = name + self.__callback = callback + self.__data = data + self.__errors = errors + + def before_evaluation(self, series_context: EvaluationSeriesContext, data: dict) -> Any: + return self.__post("beforeEvaluation", series_context, data, None) + + def after_evaluation(self, series_context: EvaluationSeriesContext, data: Any, detail: EvaluationDetail) -> Any: + return self.__post("afterEvaluation", series_context, data, detail) + + def __post(self, stage: str, series_context: EvaluationSeriesContext, data: Any, detail: Optional[EvaluationDetail]) -> Any: + if stage in self.__errors: + raise Exception(self.__errors[stage]) + + payload = { + 'evaluationSeriesContext': { + 'flagKey': series_context.key, + 'context': series_context.context.to_dict(), + 'defaultValue': series_context.default_value, + 'method': series_context.method, + }, + 'evaluationSeriesData': data, + 'stage': stage, + } + + if detail is not None: + payload['evaluationDetail'] = { + 'value': detail.value, + 'variationIndex': detail.variation_index, + 'reason': detail.reason, + } + + requests.post(self.__callback, json=payload) + + return {**(data or {}), **self.__data.get(stage, {})} diff --git a/contract-tests/service.py b/contract-tests/service.py index 201d5b2..1fa9a97 100644 --- a/contract-tests/service.py +++ b/contract-tests/service.py @@ -74,6 +74,7 @@ def status(): 'polling-gzip', 'inline-context', 'anonymous-redaction', + 'evaluation-hooks' ] } return (json.dumps(body), 200, {'Content-type': 'application/json'}) diff --git a/ldclient/client.py b/ldclient/client.py index 4211366..d7bf1e0 100644 --- a/ldclient/client.py +++ b/ldclient/client.py @@ -2,7 +2,7 @@ This submodule contains the client class that provides most of the SDK functionality. """ -from typing import Optional, Any, Dict, Mapping, Union, Tuple, Callable, List +from typing import Optional, Any, Dict, Mapping, Tuple, Callable, List from .impl import AnyNum @@ -10,7 +10,6 @@ import hmac import threading import traceback -import warnings from ldclient.config import Config from ldclient.context import Context @@ -451,7 +450,7 @@ def evaluate(): tracker = OpTracker(key, flag, context, detail, default_stage) return _EvaluationWithHookResult(evaluation_detail=detail, results={'default_stage': default_stage, 'tracker': tracker}) - hook_result = self.__evaluate_with_hooks(key=key, context=context, default_value=default_stage, method="migration_variation", block=evaluate) + hook_result = self.__evaluate_with_hooks(key=key, context=context, default_value=default_stage.value, method="migration_variation", block=evaluate) return hook_result.results['default_stage'], hook_result.results['tracker'] def _evaluate_internal(self, key: str, context: Context, default: Any, event_factory) -> Tuple[EvaluationDetail, Optional[FeatureFlag]]: @@ -652,7 +651,7 @@ def __execute_after_evaluation(self, hooks: List[Hook], series_context: Evaluati for (hook, data) in reversed(list(zip(hooks, hook_data))) ] - def __try_execute_stage(self, method: str, hook_name: str, block: Callable[[], dict]) -> Optional[dict]: + def __try_execute_stage(self, method: str, hook_name: str, block: Callable[[], Any]) -> Any: try: return block() except BaseException as e: diff --git a/ldclient/hook.py b/ldclient/hook.py index 3f594fc..349d5d1 100644 --- a/ldclient/hook.py +++ b/ldclient/hook.py @@ -48,7 +48,7 @@ def metadata(self) -> Metadata: return Metadata(name='UNDEFINED') @abstractmethod - def before_evaluation(self, series_context: EvaluationSeriesContext, data: dict) -> dict: + def before_evaluation(self, series_context: EvaluationSeriesContext, data: Any) -> Any: """ The before method is called during the execution of a variation method before the flag value has been determined. The method is executed @@ -63,7 +63,7 @@ def before_evaluation(self, series_context: EvaluationSeriesContext, data: dict) return data @abstractmethod - def after_evaluation(self, series_context: EvaluationSeriesContext, data: dict, detail: EvaluationDetail) -> dict: + def after_evaluation(self, series_context: EvaluationSeriesContext, data: Any, detail: EvaluationDetail) -> dict: """ The after method is called during the execution of the variation method after the flag value has been determined. The method is executed