Merge branch 'main' into feat/process-flag-evaluation-options
Signed-off-by: Tom Carrio <tom@carrio.dev>
This commit is contained in:
commit
f2534eb333
|
|
@ -14,13 +14,29 @@ permissions: # added using https://github.com/step-security/secure-workflows
|
|||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
release-please:
|
||||
permissions:
|
||||
contents: write # for google-github-actions/release-please-action to create release commit
|
||||
pull-requests: write # for google-github-actions/release-please-action to create release PR
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
container: [ "python:3.10" ]
|
||||
|
||||
steps:
|
||||
- uses: google-github-actions/release-please-action@v3
|
||||
id: release
|
||||
with:
|
||||
command: manifest
|
||||
token: ${{secrets.GITHUB_TOKEN}}
|
||||
default-branch: main
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
release_tag_name: ${{ steps.release.outputs.tag_name }}
|
||||
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
needs: release-please
|
||||
if: ${{ needs.release-please.outputs.release_created }}
|
||||
container:
|
||||
image: ${{ matrix.container }}
|
||||
image: "python:3.11"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{".":"0.0.4"}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Changelog
|
||||
|
||||
## [0.0.4](https://github.com/open-feature/python-sdk/compare/v0.0.3...v0.0.4) (2022-11-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add needs to release job ([#52](https://github.com/open-feature/python-sdk/issues/52)) ([fb7655a](https://github.com/open-feature/python-sdk/commit/fb7655aa3aae0fb021e0aae57c0a7d182a8218cf))
|
||||
|
||||
## [0.0.3](https://github.com/open-feature/python-sdk/compare/v0.0.2...v0.0.3) (2022-11-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Run a single container for sdk release ([#50](https://github.com/open-feature/python-sdk/issues/50)) ([87c62cf](https://github.com/open-feature/python-sdk/commit/87c62cfae7ce2bd47ed79adb7bb9b58d3b0072fd))
|
||||
|
||||
## [0.0.2](https://github.com/open-feature/python-sdk/compare/v0.0.1...v0.0.2) (2022-11-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add metadata to providers ([#26](https://github.com/open-feature/python-sdk/issues/26)) ([b39cced](https://github.com/open-feature/python-sdk/commit/b39cced329d16741aa8fa8768fd44ff51f916bfa))
|
||||
* Add release please to handle releases ([#45](https://github.com/open-feature/python-sdk/issues/45)) ([5bc0571](https://github.com/open-feature/python-sdk/commit/5bc057192d69659d17b9552cae854843a86d879c))
|
||||
* Fix release workflow ([#48](https://github.com/open-feature/python-sdk/issues/48)) ([2c44d55](https://github.com/open-feature/python-sdk/commit/2c44d55af349e9485a3f697f28c1391cc11c5ed0))
|
||||
* spec-0.2.0 ([#38](https://github.com/open-feature/python-sdk/issues/38)) ([311b8ee](https://github.com/open-feature/python-sdk/commit/311b8eef53cfd535f8f45e5cd680381cc79abbc1))
|
||||
* specification-0.5.0 ([#44](https://github.com/open-feature/python-sdk/issues/44)) ([04a4323](https://github.com/open-feature/python-sdk/commit/04a432331036cf771a613dd66dcc46c4e10d9284))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* eval context fixes and new error types ([#43](https://github.com/open-feature/python-sdk/issues/43)) ([06d0494](https://github.com/open-feature/python-sdk/commit/06d0494331b62deb0c0ec96846ffed5ab8471e60))
|
||||
* Move flag evaluation details to a dataclass ([#27](https://github.com/open-feature/python-sdk/issues/27)) ([b44224b](https://github.com/open-feature/python-sdk/commit/b44224be0ddaf3745d031d6e7caea19f41322cf1))
|
||||
* requirements-dev.txt to reduce vulnerabilities ([#37](https://github.com/open-feature/python-sdk/issues/37)) ([1e82122](https://github.com/open-feature/python-sdk/commit/1e82122978e92173fab3c65c75ca3b5477ce3655))
|
||||
* Unit tests ([#28](https://github.com/open-feature/python-sdk/issues/28)) ([df0c033](https://github.com/open-feature/python-sdk/commit/df0c03308346c5b3be7223edc162582c578b4678))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add badge showing supported Python version range ([c2d214a](https://github.com/open-feature/python-sdk/commit/c2d214a809bf2b9f12f50dc8a5a6aec32f9d1dca))
|
||||
* add badge showing supported Python version range ([f845d9e](https://github.com/open-feature/python-sdk/commit/f845d9e4184ca9328311906c825d42b2cc73a319))
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
import typing
|
||||
|
||||
|
||||
class EvaluationContext:
|
||||
def __init__(self, targeting_key: str = None, attributes: dict = None):
|
||||
def __init__(
|
||||
self,
|
||||
targeting_key: typing.Optional[str] = None,
|
||||
attributes: typing.Optional[dict] = None,
|
||||
):
|
||||
self.targeting_key = targeting_key
|
||||
self.attributes = attributes or {}
|
||||
|
||||
|
|
@ -7,7 +14,7 @@ class EvaluationContext:
|
|||
if not (self and ctx2):
|
||||
return self or ctx2
|
||||
|
||||
self.attributes = {**self.attributes, **ctx2.attributes}
|
||||
self.targeting_key = ctx2.targeting_key or self.targeting_key
|
||||
attributes = {**self.attributes, **ctx2.attributes}
|
||||
targeting_key = ctx2.targeting_key or self.targeting_key
|
||||
|
||||
return self
|
||||
return EvaluationContext(targeting_key=targeting_key, attributes=attributes)
|
||||
|
|
|
|||
|
|
@ -6,4 +6,6 @@ class ErrorCode(Enum):
|
|||
FLAG_NOT_FOUND = "FLAG_NOT_FOUND"
|
||||
PARSE_ERROR = "PARSE_ERROR"
|
||||
TYPE_MISMATCH = "TYPE_MISMATCH"
|
||||
TARGETING_KEY_MISSING = "TARGETING_KEY_MISSING"
|
||||
INVALID_CONTEXT = "INVALID_CONTEXT"
|
||||
GENERAL = "GENERAL"
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
from open_feature.flag_evaluation.error_code import ErrorCode
|
||||
import typing
|
||||
|
||||
from open_feature.exception.error_code import ErrorCode
|
||||
|
||||
|
||||
class OpenFeatureError(Exception):
|
||||
|
|
@ -7,13 +9,14 @@ class OpenFeatureError(Exception):
|
|||
the more specific exceptions extending this one should be used.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: str = None, error_code: ErrorCode = None):
|
||||
def __init__(
|
||||
self, error_code: ErrorCode, error_message: typing.Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Constructor for the generic OpenFeatureError.
|
||||
@param error_message: a string message representing why the error has been
|
||||
raised
|
||||
@param error_message: an optional string message representing why the
|
||||
error has been raised
|
||||
@param error_code: the ErrorCode string enum value for the type of error
|
||||
@return: the generic OpenFeatureError exception
|
||||
"""
|
||||
self.error_message = error_message
|
||||
self.error_code = error_code
|
||||
|
|
@ -25,15 +28,14 @@ class FlagNotFoundError(OpenFeatureError):
|
|||
key provided by the user.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: str = None):
|
||||
def __init__(self, error_message: typing.Optional[str] = None):
|
||||
"""
|
||||
Constructor for the FlagNotFoundError. The error code for
|
||||
this type of exception is ErrorCode.FLAG_NOT_FOUND.
|
||||
@param error_message: a string message representing why the error has been
|
||||
raised
|
||||
@return: the generic FlagNotFoundError exception
|
||||
@param error_message: an optional string message representing
|
||||
why the error has been raised
|
||||
"""
|
||||
super().__init__(error_message, ErrorCode.FLAG_NOT_FOUND)
|
||||
super().__init__(ErrorCode.FLAG_NOT_FOUND, error_message)
|
||||
|
||||
|
||||
class GeneralError(OpenFeatureError):
|
||||
|
|
@ -42,15 +44,14 @@ class GeneralError(OpenFeatureError):
|
|||
feature python sdk.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: str = None):
|
||||
def __init__(self, error_message: typing.Optional[str] = None):
|
||||
"""
|
||||
Constructor for the GeneralError. The error code for this type of exception
|
||||
is ErrorCode.GENERAL.
|
||||
@param error_message: a string message representing why the error has been
|
||||
raised
|
||||
@return: the generic GeneralError exception
|
||||
@param error_message: an optional string message representing why the error
|
||||
has been raised
|
||||
"""
|
||||
super().__init__(error_message, ErrorCode.GENERAL)
|
||||
super().__init__(ErrorCode.GENERAL, error_message)
|
||||
|
||||
|
||||
class ParseError(OpenFeatureError):
|
||||
|
|
@ -59,15 +60,14 @@ class ParseError(OpenFeatureError):
|
|||
be parsed into a FlagEvaluationDetails object.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: str = None):
|
||||
def __init__(self, error_message: typing.Optional[str] = None):
|
||||
"""
|
||||
Constructor for the ParseError. The error code for this type of exception
|
||||
is ErrorCode.PARSE_ERROR.
|
||||
@param error_message: a string message representing why the error has been
|
||||
raised
|
||||
@return: the generic ParseError exception
|
||||
@param error_message: an optional string message representing why the
|
||||
error has been raised
|
||||
"""
|
||||
super().__init__(error_message, ErrorCode.PARSE_ERROR)
|
||||
super().__init__(ErrorCode.PARSE_ERROR, error_message)
|
||||
|
||||
|
||||
class TypeMismatchError(OpenFeatureError):
|
||||
|
|
@ -76,12 +76,43 @@ class TypeMismatchError(OpenFeatureError):
|
|||
not match the type requested by the user.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: str = None):
|
||||
def __init__(self, error_message: typing.Optional[str] = None):
|
||||
"""
|
||||
Constructor for the TypeMismatchError. The error code for this type of
|
||||
exception is ErrorCode.TYPE_MISMATCH.
|
||||
@param error_message: an optional string message representing why the
|
||||
error has been raised
|
||||
"""
|
||||
super().__init__(ErrorCode.TYPE_MISMATCH, error_message)
|
||||
|
||||
|
||||
class TargetingKeyMissingError(OpenFeatureError):
|
||||
"""
|
||||
This exception should be raised when the provider requires a targeting key
|
||||
but one was not provided in the evaluation context.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: typing.Optional[str] = None):
|
||||
"""
|
||||
Constructor for the TargetingKeyMissingError. The error code for this type of
|
||||
exception is ErrorCode.TARGETING_KEY_MISSING.
|
||||
@param error_message: a string message representing why the error has been
|
||||
raised
|
||||
@return: the generic TypeMismatchError exception
|
||||
"""
|
||||
super().__init__(error_message, ErrorCode.TYPE_MISMATCH)
|
||||
super().__init__(ErrorCode.TARGETING_KEY_MISSING, error_message)
|
||||
|
||||
|
||||
class InvalidContextError(OpenFeatureError):
|
||||
"""
|
||||
This exception should be raised when the evaluation context does not meet provider
|
||||
requirements.
|
||||
"""
|
||||
|
||||
def __init__(self, error_message: typing.Optional[str]):
|
||||
"""
|
||||
Constructor for the InvalidContextError. The error code for this type of
|
||||
exception is ErrorCode.INVALID_CONTEXT.
|
||||
@param error_message: a string message representing why the error has been
|
||||
raised
|
||||
"""
|
||||
super().__init__(ErrorCode.INVALID_CONTEXT, error_message)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
from open_feature.flag_evaluation.error_code import ErrorCode
|
||||
from open_feature.exception.error_code import ErrorCode
|
||||
from open_feature.flag_evaluation.reason import Reason
|
||||
|
||||
T = typing.TypeVar("T", covariant=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlagEvaluationDetails:
|
||||
class FlagEvaluationDetails(typing.Generic[T]):
|
||||
flag_key: str
|
||||
value: typing.Any
|
||||
variant: str = None
|
||||
reason: Reason = None
|
||||
error_code: ErrorCode = None
|
||||
error_message: str = None
|
||||
value: T
|
||||
variant: typing.Optional[str] = None
|
||||
reason: typing.Optional[Reason] = None
|
||||
error_code: typing.Optional[ErrorCode] = None
|
||||
error_message: typing.Optional[str] = None
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from open_feature.hooks.hook import Hook
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlagEvaluationOptions:
|
||||
hooks: typing.List[Hook] = field(default_factory=list)
|
||||
hook_hints: dict = field(default_factory=dict)
|
||||
|
|
@ -2,7 +2,8 @@ from enum import Enum
|
|||
|
||||
|
||||
class FlagType(Enum):
|
||||
BOOLEAN = 1
|
||||
STRING = 2
|
||||
NUMBER = 3
|
||||
OBJECT = 4
|
||||
BOOLEAN = bool
|
||||
STRING = str
|
||||
OBJECT = dict
|
||||
FLOAT = float
|
||||
INTEGER = int
|
||||
|
|
|
|||
|
|
@ -11,5 +11,5 @@ class HookContext:
|
|||
flag_type: FlagType
|
||||
default_value: typing.Any
|
||||
evaluation_context: EvaluationContext
|
||||
client_metadata: dict = None
|
||||
provider_metadata: dict = None
|
||||
client_metadata: typing.Optional[dict] = None
|
||||
provider_metadata: typing.Optional[dict] = None
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ def error_hooks(
|
|||
hook_context: HookContext,
|
||||
exception: Exception,
|
||||
hooks: typing.List[Hook],
|
||||
hints: dict,
|
||||
hints: typing.Optional[typing.Mapping] = None,
|
||||
):
|
||||
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
|
||||
_execute_hooks(
|
||||
|
|
@ -27,7 +27,7 @@ def after_all_hooks(
|
|||
flag_type: FlagType,
|
||||
hook_context: HookContext,
|
||||
hooks: typing.List[Hook],
|
||||
hints: dict,
|
||||
hints: typing.Optional[typing.Mapping] = None,
|
||||
):
|
||||
kwargs = {"hook_context": hook_context, "hints": hints}
|
||||
_execute_hooks(
|
||||
|
|
@ -40,7 +40,7 @@ def after_hooks(
|
|||
hook_context: HookContext,
|
||||
details: FlagEvaluationDetails,
|
||||
hooks: typing.List[Hook],
|
||||
hints: dict,
|
||||
hints: typing.Optional[typing.Mapping] = None,
|
||||
):
|
||||
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
|
||||
_execute_hooks_unchecked(
|
||||
|
|
@ -52,7 +52,7 @@ def before_hooks(
|
|||
flag_type: FlagType,
|
||||
hook_context: HookContext,
|
||||
hooks: typing.List[Hook],
|
||||
hints: dict,
|
||||
hints: typing.Optional[typing.Mapping] = None,
|
||||
) -> EvaluationContext:
|
||||
kwargs = {"hook_context": hook_context, "hints": hints}
|
||||
executed_hooks = _execute_hooks_unchecked(
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ from open_feature.exception.exceptions import GeneralError
|
|||
from open_feature.open_feature_client import OpenFeatureClient
|
||||
from open_feature.provider.provider import AbstractProvider
|
||||
|
||||
_provider = None
|
||||
_provider: typing.Optional[AbstractProvider] = None
|
||||
|
||||
|
||||
def get_client(name: str = None, version: str = None) -> OpenFeatureClient:
|
||||
def get_client(
|
||||
name: typing.Optional[str] = None, version: typing.Optional[str] = None
|
||||
) -> OpenFeatureClient:
|
||||
if _provider is None:
|
||||
raise GeneralError(
|
||||
error_message="Provider not set. Call set_provider before using get_client"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import logging
|
||||
import typing
|
||||
from numbers import Number
|
||||
|
||||
from open_feature.evaluation_context.evaluation_context import EvaluationContext
|
||||
from open_feature.exception.exceptions import GeneralError, OpenFeatureError
|
||||
from open_feature.flag_evaluation.error_code import ErrorCode
|
||||
from open_feature.exception.error_code import ErrorCode
|
||||
from open_feature.exception.exceptions import (
|
||||
GeneralError,
|
||||
OpenFeatureError,
|
||||
TypeMismatchError,
|
||||
)
|
||||
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
|
||||
from open_feature.flag_evaluation.flag_evaluation_options import FlagEvaluationOptions
|
||||
from open_feature.flag_evaluation.flag_type import FlagType
|
||||
from open_feature.flag_evaluation.reason import Reason
|
||||
from open_feature.hooks.hook import Hook
|
||||
|
|
@ -22,14 +26,33 @@ from open_feature.provider.no_op_provider import NoOpProvider
|
|||
from open_feature.provider.provider import AbstractProvider
|
||||
|
||||
|
||||
GetDetailCallable = typing.Union[
|
||||
typing.Callable[
|
||||
[str, bool, typing.Optional[EvaluationContext]], FlagEvaluationDetails[bool]
|
||||
],
|
||||
typing.Callable[
|
||||
[str, int, typing.Optional[EvaluationContext]], FlagEvaluationDetails[int]
|
||||
],
|
||||
typing.Callable[
|
||||
[str, float, typing.Optional[EvaluationContext]], FlagEvaluationDetails[float]
|
||||
],
|
||||
typing.Callable[
|
||||
[str, str, typing.Optional[EvaluationContext]], FlagEvaluationDetails[str]
|
||||
],
|
||||
typing.Callable[
|
||||
[str, dict, typing.Optional[EvaluationContext]], FlagEvaluationDetails[dict]
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
class OpenFeatureClient:
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
context: EvaluationContext = None,
|
||||
hooks: typing.List[Hook] = None,
|
||||
provider: AbstractProvider = None,
|
||||
name: typing.Optional[str],
|
||||
version: typing.Optional[str],
|
||||
provider: AbstractProvider,
|
||||
context: typing.Optional[EvaluationContext] = None,
|
||||
hooks: typing.Optional[typing.List[Hook]] = None,
|
||||
):
|
||||
self.name = name
|
||||
self.version = version
|
||||
|
|
@ -44,8 +67,8 @@ class OpenFeatureClient:
|
|||
self,
|
||||
flag_key: str,
|
||||
default_value: bool,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> bool:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.BOOLEAN,
|
||||
|
|
@ -59,8 +82,8 @@ class OpenFeatureClient:
|
|||
self,
|
||||
flag_key: str,
|
||||
default_value: bool,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.BOOLEAN,
|
||||
|
|
@ -74,8 +97,8 @@ class OpenFeatureClient:
|
|||
self,
|
||||
flag_key: str,
|
||||
default_value: str,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> str:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.STRING,
|
||||
|
|
@ -89,8 +112,8 @@ class OpenFeatureClient:
|
|||
self,
|
||||
flag_key: str,
|
||||
default_value: str,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.STRING,
|
||||
|
|
@ -100,30 +123,58 @@ class OpenFeatureClient:
|
|||
flag_evaluation_options,
|
||||
)
|
||||
|
||||
def get_number_value(
|
||||
def get_integer_value(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: Number,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
) -> Number:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.NUMBER,
|
||||
default_value: int,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> int:
|
||||
return self.get_integer_details(
|
||||
flag_key,
|
||||
default_value,
|
||||
evaluation_context,
|
||||
flag_evaluation_options,
|
||||
).value
|
||||
|
||||
def get_number_details(
|
||||
def get_integer_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: Number,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
default_value: int,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.NUMBER,
|
||||
FlagType.INTEGER,
|
||||
flag_key,
|
||||
default_value,
|
||||
evaluation_context,
|
||||
flag_evaluation_options,
|
||||
)
|
||||
|
||||
def get_float_value(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: float,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> float:
|
||||
return self.get_float_details(
|
||||
flag_key,
|
||||
default_value,
|
||||
evaluation_context,
|
||||
flag_evaluation_options,
|
||||
).value
|
||||
|
||||
def get_float_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: float,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.FLOAT,
|
||||
flag_key,
|
||||
default_value,
|
||||
evaluation_context,
|
||||
|
|
@ -134,8 +185,8 @@ class OpenFeatureClient:
|
|||
self,
|
||||
flag_key: str,
|
||||
default_value: dict,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> dict:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.OBJECT,
|
||||
|
|
@ -149,8 +200,8 @@ class OpenFeatureClient:
|
|||
self,
|
||||
flag_key: str,
|
||||
default_value: dict,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
return self.evaluate_flag_details(
|
||||
FlagType.OBJECT,
|
||||
|
|
@ -165,14 +216,14 @@ class OpenFeatureClient:
|
|||
flag_type: FlagType,
|
||||
flag_key: str,
|
||||
default_value: typing.Any,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
flag_evaluation_options: typing.Any = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Evaluate the flag requested by the user from the clients provider.
|
||||
|
||||
:param flag_type: the type of the flag being returned
|
||||
:param key: the string key of the selected flag
|
||||
:param flag_key: the string key of the selected flag
|
||||
:param default_value: backup value returned if no result found by the provider
|
||||
:param evaluation_context: Information for the purposes of flag evaluation
|
||||
:param flag_evaluation_options: Additional flag evaluation information
|
||||
|
|
@ -195,18 +246,34 @@ class OpenFeatureClient:
|
|||
client_metadata=None,
|
||||
provider_metadata=None,
|
||||
)
|
||||
merged_hooks = self.hooks
|
||||
# Todo add api level hooks
|
||||
# https://github.com/open-feature/spec/blob/main/specification/sections/04-hooks.md#requirement-442
|
||||
# Hooks need to be handled in different orders at different stages
|
||||
# in the flag evaluation
|
||||
# before: API, Client, Invocation, Provider
|
||||
merged_hooks = (
|
||||
self.hooks
|
||||
+ flag_evaluation_options.hooks
|
||||
+ self.provider.get_provider_hooks()
|
||||
)
|
||||
# after, error, finally: Provider, Invocation, Client, API
|
||||
reversed_merged_hooks = (
|
||||
self.provider.get_provider_hooks()
|
||||
+ flag_evaluation_options.hooks
|
||||
+ self.hooks
|
||||
)
|
||||
|
||||
try:
|
||||
# https://github.com/open-feature/spec/blob/main/specification/sections/03-evaluation-context.md
|
||||
# Any resulting evaluation context from a before hook will overwrite
|
||||
# duplicate fields defined globally, on the client, or in the invocation.
|
||||
# Requirement 3.2.2, 4.3.4: API.context->client.context->invocation.context
|
||||
invocation_context = before_hooks(
|
||||
flag_type, hook_context, merged_hooks, hook_hints
|
||||
)
|
||||
invocation_context.merge(ctx2=evaluation_context)
|
||||
invocation_context = invocation_context.merge(ctx2=evaluation_context)
|
||||
|
||||
# merge of: API.context, client.context, invocation.context
|
||||
# Requirement 3.2.2 merge: API.context->client.context->invocation.context
|
||||
merged_context = (
|
||||
api_evaluation_context().merge(self.context).merge(invocation_context)
|
||||
)
|
||||
|
|
@ -218,12 +285,14 @@ class OpenFeatureClient:
|
|||
merged_context,
|
||||
)
|
||||
|
||||
after_hooks(type, hook_context, flag_evaluation, merged_hooks, hook_hints)
|
||||
after_hooks(
|
||||
flag_type, hook_context, flag_evaluation, reversed_merged_hooks, hook_hints
|
||||
|
||||
return flag_evaluation
|
||||
|
||||
except OpenFeatureError as e:
|
||||
error_hooks(flag_type, hook_context, e, merged_hooks, hook_hints)
|
||||
error_hooks(flag_type, hook_context, e, reversed_merged_hooks, hook_hints)
|
||||
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
value=default_value,
|
||||
|
|
@ -234,7 +303,8 @@ class OpenFeatureClient:
|
|||
# Catch any type of exception here since the user can provide any exception
|
||||
# in the error hooks
|
||||
except Exception as e: # noqa
|
||||
error_hooks(flag_type, hook_context, e, merged_hooks, hook_hints)
|
||||
error_hooks(flag_type, hook_context, e, reversed_merged_hooks, hook_hints)
|
||||
|
||||
error_message = getattr(e, "error_message", str(e))
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
|
|
@ -245,14 +315,14 @@ class OpenFeatureClient:
|
|||
)
|
||||
|
||||
finally:
|
||||
after_all_hooks(flag_type, hook_context, merged_hooks, hook_hints)
|
||||
after_all_hooks(flag_type, hook_context, reversed_merged_hooks, hook_hints)
|
||||
|
||||
def _create_provider_evaluation(
|
||||
self,
|
||||
flag_type: FlagType,
|
||||
flag_key: str,
|
||||
default_value: typing.Any,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails:
|
||||
"""
|
||||
Encapsulated method to create a FlagEvaluationDetail from a specific provider.
|
||||
|
|
@ -274,18 +344,25 @@ class OpenFeatureClient:
|
|||
logging.info("No provider configured, using no-op provider.")
|
||||
self.provider = NoOpProvider()
|
||||
|
||||
get_details_callable = {
|
||||
FlagType.BOOLEAN: self.provider.get_boolean_details,
|
||||
FlagType.NUMBER: self.provider.get_number_details,
|
||||
FlagType.OBJECT: self.provider.get_object_details,
|
||||
FlagType.STRING: self.provider.get_string_details,
|
||||
}.get(flag_type)
|
||||
get_details_callables: typing.Mapping[FlagType, GetDetailCallable] = {
|
||||
FlagType.BOOLEAN: self.provider.resolve_boolean_details,
|
||||
FlagType.INTEGER: self.provider.resolve_integer_details,
|
||||
FlagType.FLOAT: self.provider.resolve_float_details,
|
||||
FlagType.OBJECT: self.provider.resolve_object_details,
|
||||
FlagType.STRING: self.provider.resolve_string_details,
|
||||
}
|
||||
|
||||
get_details_callable = get_details_callables.get(flag_type)
|
||||
if not get_details_callable:
|
||||
raise GeneralError(error_message="Unknown flag type")
|
||||
|
||||
return get_details_callable(*args)
|
||||
value = get_details_callable(*args)
|
||||
|
||||
if not isinstance(value.value, flag_type.value):
|
||||
raise TypeMismatchError()
|
||||
|
||||
return value
|
||||
|
||||
def __extract_evaluation_options(
|
||||
self, flag_evaluation_options: typing.Any
|
||||
) -> typing.Tuple(typing.List[Hook], MappingProxyType):
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from numbers import Number
|
||||
import typing
|
||||
|
||||
from open_feature.evaluation_context.evaluation_context import EvaluationContext
|
||||
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
|
||||
from open_feature.flag_evaluation.reason import Reason
|
||||
from open_feature.hooks.hook import Hook
|
||||
from open_feature.provider.metadata import Metadata
|
||||
from open_feature.provider.no_op_metadata import NoOpMetadata
|
||||
from open_feature.provider.provider import AbstractProvider
|
||||
|
|
@ -14,12 +15,15 @@ class NoOpProvider(AbstractProvider):
|
|||
def get_metadata(self) -> Metadata:
|
||||
return NoOpMetadata()
|
||||
|
||||
def get_boolean_details(
|
||||
def get_provider_hooks(self) -> typing.List[Hook]:
|
||||
return []
|
||||
|
||||
def resolve_boolean_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: bool,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
):
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[bool]:
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
value=default_value,
|
||||
|
|
@ -27,12 +31,12 @@ class NoOpProvider(AbstractProvider):
|
|||
variant=PASSED_IN_DEFAULT,
|
||||
)
|
||||
|
||||
def get_string_details(
|
||||
def resolve_string_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: str,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
):
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[str]:
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
value=default_value,
|
||||
|
|
@ -40,12 +44,12 @@ class NoOpProvider(AbstractProvider):
|
|||
variant=PASSED_IN_DEFAULT,
|
||||
)
|
||||
|
||||
def get_number_details(
|
||||
def resolve_integer_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: Number,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
):
|
||||
default_value: int,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[int]:
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
value=default_value,
|
||||
|
|
@ -53,12 +57,25 @@ class NoOpProvider(AbstractProvider):
|
|||
variant=PASSED_IN_DEFAULT,
|
||||
)
|
||||
|
||||
def get_object_details(
|
||||
def resolve_float_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: float,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[float]:
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
value=default_value,
|
||||
reason=Reason.DEFAULT,
|
||||
variant=PASSED_IN_DEFAULT,
|
||||
)
|
||||
|
||||
def resolve_object_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: dict,
|
||||
evaluation_context: EvaluationContext = None,
|
||||
):
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[dict]:
|
||||
return FlagEvaluationDetails(
|
||||
flag_key=flag_key,
|
||||
value=default_value,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import typing
|
||||
from abc import abstractmethod
|
||||
from numbers import Number
|
||||
|
||||
from open_feature.evaluation_context.evaluation_context import EvaluationContext
|
||||
from open_feature.flag_evaluation.flag_evaluation_details import FlagEvaluationDetails
|
||||
from open_feature.hooks.hook import Hook
|
||||
from open_feature.provider.metadata import Metadata
|
||||
|
||||
|
||||
|
|
@ -11,37 +13,50 @@ class AbstractProvider:
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_boolean_details(
|
||||
def get_provider_hooks(self) -> typing.List[Hook]:
|
||||
return []
|
||||
|
||||
@abstractmethod
|
||||
def resolve_boolean_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: bool,
|
||||
evaluation_context: EvaluationContext = EvaluationContext(),
|
||||
):
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[bool]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_string_details(
|
||||
def resolve_string_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: str,
|
||||
evaluation_context: EvaluationContext = EvaluationContext(),
|
||||
):
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[str]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_number_details(
|
||||
def resolve_integer_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: Number,
|
||||
evaluation_context: EvaluationContext = EvaluationContext(),
|
||||
):
|
||||
default_value: int,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[int]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_object_details(
|
||||
def resolve_float_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: float,
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[float]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def resolve_object_details(
|
||||
self,
|
||||
flag_key: str,
|
||||
default_value: dict,
|
||||
evaluation_context: EvaluationContext = EvaluationContext(),
|
||||
):
|
||||
evaluation_context: typing.Optional[EvaluationContext] = None,
|
||||
) -> FlagEvaluationDetails[dict]:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "openfeature_sdk"
|
||||
version = "0.0.1"
|
||||
version = "0.0.4"
|
||||
description = "Standardizing Feature Flagging for Everyone"
|
||||
readme = "readme.md"
|
||||
authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]
|
||||
|
|
|
|||
31
readme.md
31
readme.md
|
|
@ -1,4 +1,5 @@
|
|||
# Open Feature SDK for Python
|
||||
# OpenFeature SDK for Python
|
||||
|
||||
[](https://badge.fury.io/py/openfeature-sdk)
|
||||

|
||||
[](https://www.repostatus.org/#wip)
|
||||
|
|
@ -8,42 +9,56 @@
|
|||
|
||||
This is the Python implementation of [OpenFeature](https://openfeature.dev), a vendor-agnostic abstraction library for evaluating feature flags.
|
||||
|
||||
We support multiple data types for flags (numbers, strings, booleans, objects) as well as hooks, which can alter the lifecycle of a flag evaluation.
|
||||
We support multiple data types for flags (numbers, strings, booleans, objects) as well as hooks, which can alter the lifecycle of a flag evaluation.
|
||||
|
||||
This library is intended to be used in server-side contexts and has not been evaluated for use in mobile devices.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
While Boolean provides the simplest introduction, we offer a variety of flag types.
|
||||
|
||||
```python
|
||||
# Depending on the flag type, use one of the methods below
|
||||
flag_key = "PROVIDER_FLAG"
|
||||
boolean_result = open_feature_client.get_boolean_value(key=flag_key,default_value=False)
|
||||
number_result = open_feature_client.get_number_value(key=flag_key,default_value=-1)
|
||||
integer_result = open_feature_client.get_integer_value(key=flag_key,default_value=-1)
|
||||
float_result = open_feature_client.get_float_value(key=flag_key,default_value=-1)
|
||||
string_result = open_feature_client.get_string_value(key=flag_key,default_value="")
|
||||
object_result = open_feature_client.get_object_value(key=flag_key,default_value={})
|
||||
```
|
||||
|
||||
Each provider class may have further setup required i.e. secret keys, environment variables etc
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
|
||||
## Installation
|
||||
|
||||
### Add it to your build
|
||||
|
||||
<!---x-release-please-start-version-->
|
||||
|
||||
Pip install
|
||||
|
||||
```bash
|
||||
pip install python-open-feature-sdk==0.0.1
|
||||
pip install openfeature-sdk==0.0.4
|
||||
```
|
||||
|
||||
requirements.txt
|
||||
|
||||
```bash
|
||||
python-open-feature-sdk==0.0.1
|
||||
openfeature-sdk==0.0.4
|
||||
```
|
||||
|
||||
```python
|
||||
pip install requirements.txt
|
||||
```
|
||||
|
||||
<!---x-release-please-end-->
|
||||
|
||||
### Configure it
|
||||
|
||||
In order to use the sdk there is some minor configuration. Follow the script below:
|
||||
|
||||
```python
|
||||
|
|
@ -54,6 +69,7 @@ open_feature_client = open_feature_api.get_client()
|
|||
```
|
||||
|
||||
## Contacting us
|
||||
|
||||
We hold regular meetings which you can see [here](https://github.com/open-feature/community/#meetings-and-events).
|
||||
|
||||
We are also present on the `#openfeature` channel in the [CNCF slack](https://slack.cncf.io/).
|
||||
|
|
@ -66,5 +82,4 @@ Thanks so much to our contributors.
|
|||
<img src="https://contrib.rocks/image?repo=open-feature/python-sdk" />
|
||||
</a>
|
||||
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"bootstrap-sha": "198336b098f167f858675235214cc907ede10182",
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "python",
|
||||
"monorepo-tags": false,
|
||||
"include-component-in-tag": false,
|
||||
"prerelease": false,
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"extra-files": [
|
||||
"README.md"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -102,3 +102,4 @@ wrapt==1.14.1
|
|||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
||||
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
||||
|
|
|
|||
|
|
@ -13,37 +13,47 @@ def test_should_return_no_op_provider_metadata():
|
|||
assert metadata.is_default_provider
|
||||
|
||||
|
||||
def test_should_get_boolean_flag_from_no_op():
|
||||
def test_should_resolve_boolean_flag_from_no_op():
|
||||
# Given
|
||||
# When
|
||||
flag = NoOpProvider().get_boolean_details(flag_key="Key", default_value=True)
|
||||
flag = NoOpProvider().resolve_boolean_details(flag_key="Key", default_value=True)
|
||||
# Then
|
||||
assert flag is not None
|
||||
assert flag.value
|
||||
assert isinstance(flag.value, bool)
|
||||
|
||||
|
||||
def test_should_get_number_flag_from_no_op():
|
||||
def test_should_resolve_integer_flag_from_no_op():
|
||||
# Given
|
||||
# When
|
||||
flag = NoOpProvider().get_number_details(flag_key="Key", default_value=100)
|
||||
flag = NoOpProvider().resolve_integer_details(flag_key="Key", default_value=100)
|
||||
# Then
|
||||
assert flag is not None
|
||||
assert flag.value == 100
|
||||
assert isinstance(flag.value, Number)
|
||||
|
||||
|
||||
def test_should_get_string_flag_from_no_op():
|
||||
def test_should_resolve_float_flag_from_no_op():
|
||||
# Given
|
||||
# When
|
||||
flag = NoOpProvider().get_string_details(flag_key="Key", default_value="String")
|
||||
flag = NoOpProvider().resolve_float_details(flag_key="Key", default_value=10.23)
|
||||
# Then
|
||||
assert flag is not None
|
||||
assert flag.value == 10.23
|
||||
assert isinstance(flag.value, Number)
|
||||
|
||||
|
||||
def test_should_resolve_string_flag_from_no_op():
|
||||
# Given
|
||||
# When
|
||||
flag = NoOpProvider().resolve_string_details(flag_key="Key", default_value="String")
|
||||
# Then
|
||||
assert flag is not None
|
||||
assert flag.value == "String"
|
||||
assert isinstance(flag.value, str)
|
||||
|
||||
|
||||
def test_should_get_object_flag_from_no_op():
|
||||
def test_should_resolve_object_flag_from_no_op():
|
||||
# Given
|
||||
return_value = {
|
||||
"String": "string",
|
||||
|
|
@ -51,7 +61,9 @@ def test_should_get_object_flag_from_no_op():
|
|||
"Boolean": True,
|
||||
}
|
||||
# When
|
||||
flag = NoOpProvider().get_object_details(flag_key="Key", default_value=return_value)
|
||||
flag = NoOpProvider().resolve_object_details(
|
||||
flag_key="Key", default_value=return_value
|
||||
)
|
||||
# Then
|
||||
assert flag is not None
|
||||
assert flag.value == return_value
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from open_feature.exception.error_code import ErrorCode
|
||||
from open_feature.exception.exceptions import GeneralError
|
||||
from open_feature.flag_evaluation.error_code import ErrorCode
|
||||
from open_feature.open_feature_api import get_client, get_provider, set_provider
|
||||
from open_feature.provider.no_op_provider import NoOpProvider
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
from numbers import Number
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from open_feature.exception.error_code import ErrorCode
|
||||
from open_feature.exception.exceptions import OpenFeatureError
|
||||
from open_feature.flag_evaluation.error_code import ErrorCode
|
||||
from open_feature.flag_evaluation.reason import Reason
|
||||
from open_feature.hooks.hook import Hook
|
||||
|
||||
|
|
@ -14,7 +13,8 @@ from open_feature.hooks.hook import Hook
|
|||
(
|
||||
(bool, True, "get_boolean_value"),
|
||||
(str, "String", "get_string_value"),
|
||||
(Number, 100, "get_number_value"),
|
||||
(int, 100, "get_integer_value"),
|
||||
(float, 10.23, "get_float_value"),
|
||||
(
|
||||
dict,
|
||||
{
|
||||
|
|
@ -45,7 +45,8 @@ def test_should_get_flag_value_based_on_method_type(
|
|||
(
|
||||
(bool, True, "get_boolean_details"),
|
||||
(str, "String", "get_string_details"),
|
||||
(Number, 100, "get_number_details"),
|
||||
(int, 100, "get_integer_details"),
|
||||
(float, 10.23, "get_float_details"),
|
||||
(
|
||||
dict,
|
||||
{
|
||||
|
|
@ -107,7 +108,7 @@ def test_should_handle_an_open_feature_exception_thrown_by_a_provider(
|
|||
# Given
|
||||
exception_hook = MagicMock(spec=Hook)
|
||||
exception_hook.after.side_effect = OpenFeatureError(
|
||||
"error_message", ErrorCode.GENERAL
|
||||
ErrorCode.GENERAL, "error_message"
|
||||
)
|
||||
no_op_provider_client.add_hooks([exception_hook])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import pytest
|
||||
|
||||
from open_feature.evaluation_context.evaluation_context import EvaluationContext
|
||||
from open_feature.exception.error_code import ErrorCode
|
||||
from open_feature.exception.exceptions import GeneralError
|
||||
from open_feature.flag_evaluation.error_code import ErrorCode
|
||||
from open_feature.open_feature_evaluation_context import (
|
||||
api_evaluation_context,
|
||||
set_api_evaluation_context,
|
||||
|
|
|
|||
Loading…
Reference in New Issue