From 5ae8571ccd5f30c0aef87b0bc7f1a08a65254df0 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Wed, 12 Feb 2025 02:21:06 -0500 Subject: [PATCH 01/37] chore: use existing submodule version for e2e tests (#444) * chore: use existing submodule version for e2e tests Signed-off-by: Michael Beemer * reset submoduels Signed-off-by: Michael Beemer --------- Signed-off-by: Michael Beemer --- pyproject.toml | 2 +- spec | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 spec diff --git a/pyproject.toml b/pyproject.toml index 3cb853e0..dc2e0dce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ cov = [ "cov-report", ] e2e = [ - "git submodule add --force https://github.com/open-feature/spec.git spec", + "git submodule update --init --recursive", "cp spec/specification/assets/gherkin/* tests/features/", "behave tests/features/", "rm tests/features/*.feature", diff --git a/spec b/spec new file mode 160000 index 00000000..be56f22a --- /dev/null +++ b/spec @@ -0,0 +1 @@ +Subproject commit be56f22af99191eb36039db7b2fa18f46434a383 From f907855966cf788a3522e7626c76bd050de59a7e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 23:27:56 +0000 Subject: [PATCH 02/37] chore(deps): update spec digest to 54952f3 (#447) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index be56f22a..54952f3b 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit be56f22af99191eb36039db7b2fa18f46434a383 +Subproject commit 54952f3b545a09ce966a4dbb86c9490a1ce3333b From f29c4506a6a13307ba95a9b450a1b19c328975b3 Mon Sep 17 00:00:00 2001 From: Leo <37860104+leohoare@users.noreply.github.com> Date: Fri, 14 Feb 2025 04:06:54 +1100 Subject: [PATCH 03/37] chore: use keyword arguments, validate test (#446) Signed-off-by: leohoare --- openfeature/client.py | 23 ++++++++++------------- tests/test_client.py | 12 ++++++++++-- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/openfeature/client.py b/openfeature/client.py index 7d1f26df..d73a3800 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -701,11 +701,6 @@ async def _create_provider_evaluation_async( default_value: typing.Any, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagEvaluationDetails[typing.Any]: - args = ( - flag_key, - default_value, - evaluation_context, - ) get_details_callables_async: typing.Mapping[ FlagType, GetDetailCallableAsync ] = { @@ -719,7 +714,11 @@ async def _create_provider_evaluation_async( if not get_details_callable: raise GeneralError(error_message="Unknown flag type") - resolution = await get_details_callable(*args) + resolution = await get_details_callable( # type: ignore[call-arg] + flag_key=flag_key, + default_value=default_value, + evaluation_context=evaluation_context, + ) resolution.raise_for_error() # we need to check the get_args to be compatible with union types. @@ -753,12 +752,6 @@ def _create_provider_evaluation( :return: a FlagEvaluationDetails object with the fully evaluated flag from a provider """ - args = ( - flag_key, - default_value, - evaluation_context, - ) - get_details_callables: typing.Mapping[FlagType, GetDetailCallable] = { FlagType.BOOLEAN: provider.resolve_boolean_details, FlagType.INTEGER: provider.resolve_integer_details, @@ -771,7 +764,11 @@ def _create_provider_evaluation( if not get_details_callable: raise GeneralError(error_message="Unknown flag type") - resolution = get_details_callable(*args) + resolution = get_details_callable( # type: ignore[call-arg] + flag_key=flag_key, + default_value=default_value, + evaluation_context=evaluation_context, + ) resolution.raise_for_error() # we need to check the get_args to be compatible with union types. diff --git a/tests/test_client.py b/tests/test_client.py index 5d333993..9264018d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -526,12 +526,20 @@ def test_client_should_merge_contexts(): invocation_context = EvaluationContext( targeting_key="invocation", attributes={"invocation_attr": "invocation_value"} ) - client.get_boolean_details("flag", False, invocation_context) + flag_input = "flag" + flag_default = False + client.get_boolean_details(flag_input, flag_default, invocation_context) # Retrieve the call arguments args, kwargs = provider.resolve_boolean_details.call_args - flag_key, default_value, context = args + flag_key, default_value, context = ( + kwargs["flag_key"], + kwargs["default_value"], + kwargs["evaluation_context"], + ) + assert flag_key == flag_input + assert default_value is flag_default assert context.targeting_key == "invocation" # Last one in the merge chain assert context.attributes["global_attr"] == "global_value" assert context.attributes["transaction_attr"] == "transaction_value" From 31afa6490f7c2fc7a553b69c56840d494a520836 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Thu, 13 Feb 2025 21:52:41 +0100 Subject: [PATCH 04/37] chore: improve resolve details callable type hints (#449) improve resolve details callable type hints Signed-off-by: gruebel --- openfeature/client.py | 69 ++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/openfeature/client.py b/openfeature/client.py index d73a3800..29e3a32a 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -1,5 +1,6 @@ import logging import typing +from collections.abc import Awaitable from dataclasses import dataclass from openfeature import _event_support, api @@ -37,46 +38,6 @@ logger = logging.getLogger("openfeature") -GetDetailCallable = typing.Union[ - typing.Callable[ - [str, bool, typing.Optional[EvaluationContext]], FlagResolutionDetails[bool] - ], - typing.Callable[ - [str, int, typing.Optional[EvaluationContext]], FlagResolutionDetails[int] - ], - typing.Callable[ - [str, float, typing.Optional[EvaluationContext]], FlagResolutionDetails[float] - ], - typing.Callable[ - [str, str, typing.Optional[EvaluationContext]], FlagResolutionDetails[str] - ], - typing.Callable[ - [str, typing.Union[dict, list], typing.Optional[EvaluationContext]], - FlagResolutionDetails[typing.Union[dict, list]], - ], -] -GetDetailCallableAsync = typing.Union[ - typing.Callable[ - [str, bool, typing.Optional[EvaluationContext]], - typing.Awaitable[FlagResolutionDetails[bool]], - ], - typing.Callable[ - [str, int, typing.Optional[EvaluationContext]], - typing.Awaitable[FlagResolutionDetails[int]], - ], - typing.Callable[ - [str, float, typing.Optional[EvaluationContext]], - typing.Awaitable[FlagResolutionDetails[float]], - ], - typing.Callable[ - [str, str, typing.Optional[EvaluationContext]], - typing.Awaitable[FlagResolutionDetails[str]], - ], - typing.Callable[ - [str, typing.Union[dict, list], typing.Optional[EvaluationContext]], - typing.Awaitable[FlagResolutionDetails[typing.Union[dict, list]]], - ], -] TypeMap = dict[ FlagType, typing.Union[ @@ -88,6 +49,26 @@ ], ] +T = typing.TypeVar("T", bool, int, float, str, typing.Union[dict, list]) + + +class ResolveDetailsCallable(typing.Protocol[T]): + def __call__( + self, + flag_key: str, + default_value: T, + evaluation_context: typing.Optional[EvaluationContext], + ) -> FlagResolutionDetails[T]: ... + + +class ResolveDetailsCallableAsync(typing.Protocol[T]): + def __call__( + self, + flag_key: str, + default_value: T, + evaluation_context: typing.Optional[EvaluationContext], + ) -> Awaitable[FlagResolutionDetails[T]]: ... + @dataclass class ClientMetadata: @@ -702,7 +683,7 @@ async def _create_provider_evaluation_async( evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagEvaluationDetails[typing.Any]: get_details_callables_async: typing.Mapping[ - FlagType, GetDetailCallableAsync + FlagType, ResolveDetailsCallableAsync ] = { FlagType.BOOLEAN: provider.resolve_boolean_details_async, FlagType.INTEGER: provider.resolve_integer_details_async, @@ -714,7 +695,7 @@ async def _create_provider_evaluation_async( if not get_details_callable: raise GeneralError(error_message="Unknown flag type") - resolution = await get_details_callable( # type: ignore[call-arg] + resolution = await get_details_callable( flag_key=flag_key, default_value=default_value, evaluation_context=evaluation_context, @@ -752,7 +733,7 @@ def _create_provider_evaluation( :return: a FlagEvaluationDetails object with the fully evaluated flag from a provider """ - get_details_callables: typing.Mapping[FlagType, GetDetailCallable] = { + get_details_callables: typing.Mapping[FlagType, ResolveDetailsCallable] = { FlagType.BOOLEAN: provider.resolve_boolean_details, FlagType.INTEGER: provider.resolve_integer_details, FlagType.FLOAT: provider.resolve_float_details, @@ -764,7 +745,7 @@ def _create_provider_evaluation( if not get_details_callable: raise GeneralError(error_message="Unknown flag type") - resolution = get_details_callable( # type: ignore[call-arg] + resolution = get_details_callable( flag_key=flag_key, default_value=default_value, evaluation_context=evaluation_context, From 95b33b39e6ef472264002322162e83665054d71b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 02:38:44 +0000 Subject: [PATCH 05/37] chore(deps): update spec digest to a69f748 (#452) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 54952f3b..a69f748d 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 54952f3b545a09ce966a4dbb86c9490a1ce3333b +Subproject commit a69f748db2edfec7015ca6bb702ca22fd8c5ef30 From 11987280ba53ba087b1792316acc920a81434630 Mon Sep 17 00:00:00 2001 From: Michael Beemer Date: Tue, 18 Feb 2025 08:29:49 -0500 Subject: [PATCH 06/37] docs: fix linting issue on the readme Signed-off-by: Michael Beemer --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c018e180..a9f64b0c 100644 --- a/README.md +++ b/README.md @@ -411,8 +411,9 @@ class MyProvider(AbstractProvider): Providers can also be extended to support async functionality. To support add asynchronous calls to a provider: -* Implement the `AbstractProvider` as shown above. -* Define asynchronous calls for each data type. + +- Implement the `AbstractProvider` as shown above. +- Define asynchronous calls for each data type. ```python class MyProvider(AbstractProvider): From 088409ea5cdefef33f28fc4f45026fabac52377a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Tue, 18 Feb 2025 14:45:12 +0100 Subject: [PATCH 07/37] fix: add passthrough init to abstract provider (#450) Signed-off-by: gruebel --- openfeature/provider/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openfeature/provider/__init__.py b/openfeature/provider/__init__.py index 5b9ffd09..17300c0a 100644 --- a/openfeature/provider/__init__.py +++ b/openfeature/provider/__init__.py @@ -112,6 +112,10 @@ async def resolve_object_details_async( class AbstractProvider(FeatureProvider): + def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: + # this makes sure to invoke the parent of `FeatureProvider` -> `object` + super(FeatureProvider, self).__init__(*args, **kwargs) + def attach( self, on_emit: typing.Callable[ From a5cb27b67839d60ea631001759478b2e74b75f28 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:04:06 +0000 Subject: [PATCH 08/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.7 (#453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 197d17b5..ac0dbec3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.6 + rev: v0.9.7 hooks: - id: ruff args: [--fix] From 613388ddde33b6ce5ff3a39760970297dfa83255 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:45:32 +0000 Subject: [PATCH 09/37] chore(deps): update github/codeql-action digest to b56ba49 (#454) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7023c114..fa95f311 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,10 +80,10 @@ jobs: python-version: "3.13" - name: Initialize CodeQL - uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 with: languages: python config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3 + uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 From 2d1ba85c93cdd954f539d2872783b21683bd8b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Sat, 22 Feb 2025 16:50:27 +0100 Subject: [PATCH 10/37] feat: add OTel utility function (#451) add OTel utility function Signed-off-by: gruebel Co-authored-by: Michael Beemer --- openfeature/exception.py | 5 +- openfeature/flag_evaluation.py | 3 +- openfeature/telemetry/__init__.py | 75 +++++++++++++++++ openfeature/telemetry/attributes.py | 19 +++++ openfeature/telemetry/body.py | 11 +++ openfeature/telemetry/metadata.py | 13 +++ tests/telemetry/__init__.py | 0 tests/telemetry/test_evaluation_event.py | 101 +++++++++++++++++++++++ 8 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 openfeature/telemetry/__init__.py create mode 100644 openfeature/telemetry/attributes.py create mode 100644 openfeature/telemetry/body.py create mode 100644 openfeature/telemetry/metadata.py create mode 100644 tests/telemetry/__init__.py create mode 100644 tests/telemetry/test_evaluation_event.py diff --git a/openfeature/exception.py b/openfeature/exception.py index 0576ec17..0912ef5f 100644 --- a/openfeature/exception.py +++ b/openfeature/exception.py @@ -2,7 +2,8 @@ import typing from collections.abc import Mapping -from enum import Enum + +from openfeature._backports.strenum import StrEnum __all__ = [ "ErrorCode", @@ -163,7 +164,7 @@ def __init__(self, error_message: typing.Optional[str]): super().__init__(ErrorCode.INVALID_CONTEXT, error_message) -class ErrorCode(Enum): +class ErrorCode(StrEnum): PROVIDER_NOT_READY = "PROVIDER_NOT_READY" PROVIDER_FATAL = "PROVIDER_FATAL" FLAG_NOT_FOUND = "FLAG_NOT_FOUND" diff --git a/openfeature/flag_evaluation.py b/openfeature/flag_evaluation.py index 5cab623c..c522eecd 100644 --- a/openfeature/flag_evaluation.py +++ b/openfeature/flag_evaluation.py @@ -34,8 +34,9 @@ class Reason(StrEnum): DEFAULT = "DEFAULT" DISABLED = "DISABLED" ERROR = "ERROR" - STATIC = "STATIC" SPLIT = "SPLIT" + STATIC = "STATIC" + STALE = "STALE" TARGETING_MATCH = "TARGETING_MATCH" UNKNOWN = "UNKNOWN" diff --git a/openfeature/telemetry/__init__.py b/openfeature/telemetry/__init__.py new file mode 100644 index 00000000..a4b82ab6 --- /dev/null +++ b/openfeature/telemetry/__init__.py @@ -0,0 +1,75 @@ +import typing +from collections.abc import Mapping +from dataclasses import dataclass + +from openfeature.exception import ErrorCode +from openfeature.flag_evaluation import FlagEvaluationDetails, Reason +from openfeature.hook import HookContext +from openfeature.telemetry.attributes import TelemetryAttribute +from openfeature.telemetry.body import TelemetryBodyField +from openfeature.telemetry.metadata import TelemetryFlagMetadata + +__all__ = [ + "EvaluationEvent", + "TelemetryAttribute", + "TelemetryBodyField", + "TelemetryFlagMetadata", + "create_evaluation_event", +] + +FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation" + +T_co = typing.TypeVar("T_co", covariant=True) + + +@dataclass +class EvaluationEvent(typing.Generic[T_co]): + name: str + attributes: Mapping[TelemetryAttribute, typing.Union[str, T_co]] + body: Mapping[TelemetryBodyField, T_co] + + +def create_evaluation_event( + hook_context: HookContext, details: FlagEvaluationDetails[T_co] +) -> EvaluationEvent[T_co]: + attributes = { + TelemetryAttribute.KEY: details.flag_key, + TelemetryAttribute.EVALUATION_REASON: ( + details.reason or Reason.UNKNOWN + ).lower(), + } + body = {} + + if variant := details.variant: + attributes[TelemetryAttribute.VARIANT] = variant + else: + body[TelemetryBodyField.VALUE] = details.value + + context_id = details.flag_metadata.get( + TelemetryFlagMetadata.CONTEXT_ID, hook_context.evaluation_context.targeting_key + ) + if context_id: + attributes[TelemetryAttribute.CONTEXT_ID] = context_id + + if set_id := details.flag_metadata.get(TelemetryFlagMetadata.FLAG_SET_ID): + attributes[TelemetryAttribute.SET_ID] = set_id + + if version := details.flag_metadata.get(TelemetryFlagMetadata.VERSION): + attributes[TelemetryAttribute.VERSION] = version + + if metadata := hook_context.provider_metadata: + attributes[TelemetryAttribute.PROVIDER_NAME] = metadata.name + + if details.reason == Reason.ERROR: + attributes[TelemetryAttribute.ERROR_TYPE] = ( + details.error_code or ErrorCode.GENERAL + ).lower() + + if err_msg := details.error_message: + attributes[TelemetryAttribute.EVALUATION_ERROR_MESSAGE] = err_msg + + return EvaluationEvent( + name=FLAG_EVALUATION_EVENT_NAME, + attributes=attributes, + body=body, + ) diff --git a/openfeature/telemetry/attributes.py b/openfeature/telemetry/attributes.py new file mode 100644 index 00000000..e232cee6 --- /dev/null +++ b/openfeature/telemetry/attributes.py @@ -0,0 +1,19 @@ +from openfeature._backports.strenum import StrEnum + + +class TelemetryAttribute(StrEnum): + """ + The attributes of an OpenTelemetry compliant event for flag evaluation. + + See: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ + """ + + CONTEXT_ID = "feature_flag.context.id" + ERROR_TYPE = "error.type" + EVALUATION_ERROR_MESSAGE = "feature_flag.evaluation.error.message" + EVALUATION_REASON = "feature_flag.evaluation.reason" + KEY = "feature_flag.key" + PROVIDER_NAME = "feature_flag.provider_name" + SET_ID = "feature_flag.set.id" + VARIANT = "feature_flag.variant" + VERSION = "feature_flag.version" diff --git a/openfeature/telemetry/body.py b/openfeature/telemetry/body.py new file mode 100644 index 00000000..7b47bbff --- /dev/null +++ b/openfeature/telemetry/body.py @@ -0,0 +1,11 @@ +from openfeature._backports.strenum import StrEnum + + +class TelemetryBodyField(StrEnum): + """ + OpenTelemetry event body fields. + + See: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ + """ + + VALUE = "value" diff --git a/openfeature/telemetry/metadata.py b/openfeature/telemetry/metadata.py new file mode 100644 index 00000000..5b7b1085 --- /dev/null +++ b/openfeature/telemetry/metadata.py @@ -0,0 +1,13 @@ +from openfeature._backports.strenum import StrEnum + + +class TelemetryFlagMetadata(StrEnum): + """ + Well-known flag metadata attributes for telemetry events. + + See: https://openfeature.dev/specification/appendix-d/#flag-metadata + """ + + CONTEXT_ID = "contextId" + FLAG_SET_ID = "flagSetId" + VERSION = "version" diff --git a/tests/telemetry/__init__.py b/tests/telemetry/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/telemetry/test_evaluation_event.py b/tests/telemetry/test_evaluation_event.py new file mode 100644 index 00000000..3def8655 --- /dev/null +++ b/tests/telemetry/test_evaluation_event.py @@ -0,0 +1,101 @@ +from openfeature.evaluation_context import EvaluationContext +from openfeature.exception import ErrorCode +from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType, Reason +from openfeature.hook import HookContext +from openfeature.provider import Metadata +from openfeature.telemetry import ( + TelemetryAttribute, + TelemetryBodyField, + TelemetryFlagMetadata, + create_evaluation_event, +) + + +def test_create_evaluation_event(): + # given + hook_context = HookContext( + flag_key="flag_key", + flag_type=FlagType.BOOLEAN, + default_value=True, + evaluation_context=EvaluationContext(), + provider_metadata=Metadata(name="test_provider"), + ) + details = FlagEvaluationDetails( + flag_key=hook_context.flag_key, + value=False, + reason=Reason.CACHED, + ) + + # when + event = create_evaluation_event(hook_context=hook_context, details=details) + + # then + assert event.name == "feature_flag.evaluation" + assert event.attributes[TelemetryAttribute.KEY] == "flag_key" + assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "cached" + assert event.attributes[TelemetryAttribute.PROVIDER_NAME] == "test_provider" + assert event.body[TelemetryBodyField.VALUE] is False + + +def test_create_evaluation_event_with_variant(): + # given + hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext()) + details = FlagEvaluationDetails( + flag_key=hook_context.flag_key, + value=True, + variant="true", + ) + + # when + event = create_evaluation_event(hook_context=hook_context, details=details) + + # then + assert event.name == "feature_flag.evaluation" + assert event.attributes[TelemetryAttribute.KEY] == "flag_key" + assert event.attributes[TelemetryAttribute.VARIANT] == "true" + assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "unknown" + + +def test_create_evaluation_event_with_metadata(): + # given + hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext()) + details = FlagEvaluationDetails( + flag_key=hook_context.flag_key, + value=False, + flag_metadata={ + TelemetryFlagMetadata.CONTEXT_ID: "5157782b-2203-4c80-a857-dbbd5e7761db", + TelemetryFlagMetadata.FLAG_SET_ID: "proj-1", + TelemetryFlagMetadata.VERSION: "v1", + }, + ) + + # when + event = create_evaluation_event(hook_context=hook_context, details=details) + + # then + assert ( + event.attributes[TelemetryAttribute.CONTEXT_ID] + == "5157782b-2203-4c80-a857-dbbd5e7761db" + ) + assert event.attributes[TelemetryAttribute.SET_ID] == "proj-1" + assert event.attributes[TelemetryAttribute.VERSION] == "v1" + + +def test_create_evaluation_event_with_error(): + # given + hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext()) + details = FlagEvaluationDetails( + flag_key=hook_context.flag_key, + value=False, + reason=Reason.ERROR, + error_code=ErrorCode.FLAG_NOT_FOUND, + error_message="flag error", + ) + + # when + event = create_evaluation_event(hook_context=hook_context, details=details) + + # then + assert event.attributes[TelemetryAttribute.EVALUATION_REASON] == "error" + assert event.attributes[TelemetryAttribute.ERROR_TYPE] == "flag_not_found" + assert event.attributes[TelemetryAttribute.EVALUATION_ERROR_MESSAGE] == "flag error" From fe99f08e9465e8d35dd2b187d8ac01eae98432b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:04:34 +0000 Subject: [PATCH 11/37] chore(deps): update spec digest to 0cd553d (#455) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index a69f748d..0cd553d8 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit a69f748db2edfec7015ca6bb702ca22fd8c5ef30 +Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88 From a666227f55b14d4d2b6e43b6487ac643b6893739 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 03:09:18 +0000 Subject: [PATCH 12/37] chore(deps): update codecov/codecov-action action to v5.4.0 (#456) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fa95f311..099822b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: - if: matrix.python-version == '3.13' name: Upload coverage to Codecov - uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # v5.3.1 + uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0 with: flags: unittests # optional name: coverage # optional From 0c1a388ca121e232f5c36b4b7a550d541ae34e5b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:41:09 +0000 Subject: [PATCH 13/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.8 (#457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ac0dbec3..5b35fe21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.7 + rev: v0.9.8 hooks: - id: ruff args: [--fix] From 9ce51ebff5a896b818e241fd8e3c2dea2fee610c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:08:52 +0000 Subject: [PATCH 14/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.9 (#458) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b35fe21..f2b42b08 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.8 + rev: v0.9.9 hooks: - id: ruff args: [--fix] From 40cbd82dda20604a7a7be00e6913710d4a1ab56f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 06:15:26 +0000 Subject: [PATCH 15/37] chore(deps): update spec digest to 25c57ee (#459) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 0cd553d8..25c57ee7 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88 +Subproject commit 25c57ee7b6fe5808d1aa9d1eee4548240ed42142 From 547781fbd82d1e2ee8a17988d11da3875d6a73dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:44:45 +0000 Subject: [PATCH 16/37] chore(deps): update spec digest to 09aef37 (#460) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 25c57ee7..09aef370 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 25c57ee7b6fe5808d1aa9d1eee4548240ed42142 +Subproject commit 09aef370639ebe00d0e0b5ddfe003a38655e3b6d From 9057c6b3df6ca5dc9e429db231eb4427cce031ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 16:00:57 +0000 Subject: [PATCH 17/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.9.10 (#461) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2b42b08..7366235e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.9.10 hooks: - id: ruff args: [--fix] From 0396592586b6f721754c18a46b6d2fee3c2f80e8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:59:00 +0000 Subject: [PATCH 18/37] chore(deps): update github/codeql-action digest to 6bb031a (#462) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 099822b0..5499b2aa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,10 +80,10 @@ jobs: python-version: "3.13" - name: Initialize CodeQL - uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 with: languages: python config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 From 5fede4d4f0cb6e39f84e85c72c9a2dd13434bc78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:36:48 +0000 Subject: [PATCH 19/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.10.0 (#463) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7366235e..022ae99a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.10 + rev: v0.10.0 hooks: - id: ruff args: [--fix] From d15388b542798f7703578927dc5013863a83efa1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:10:18 +0000 Subject: [PATCH 20/37] chore(deps): update spec digest to aad6193 (#464) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 09aef370..aad6193d 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 09aef370639ebe00d0e0b5ddfe003a38655e3b6d +Subproject commit aad6193d77eca269bc5a8dc0a0b626dbf98d924b From d1eb3a08a8da75022788cc4b9ea7b7d95aec4e69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:11:34 +0000 Subject: [PATCH 21/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 (#465) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 022ae99a..f66da009 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.10.0 + rev: v0.11.0 hooks: - id: ruff args: [--fix] From d69b7594a956a49385ef3030c212624d628aec74 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:08:59 +0000 Subject: [PATCH 22/37] chore(deps): update github/codeql-action digest to 5f8171a (#467) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5499b2aa..7b9d24cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,10 +80,10 @@ jobs: python-version: "3.13" - name: Initialize CodeQL - uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3 with: languages: python config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3 From c07d3d64677c2ce475b098580b5eba1dd7f95a2e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:57:03 +0000 Subject: [PATCH 23/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.1 (#468) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f66da009..4b5ff315 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.0 + rev: v0.11.1 hooks: - id: ruff args: [--fix] From 95e87c71fc835cde7f7528e974509438ab8f2dc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:16:51 +0000 Subject: [PATCH 24/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.2 (#469) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b5ff315..cb379dcb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.1 + rev: v0.11.2 hooks: - id: ruff args: [--fix] From 4eeab3b6914bd947a63f8d3c5bb89b85b7c2ced1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:48:43 +0000 Subject: [PATCH 25/37] chore(deps): update github/codeql-action digest to 1b549b9 (#470) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b9d24cb..39cd1f1a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,10 +80,10 @@ jobs: python-version: "3.13" - name: Initialize CodeQL - uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3 + uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 with: languages: python config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3 + uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 From 9ced6bf2d1c7e3b0f01d062564ee63e49254af00 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:50:16 +0000 Subject: [PATCH 26/37] chore(deps): update spec digest to 130df3e (#471) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index aad6193d..130df3eb 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit aad6193d77eca269bc5a8dc0a0b626dbf98d924b +Subproject commit 130df3eb61f1b8d1a121d54f33563ce0ec27c1b6 From 490cd068533bb5ad702adf71915b6e0ac49706d8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 02:34:26 +0000 Subject: [PATCH 27/37] chore(deps): update spec digest to 27e4461 (#472) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 130df3eb..27e4461b 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 130df3eb61f1b8d1a121d54f33563ce0ec27c1b6 +Subproject commit 27e4461b452429ec64bcb89af0301f7664cb702a From a1359112e9c1d740bcca501cbb5aadd9da3602b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 06:01:28 +0000 Subject: [PATCH 28/37] chore(deps): update actions/setup-python digest to 8d9ed9a (#473) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39cd1f1a..c7dfd200 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: submodules: recursive - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: ${{ matrix.python-version }} cache: "pip" @@ -59,7 +59,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 + - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: "3.13" cache: "pip" @@ -75,7 +75,7 @@ jobs: security-events: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 + - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: "3.13" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d6ac2cb..2ca616b9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,7 +44,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5 + - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: '3.13' From 2be2c06569d89309a70793bb14a82be91d2ccf20 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Apr 2025 16:18:36 +0000 Subject: [PATCH 29/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.3 (#475) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb379dcb..1c321a0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.11.3 hooks: - id: ruff args: [--fix] From 0ebec538db4d1180bad05e89bb62db23ca606a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Mon, 7 Apr 2025 15:21:55 +0200 Subject: [PATCH 30/37] chore: revert spec to commit 0cd553d (#479) Signed-off-by: gruebel --- spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec b/spec index 27e4461b..0cd553d8 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit 27e4461b452429ec64bcb89af0301f7664cb702a +Subproject commit 0cd553d85f03b0b7ab983a988ce1720a3190bc88 From 1ae9fc2361f1671cee8c794f02c01eb6ca0b77a6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 13:25:19 +0000 Subject: [PATCH 31/37] chore(deps): update github/codeql-action digest to fc7e4a0 (#481) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c7dfd200..64667caf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,10 +80,10 @@ jobs: python-version: "3.13" - name: Initialize CodeQL - uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/init@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3 with: languages: python config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3 + uses: github/codeql-action/analyze@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3 From 7a30ef914b3180fc72be9a1d2072a8a288e8b54d Mon Sep 17 00:00:00 2001 From: Simon Schrottner Date: Mon, 7 Apr 2025 15:29:13 +0100 Subject: [PATCH 32/37] chore: add codeowner file to be consistent with the rest of openfeature (#477) All the other sdks use codeowner files to ensure proper approvals, we should also utilize this within python-sdk Signed-off-by: Simon Schrottner Co-authored-by: Michael Beemer --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..ddabdfe4 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @open-feature/sdk-python-maintainers @open-feature/maintainers From 8acc88328836c70f168ca87b71f4c49a6dba9381 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:00:06 +0000 Subject: [PATCH 33/37] chore(deps): update pre-commit hook astral-sh/ruff-pre-commit to v0.11.4 (#476) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c321a0c..5962fb3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ default_stages: [pre-commit] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.3 + rev: v0.11.4 hooks: - id: ruff args: [--fix] From 5a2825b00db0653c6d0496ec7f4703f9125cbed7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 02:13:02 +0000 Subject: [PATCH 34/37] chore(deps): update github/codeql-action digest to 45775bd (#483) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64667caf..990b3802 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,10 +80,10 @@ jobs: python-version: "3.13" - name: Initialize CodeQL - uses: github/codeql-action/init@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3 + uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3 with: languages: python config-file: ./.github/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fc7e4a0fa01c3cca5fd6a1fddec5c0740c977aa2 # v3 + uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3 From e61b69bb5079547c62a3ad51499326057db69e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Tue, 8 Apr 2025 19:54:41 +0200 Subject: [PATCH 35/37] refactor: replace exception raising with error flag resolution (#474) * replace exception raising with error flag resolution Signed-off-by: gruebel * revert spec to commit 0cd553d Signed-off-by: gruebel --------- Signed-off-by: gruebel --- openfeature/client.py | 120 +++++++++++++++------ openfeature/flag_evaluation.py | 18 +++- openfeature/provider/in_memory_provider.py | 33 +++--- tests/provider/test_in_memory_provider.py | 17 ++- tests/test_client.py | 24 +++-- 5 files changed, 155 insertions(+), 57 deletions(-) diff --git a/openfeature/client.py b/openfeature/client.py index 29e3a32a..f852a10b 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -446,12 +446,12 @@ def _establish_hooks_and_provider( def _assert_provider_status( self, - ) -> None: + ) -> typing.Optional[OpenFeatureError]: status = self.get_provider_status() if status == ProviderStatus.NOT_READY: - raise ProviderNotReadyError() + return ProviderNotReadyError() if status == ProviderStatus.FATAL: - raise ProviderFatalError() + return ProviderFatalError() return None def _before_hooks_and_merge_context( @@ -511,7 +511,22 @@ async def evaluate_flag_details_async( ) try: - self._assert_provider_status() + if provider_err := self._assert_provider_status(): + error_hooks( + flag_type, + hook_context, + provider_err, + reversed_merged_hooks, + hook_hints, + ) + flag_evaluation = FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.ERROR, + error_code=provider_err.error_code, + error_message=provider_err.error_message, + ) + return flag_evaluation merged_context = self._before_hooks_and_merge_context( flag_type, @@ -528,6 +543,11 @@ async def evaluate_flag_details_async( default_value, merged_context, ) + if err := flag_evaluation.get_exception(): + error_hooks( + flag_type, hook_context, err, reversed_merged_hooks, hook_hints + ) + return flag_evaluation after_hooks( flag_type, @@ -607,7 +627,22 @@ def evaluate_flag_details( ) try: - self._assert_provider_status() + if provider_err := self._assert_provider_status(): + error_hooks( + flag_type, + hook_context, + provider_err, + reversed_merged_hooks, + hook_hints, + ) + flag_evaluation = FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.ERROR, + error_code=provider_err.error_code, + error_message=provider_err.error_message, + ) + return flag_evaluation merged_context = self._before_hooks_and_merge_context( flag_type, @@ -624,6 +659,12 @@ def evaluate_flag_details( default_value, merged_context, ) + if err := flag_evaluation.get_exception(): + error_hooks( + flag_type, hook_context, err, reversed_merged_hooks, hook_hints + ) + flag_evaluation.value = default_value + return flag_evaluation after_hooks( flag_type, @@ -693,27 +734,33 @@ async def _create_provider_evaluation_async( } get_details_callable = get_details_callables_async.get(flag_type) if not get_details_callable: - raise GeneralError(error_message="Unknown flag type") + return FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.ERROR, + error_code=ErrorCode.GENERAL, + error_message="Unknown flag type", + ) resolution = await get_details_callable( flag_key=flag_key, default_value=default_value, evaluation_context=evaluation_context, ) - resolution.raise_for_error() + if resolution.error_code: + return resolution.to_flag_evaluation_details(flag_key) # we need to check the get_args to be compatible with union types. - _typecheck_flag_value(resolution.value, flag_type) + if err := _typecheck_flag_value(value=resolution.value, flag_type=flag_type): + return FlagEvaluationDetails( + flag_key=flag_key, + value=resolution.value, + reason=Reason.ERROR, + error_code=err.error_code, + error_message=err.error_message, + ) - return FlagEvaluationDetails( - flag_key=flag_key, - value=resolution.value, - variant=resolution.variant, - flag_metadata=resolution.flag_metadata or {}, - reason=resolution.reason, - error_code=resolution.error_code, - error_message=resolution.error_message, - ) + return resolution.to_flag_evaluation_details(flag_key) def _create_provider_evaluation( self, @@ -743,27 +790,33 @@ def _create_provider_evaluation( get_details_callable = get_details_callables.get(flag_type) if not get_details_callable: - raise GeneralError(error_message="Unknown flag type") + return FlagEvaluationDetails( + flag_key=flag_key, + value=default_value, + reason=Reason.ERROR, + error_code=ErrorCode.GENERAL, + error_message="Unknown flag type", + ) resolution = get_details_callable( flag_key=flag_key, default_value=default_value, evaluation_context=evaluation_context, ) - resolution.raise_for_error() + if resolution.error_code: + return resolution.to_flag_evaluation_details(flag_key) # we need to check the get_args to be compatible with union types. - _typecheck_flag_value(resolution.value, flag_type) + if err := _typecheck_flag_value(value=resolution.value, flag_type=flag_type): + return FlagEvaluationDetails( + flag_key=flag_key, + value=resolution.value, + reason=Reason.ERROR, + error_code=err.error_code, + error_message=err.error_message, + ) - return FlagEvaluationDetails( - flag_key=flag_key, - value=resolution.value, - variant=resolution.variant, - flag_metadata=resolution.flag_metadata or {}, - reason=resolution.reason, - error_code=resolution.error_code, - error_message=resolution.error_message, - ) + return resolution.to_flag_evaluation_details(flag_key) def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None: _event_support.add_client_handler(self, event, handler) @@ -772,7 +825,9 @@ def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None: _event_support.remove_client_handler(self, event, handler) -def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None: +def _typecheck_flag_value( + value: typing.Any, flag_type: FlagType +) -> typing.Optional[OpenFeatureError]: type_map: TypeMap = { FlagType.BOOLEAN: bool, FlagType.STRING: str, @@ -782,6 +837,7 @@ def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None: } _type = type_map.get(flag_type) if not _type: - raise GeneralError(error_message="Unknown flag type") + return GeneralError(error_message="Unknown flag type") if not isinstance(value, _type): - raise TypeMismatchError(f"Expected type {_type} but got {type(value)}") + return TypeMismatchError(f"Expected type {_type} but got {type(value)}") + return None diff --git a/openfeature/flag_evaluation.py b/openfeature/flag_evaluation.py index c522eecd..c26ea485 100644 --- a/openfeature/flag_evaluation.py +++ b/openfeature/flag_evaluation.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from openfeature._backports.strenum import StrEnum -from openfeature.exception import ErrorCode +from openfeature.exception import ErrorCode, OpenFeatureError if typing.TYPE_CHECKING: # pragma: no cover # resolves a circular dependency in type annotations @@ -56,6 +56,11 @@ class FlagEvaluationDetails(typing.Generic[T_co]): error_code: typing.Optional[ErrorCode] = None error_message: typing.Optional[str] = None + def get_exception(self) -> typing.Optional[OpenFeatureError]: + if self.error_code: + return ErrorCode.to_exception(self.error_code, self.error_message or "") + return None + @dataclass class FlagEvaluationOptions: @@ -79,3 +84,14 @@ def raise_for_error(self) -> None: if self.error_code: raise ErrorCode.to_exception(self.error_code, self.error_message or "") return None + + def to_flag_evaluation_details(self, flag_key: str) -> FlagEvaluationDetails[U_co]: + return FlagEvaluationDetails( + flag_key=flag_key, + value=self.value, + variant=self.variant, + flag_metadata=self.flag_metadata, + reason=self.reason, + error_code=self.error_code, + error_message=self.error_message, + ) diff --git a/openfeature/provider/in_memory_provider.py b/openfeature/provider/in_memory_provider.py index 3bd3fa1b..13481861 100644 --- a/openfeature/provider/in_memory_provider.py +++ b/openfeature/provider/in_memory_provider.py @@ -3,7 +3,7 @@ from openfeature._backports.strenum import StrEnum from openfeature.evaluation_context import EvaluationContext -from openfeature.exception import FlagNotFoundError +from openfeature.exception import ErrorCode from openfeature.flag_evaluation import FlagMetadata, FlagResolutionDetails, Reason from openfeature.hook import Hook from openfeature.provider import AbstractProvider, Metadata @@ -74,7 +74,7 @@ def resolve_boolean_details( default_value: bool, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[bool]: - return self._resolve(flag_key, evaluation_context) + return self._resolve(flag_key, default_value, evaluation_context) async def resolve_boolean_details_async( self, @@ -82,7 +82,7 @@ async def resolve_boolean_details_async( default_value: bool, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[bool]: - return await self._resolve_async(flag_key, evaluation_context) + return await self._resolve_async(flag_key, default_value, evaluation_context) def resolve_string_details( self, @@ -90,7 +90,7 @@ def resolve_string_details( default_value: str, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[str]: - return self._resolve(flag_key, evaluation_context) + return self._resolve(flag_key, default_value, evaluation_context) async def resolve_string_details_async( self, @@ -98,7 +98,7 @@ async def resolve_string_details_async( default_value: str, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[str]: - return await self._resolve_async(flag_key, evaluation_context) + return await self._resolve_async(flag_key, default_value, evaluation_context) def resolve_integer_details( self, @@ -106,7 +106,7 @@ def resolve_integer_details( default_value: int, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[int]: - return self._resolve(flag_key, evaluation_context) + return self._resolve(flag_key, default_value, evaluation_context) async def resolve_integer_details_async( self, @@ -114,7 +114,7 @@ async def resolve_integer_details_async( default_value: int, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[int]: - return await self._resolve_async(flag_key, evaluation_context) + return await self._resolve_async(flag_key, default_value, evaluation_context) def resolve_float_details( self, @@ -122,7 +122,7 @@ def resolve_float_details( default_value: float, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[float]: - return self._resolve(flag_key, evaluation_context) + return self._resolve(flag_key, default_value, evaluation_context) async def resolve_float_details_async( self, @@ -130,7 +130,7 @@ async def resolve_float_details_async( default_value: float, evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[float]: - return await self._resolve_async(flag_key, evaluation_context) + return await self._resolve_async(flag_key, default_value, evaluation_context) def resolve_object_details( self, @@ -138,7 +138,7 @@ def resolve_object_details( default_value: typing.Union[dict, list], evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[typing.Union[dict, list]]: - return self._resolve(flag_key, evaluation_context) + return self._resolve(flag_key, default_value, evaluation_context) async def resolve_object_details_async( self, @@ -146,21 +146,28 @@ async def resolve_object_details_async( default_value: typing.Union[dict, list], evaluation_context: typing.Optional[EvaluationContext] = None, ) -> FlagResolutionDetails[typing.Union[dict, list]]: - return await self._resolve_async(flag_key, evaluation_context) + return await self._resolve_async(flag_key, default_value, evaluation_context) def _resolve( self, flag_key: str, + default_value: V, evaluation_context: typing.Optional[EvaluationContext], ) -> FlagResolutionDetails[V]: flag = self._flags.get(flag_key) if flag is None: - raise FlagNotFoundError(f"Flag '{flag_key}' not found") + return FlagResolutionDetails( + value=default_value, + reason=Reason.ERROR, + error_code=ErrorCode.FLAG_NOT_FOUND, + error_message=f"Flag '{flag_key}' not found", + ) return flag.resolve(evaluation_context) async def _resolve_async( self, flag_key: str, + default_value: V, evaluation_context: typing.Optional[EvaluationContext], ) -> FlagResolutionDetails[V]: - return self._resolve(flag_key, evaluation_context) + return self._resolve(flag_key, default_value, evaluation_context) diff --git a/tests/provider/test_in_memory_provider.py b/tests/provider/test_in_memory_provider.py index cdcea7bf..2f17a898 100644 --- a/tests/provider/test_in_memory_provider.py +++ b/tests/provider/test_in_memory_provider.py @@ -2,7 +2,7 @@ import pytest -from openfeature.exception import FlagNotFoundError +from openfeature.exception import ErrorCode from openfeature.flag_evaluation import FlagResolutionDetails, Reason from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider @@ -22,11 +22,18 @@ async def test_should_handle_unknown_flags_correctly(): # Given provider = InMemoryProvider({}) # When - with pytest.raises(FlagNotFoundError): - provider.resolve_boolean_details(flag_key="Key", default_value=True) - with pytest.raises(FlagNotFoundError): - await provider.resolve_integer_details_async(flag_key="Key", default_value=1) + flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=True) + flag_async = await provider.resolve_boolean_details_async( + flag_key="Key", default_value=True + ) # Then + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value is True + assert flag.reason == Reason.ERROR + assert flag.error_code == ErrorCode.FLAG_NOT_FOUND + assert flag.error_message == "Flag 'Key' not found" @pytest.mark.asyncio diff --git a/tests/test_client.py b/tests/test_client.py index 9264018d..76b9dde9 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -8,11 +8,11 @@ from openfeature import api from openfeature.api import add_hooks, clear_hooks, get_client, set_provider -from openfeature.client import GeneralError, OpenFeatureClient, _typecheck_flag_value +from openfeature.client import OpenFeatureClient, _typecheck_flag_value from openfeature.evaluation_context import EvaluationContext from openfeature.event import EventDetails, ProviderEvent, ProviderEventDetails from openfeature.exception import ErrorCode, OpenFeatureError -from openfeature.flag_evaluation import FlagResolutionDetails, Reason +from openfeature.flag_evaluation import FlagResolutionDetails, FlagType, Reason from openfeature.hook import Hook from openfeature.provider import FeatureProvider, ProviderStatus from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider @@ -364,15 +364,27 @@ async def test_client_type_mismatch_exceptions(): @pytest.mark.asyncio -async def test_client_general_exception(): +async def test_typecheck_flag_value_general_error(): # Given flag_value = "A" flag_type = None # When - with pytest.raises(GeneralError) as e: - flag_type = _typecheck_flag_value(flag_value, flag_type) + err = _typecheck_flag_value(value=flag_value, flag_type=flag_type) # Then - assert e.value.error_message == "Unknown flag type" + assert err.error_code == ErrorCode.GENERAL + assert err.error_message == "Unknown flag type" + + +@pytest.mark.asyncio +async def test_typecheck_flag_value_type_mismatch_error(): + # Given + flag_value = "A" + flag_type = FlagType.BOOLEAN + # When + err = _typecheck_flag_value(value=flag_value, flag_type=flag_type) + # Then + assert err.error_code == ErrorCode.TYPE_MISMATCH + assert err.error_message == "Expected type but got " def test_provider_events(): From 3636a0d75f69712844a768cbc6c2f80fdcf6eb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Gr=C3=BCbel?= Date: Wed, 9 Apr 2025 08:39:26 +0200 Subject: [PATCH 36/37] fix: fix cycle dependency between api and client (#480) * fix cycle dependency between api and client Signed-off-by: gruebel * remove comment Signed-off-by: gruebel --------- Signed-off-by: gruebel Co-authored-by: Michael Beemer --- openfeature/api.py | 65 +++------------------ openfeature/client.py | 16 +++-- openfeature/evaluation_context.py | 19 ------ openfeature/evaluation_context/__init__.py | 38 ++++++++++++ openfeature/hook/__init__.py | 26 ++++++++- openfeature/provider/_registry.py | 5 +- openfeature/transaction_context/__init__.py | 29 +++++++++ 7 files changed, 109 insertions(+), 89 deletions(-) delete mode 100644 openfeature/evaluation_context.py create mode 100644 openfeature/evaluation_context/__init__.py diff --git a/openfeature/api.py b/openfeature/api.py index 36432d0c..d2b22e9d 100644 --- a/openfeature/api.py +++ b/openfeature/api.py @@ -2,19 +2,22 @@ from openfeature import _event_support from openfeature.client import OpenFeatureClient -from openfeature.evaluation_context import EvaluationContext +from openfeature.evaluation_context import ( + get_evaluation_context, + set_evaluation_context, +) from openfeature.event import ( EventHandler, ProviderEvent, ) -from openfeature.exception import GeneralError -from openfeature.hook import Hook +from openfeature.hook import add_hooks, clear_hooks, get_hooks from openfeature.provider import FeatureProvider from openfeature.provider._registry import provider_registry from openfeature.provider.metadata import Metadata -from openfeature.transaction_context import TransactionContextPropagator -from openfeature.transaction_context.no_op_transaction_context_propagator import ( - NoOpTransactionContextPropagator, +from openfeature.transaction_context import ( + get_transaction_context, + set_transaction_context, + set_transaction_context_propagator, ) __all__ = [ @@ -35,13 +38,6 @@ "shutdown", ] -_evaluation_context = EvaluationContext() -_evaluation_transaction_context_propagator: TransactionContextPropagator = ( - NoOpTransactionContextPropagator() -) - -_hooks: list[Hook] = [] - def get_client( domain: typing.Optional[str] = None, version: typing.Optional[str] = None @@ -67,49 +63,6 @@ def get_provider_metadata(domain: typing.Optional[str] = None) -> Metadata: return provider_registry.get_provider(domain).get_metadata() -def get_evaluation_context() -> EvaluationContext: - return _evaluation_context - - -def set_evaluation_context(evaluation_context: EvaluationContext) -> None: - global _evaluation_context - if evaluation_context is None: - raise GeneralError(error_message="No api level evaluation context") - _evaluation_context = evaluation_context - - -def set_transaction_context_propagator( - transaction_context_propagator: TransactionContextPropagator, -) -> None: - global _evaluation_transaction_context_propagator - _evaluation_transaction_context_propagator = transaction_context_propagator - - -def get_transaction_context() -> EvaluationContext: - return _evaluation_transaction_context_propagator.get_transaction_context() - - -def set_transaction_context(evaluation_context: EvaluationContext) -> None: - global _evaluation_transaction_context_propagator - _evaluation_transaction_context_propagator.set_transaction_context( - evaluation_context - ) - - -def add_hooks(hooks: list[Hook]) -> None: - global _hooks - _hooks = _hooks + hooks - - -def clear_hooks() -> None: - global _hooks - _hooks = [] - - -def get_hooks() -> list[Hook]: - return _hooks - - def shutdown() -> None: provider_registry.shutdown() diff --git a/openfeature/client.py b/openfeature/client.py index f852a10b..55c19309 100644 --- a/openfeature/client.py +++ b/openfeature/client.py @@ -3,8 +3,8 @@ from collections.abc import Awaitable from dataclasses import dataclass -from openfeature import _event_support, api -from openfeature.evaluation_context import EvaluationContext +from openfeature import _event_support +from openfeature.evaluation_context import EvaluationContext, get_evaluation_context from openfeature.event import EventHandler, ProviderEvent from openfeature.exception import ( ErrorCode, @@ -21,7 +21,7 @@ FlagType, Reason, ) -from openfeature.hook import Hook, HookContext, HookHints +from openfeature.hook import Hook, HookContext, HookHints, get_hooks from openfeature.hook._hook_support import ( after_all_hooks, after_hooks, @@ -30,6 +30,7 @@ ) from openfeature.provider import FeatureProvider, ProviderStatus from openfeature.provider._registry import provider_registry +from openfeature.transaction_context import get_transaction_context __all__ = [ "ClientMetadata", @@ -433,10 +434,7 @@ def _establish_hooks_and_provider( # in the flag evaluation # before: API, Client, Invocation, Provider merged_hooks = ( - api.get_hooks() - + self.hooks - + evaluation_hooks - + provider.get_provider_hooks() + get_hooks() + self.hooks + evaluation_hooks + provider.get_provider_hooks() ) # after, error, finally: Provider, Invocation, Client, API reversed_merged_hooks = merged_hooks[:] @@ -474,8 +472,8 @@ def _before_hooks_and_merge_context( # Requirement 3.2.2 merge: API.context->transaction.context->client.context->invocation.context merged_context = ( - api.get_evaluation_context() - .merge(api.get_transaction_context()) + get_evaluation_context() + .merge(get_transaction_context()) .merge(self.context) .merge(invocation_context) ) diff --git a/openfeature/evaluation_context.py b/openfeature/evaluation_context.py deleted file mode 100644 index c3af350c..00000000 --- a/openfeature/evaluation_context.py +++ /dev/null @@ -1,19 +0,0 @@ -import typing -from dataclasses import dataclass, field - -__all__ = ["EvaluationContext"] - - -@dataclass -class EvaluationContext: - targeting_key: typing.Optional[str] = None - attributes: dict = field(default_factory=dict) - - def merge(self, ctx2: "EvaluationContext") -> "EvaluationContext": - if not (self and ctx2): - return self or ctx2 - - attributes = {**self.attributes, **ctx2.attributes} - targeting_key = ctx2.targeting_key or self.targeting_key - - return EvaluationContext(targeting_key=targeting_key, attributes=attributes) diff --git a/openfeature/evaluation_context/__init__.py b/openfeature/evaluation_context/__init__.py new file mode 100644 index 00000000..f1170a12 --- /dev/null +++ b/openfeature/evaluation_context/__init__.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import typing +from dataclasses import dataclass, field + +from openfeature.exception import GeneralError + +__all__ = ["EvaluationContext", "get_evaluation_context", "set_evaluation_context"] + + +@dataclass +class EvaluationContext: + targeting_key: typing.Optional[str] = None + attributes: dict = field(default_factory=dict) + + def merge(self, ctx2: EvaluationContext) -> EvaluationContext: + if not (self and ctx2): + return self or ctx2 + + attributes = {**self.attributes, **ctx2.attributes} + targeting_key = ctx2.targeting_key or self.targeting_key + + return EvaluationContext(targeting_key=targeting_key, attributes=attributes) + + +def get_evaluation_context() -> EvaluationContext: + return _evaluation_context + + +def set_evaluation_context(evaluation_context: EvaluationContext) -> None: + global _evaluation_context + if evaluation_context is None: + raise GeneralError(error_message="No api level evaluation context") + _evaluation_context = evaluation_context + + +# need to be at the bottom, because of the definition order +_evaluation_context = EvaluationContext() diff --git a/openfeature/hook/__init__.py b/openfeature/hook/__init__.py index 16fa4bdd..e881fdb4 100644 --- a/openfeature/hook/__init__.py +++ b/openfeature/hook/__init__.py @@ -12,7 +12,17 @@ from openfeature.client import ClientMetadata from openfeature.provider.metadata import Metadata -__all__ = ["Hook", "HookContext", "HookHints", "HookType"] +__all__ = [ + "Hook", + "HookContext", + "HookHints", + "HookType", + "add_hooks", + "clear_hooks", + "get_hooks", +] + +_hooks: list[Hook] = [] class HookType(Enum): @@ -133,3 +143,17 @@ def supports_flag_value_type(self, flag_type: FlagType) -> bool: or not (False) """ return True + + +def add_hooks(hooks: list[Hook]) -> None: + global _hooks + _hooks = _hooks + hooks + + +def clear_hooks() -> None: + global _hooks + _hooks = [] + + +def get_hooks() -> list[Hook]: + return _hooks diff --git a/openfeature/provider/_registry.py b/openfeature/provider/_registry.py index 78412f1e..1b5ff041 100644 --- a/openfeature/provider/_registry.py +++ b/openfeature/provider/_registry.py @@ -1,7 +1,7 @@ import typing from openfeature._event_support import run_handlers_for_provider -from openfeature.evaluation_context import EvaluationContext +from openfeature.evaluation_context import EvaluationContext, get_evaluation_context from openfeature.event import ( ProviderEvent, ProviderEventDetails, @@ -65,9 +65,6 @@ def shutdown(self) -> None: self._shutdown_provider(provider) def _get_evaluation_context(self) -> EvaluationContext: - # imported here to avoid circular imports - from openfeature.api import get_evaluation_context - return get_evaluation_context() def _initialize_provider(self, provider: FeatureProvider) -> None: diff --git a/openfeature/transaction_context/__init__.py b/openfeature/transaction_context/__init__.py index ca711cbf..89f05360 100644 --- a/openfeature/transaction_context/__init__.py +++ b/openfeature/transaction_context/__init__.py @@ -1,6 +1,10 @@ +from openfeature.evaluation_context import EvaluationContext from openfeature.transaction_context.context_var_transaction_context_propagator import ( ContextVarsTransactionContextPropagator, ) +from openfeature.transaction_context.no_op_transaction_context_propagator import ( + NoOpTransactionContextPropagator, +) from openfeature.transaction_context.transaction_context_propagator import ( TransactionContextPropagator, ) @@ -8,4 +12,29 @@ __all__ = [ "ContextVarsTransactionContextPropagator", "TransactionContextPropagator", + "get_transaction_context", + "set_transaction_context", + "set_transaction_context_propagator", ] + +_evaluation_transaction_context_propagator: TransactionContextPropagator = ( + NoOpTransactionContextPropagator() +) + + +def set_transaction_context_propagator( + transaction_context_propagator: TransactionContextPropagator, +) -> None: + global _evaluation_transaction_context_propagator + _evaluation_transaction_context_propagator = transaction_context_propagator + + +def get_transaction_context() -> EvaluationContext: + return _evaluation_transaction_context_propagator.get_transaction_context() + + +def set_transaction_context(evaluation_context: EvaluationContext) -> None: + global _evaluation_transaction_context_propagator + _evaluation_transaction_context_propagator.set_transaction_context( + evaluation_context + ) From 4006df768ccc1f3e01110d4bda1f238ee2be22cf Mon Sep 17 00:00:00 2001 From: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:47:46 -0400 Subject: [PATCH 37/37] chore(main): release 0.8.1 (#445) Signed-off-by: OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com> --- .release-please-manifest.json | 2 +- CHANGELOG.md | 58 +++++++++++++++++++++++++++++++++++ README.md | 8 ++--- openfeature/version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 65 insertions(+), 7 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 02e64b96..88b915dd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{".":"0.8.0"} \ No newline at end of file +{".":"0.8.1"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a43a48ef..4aa027f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## [0.8.1](https://github.com/open-feature/python-sdk/compare/v0.8.0...v0.8.1) (2025-04-09) + + +### ๐Ÿ› Bug Fixes + +* add passthrough init to abstract provider ([#450](https://github.com/open-feature/python-sdk/issues/450)) ([088409e](https://github.com/open-feature/python-sdk/commit/088409ea5cdefef33f28fc4f45026fabac52377a)) +* fix cycle dependency between api and client ([#480](https://github.com/open-feature/python-sdk/issues/480)) ([3636a0d](https://github.com/open-feature/python-sdk/commit/3636a0d75f69712844a768cbc6c2f80fdcf6eb84)) + + +### โœจ New Features + +* add OTel utility function ([#451](https://github.com/open-feature/python-sdk/issues/451)) ([2d1ba85](https://github.com/open-feature/python-sdk/commit/2d1ba85c93cdd954f539d2872783b21683bd8b07)) + + +### ๐Ÿงน Chore + +* add codeowner file to be consistent with the rest of openfeature ([#477](https://github.com/open-feature/python-sdk/issues/477)) ([7a30ef9](https://github.com/open-feature/python-sdk/commit/7a30ef914b3180fc72be9a1d2072a8a288e8b54d)) +* **deps:** update actions/setup-python digest to 8d9ed9a ([#473](https://github.com/open-feature/python-sdk/issues/473)) ([a135911](https://github.com/open-feature/python-sdk/commit/a1359112e9c1d740bcca501cbb5aadd9da3602b6)) +* **deps:** update codecov/codecov-action action to v5.4.0 ([#456](https://github.com/open-feature/python-sdk/issues/456)) ([a666227](https://github.com/open-feature/python-sdk/commit/a666227f55b14d4d2b6e43b6487ac643b6893739)) +* **deps:** update github/codeql-action digest to 1b549b9 ([#470](https://github.com/open-feature/python-sdk/issues/470)) ([4eeab3b](https://github.com/open-feature/python-sdk/commit/4eeab3b6914bd947a63f8d3c5bb89b85b7c2ced1)) +* **deps:** update github/codeql-action digest to 45775bd ([#483](https://github.com/open-feature/python-sdk/issues/483)) ([5a2825b](https://github.com/open-feature/python-sdk/commit/5a2825b00db0653c6d0496ec7f4703f9125cbed7)) +* **deps:** update github/codeql-action digest to 5f8171a ([#467](https://github.com/open-feature/python-sdk/issues/467)) ([d69b759](https://github.com/open-feature/python-sdk/commit/d69b7594a956a49385ef3030c212624d628aec74)) +* **deps:** update github/codeql-action digest to 6bb031a ([#462](https://github.com/open-feature/python-sdk/issues/462)) ([0396592](https://github.com/open-feature/python-sdk/commit/0396592586b6f721754c18a46b6d2fee3c2f80e8)) +* **deps:** update github/codeql-action digest to b56ba49 ([#454](https://github.com/open-feature/python-sdk/issues/454)) ([613388d](https://github.com/open-feature/python-sdk/commit/613388ddde33b6ce5ff3a39760970297dfa83255)) +* **deps:** update github/codeql-action digest to fc7e4a0 ([#481](https://github.com/open-feature/python-sdk/issues/481)) ([1ae9fc2](https://github.com/open-feature/python-sdk/commit/1ae9fc2361f1671cee8c794f02c01eb6ca0b77a6)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.10.0 ([#463](https://github.com/open-feature/python-sdk/issues/463)) ([5fede4d](https://github.com/open-feature/python-sdk/commit/5fede4d4f0cb6e39f84e85c72c9a2dd13434bc78)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 ([#465](https://github.com/open-feature/python-sdk/issues/465)) ([d1eb3a0](https://github.com/open-feature/python-sdk/commit/d1eb3a08a8da75022788cc4b9ea7b7d95aec4e69)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.1 ([#468](https://github.com/open-feature/python-sdk/issues/468)) ([c07d3d6](https://github.com/open-feature/python-sdk/commit/c07d3d64677c2ce475b098580b5eba1dd7f95a2e)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.2 ([#469](https://github.com/open-feature/python-sdk/issues/469)) ([95e87c7](https://github.com/open-feature/python-sdk/commit/95e87c71fc835cde7f7528e974509438ab8f2dc3)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.3 ([#475](https://github.com/open-feature/python-sdk/issues/475)) ([2be2c06](https://github.com/open-feature/python-sdk/commit/2be2c06569d89309a70793bb14a82be91d2ccf20)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.4 ([#476](https://github.com/open-feature/python-sdk/issues/476)) ([8acc883](https://github.com/open-feature/python-sdk/commit/8acc88328836c70f168ca87b71f4c49a6dba9381)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.10 ([#461](https://github.com/open-feature/python-sdk/issues/461)) ([9057c6b](https://github.com/open-feature/python-sdk/commit/9057c6b3df6ca5dc9e429db231eb4427cce031ea)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.7 ([#453](https://github.com/open-feature/python-sdk/issues/453)) ([a5cb27b](https://github.com/open-feature/python-sdk/commit/a5cb27b67839d60ea631001759478b2e74b75f28)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.8 ([#457](https://github.com/open-feature/python-sdk/issues/457)) ([0c1a388](https://github.com/open-feature/python-sdk/commit/0c1a388ca121e232f5c36b4b7a550d541ae34e5b)) +* **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.9.9 ([#458](https://github.com/open-feature/python-sdk/issues/458)) ([9ce51eb](https://github.com/open-feature/python-sdk/commit/9ce51ebff5a896b818e241fd8e3c2dea2fee610c)) +* **deps:** update spec digest to 09aef37 ([#460](https://github.com/open-feature/python-sdk/issues/460)) ([547781f](https://github.com/open-feature/python-sdk/commit/547781fbd82d1e2ee8a17988d11da3875d6a73dd)) +* **deps:** update spec digest to 0cd553d ([#455](https://github.com/open-feature/python-sdk/issues/455)) ([fe99f08](https://github.com/open-feature/python-sdk/commit/fe99f08e9465e8d35dd2b187d8ac01eae98432b7)) +* **deps:** update spec digest to 130df3e ([#471](https://github.com/open-feature/python-sdk/issues/471)) ([9ced6bf](https://github.com/open-feature/python-sdk/commit/9ced6bf2d1c7e3b0f01d062564ee63e49254af00)) +* **deps:** update spec digest to 25c57ee ([#459](https://github.com/open-feature/python-sdk/issues/459)) ([40cbd82](https://github.com/open-feature/python-sdk/commit/40cbd82dda20604a7a7be00e6913710d4a1ab56f)) +* **deps:** update spec digest to 27e4461 ([#472](https://github.com/open-feature/python-sdk/issues/472)) ([490cd06](https://github.com/open-feature/python-sdk/commit/490cd068533bb5ad702adf71915b6e0ac49706d8)) +* **deps:** update spec digest to 54952f3 ([#447](https://github.com/open-feature/python-sdk/issues/447)) ([f907855](https://github.com/open-feature/python-sdk/commit/f907855966cf788a3522e7626c76bd050de59a7e)) +* **deps:** update spec digest to a69f748 ([#452](https://github.com/open-feature/python-sdk/issues/452)) ([95b33b3](https://github.com/open-feature/python-sdk/commit/95b33b39e6ef472264002322162e83665054d71b)) +* **deps:** update spec digest to aad6193 ([#464](https://github.com/open-feature/python-sdk/issues/464)) ([d15388b](https://github.com/open-feature/python-sdk/commit/d15388b542798f7703578927dc5013863a83efa1)) +* improve resolve details callable type hints ([#449](https://github.com/open-feature/python-sdk/issues/449)) ([31afa64](https://github.com/open-feature/python-sdk/commit/31afa6490f7c2fc7a553b69c56840d494a520836)) +* revert spec to commit 0cd553d ([#479](https://github.com/open-feature/python-sdk/issues/479)) ([0ebec53](https://github.com/open-feature/python-sdk/commit/0ebec538db4d1180bad05e89bb62db23ca606a27)) +* use existing submodule version for e2e tests ([#444](https://github.com/open-feature/python-sdk/issues/444)) ([5ae8571](https://github.com/open-feature/python-sdk/commit/5ae8571ccd5f30c0aef87b0bc7f1a08a65254df0)) +* use keyword arguments, validate test ([#446](https://github.com/open-feature/python-sdk/issues/446)) ([f29c450](https://github.com/open-feature/python-sdk/commit/f29c4506a6a13307ba95a9b450a1b19c328975b3)) + + +### ๐Ÿ“š Documentation + +* fix linting issue on the readme ([1198728](https://github.com/open-feature/python-sdk/commit/11987280ba53ba087b1792316acc920a81434630)) + + +### ๐Ÿ”„ Refactoring + +* replace exception raising with error flag resolution ([#474](https://github.com/open-feature/python-sdk/issues/474)) ([e61b69b](https://github.com/open-feature/python-sdk/commit/e61b69bb5079547c62a3ad51499326057db69e7a)) + ## [0.8.0](https://github.com/open-feature/python-sdk/compare/v0.7.5...v0.8.0) (2025-02-11) diff --git a/README.md b/README.md index a9f64b0c..af778c12 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ - - Latest version + + Latest version @@ -60,13 +60,13 @@ #### Pip install ```bash -pip install openfeature-sdk==0.8.0 +pip install openfeature-sdk==0.8.1 ``` #### requirements.txt ```bash -openfeature-sdk==0.8.0 +openfeature-sdk==0.8.1 ``` ```python diff --git a/openfeature/version.py b/openfeature/version.py index 777f190d..8088f751 100644 --- a/openfeature/version.py +++ b/openfeature/version.py @@ -1 +1 @@ -__version__ = "0.8.0" +__version__ = "0.8.1" diff --git a/pyproject.toml b/pyproject.toml index dc2e0dce..7b8dcf43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "openfeature_sdk" -version = "0.8.0" +version = "0.8.1" description = "Standardizing Feature Flagging for Everyone" readme = "README.md" authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]