diff --git a/README.md b/README.md index 8c078fab..40d3602c 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,57 @@ 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. + +```python +class MyProvider(AbstractProvider): + ... + async def resolve_boolean_details_async( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[bool]: + ... + + async def resolve_string_details_async( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[str]: + ... + + async def resolve_integer_details_async( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[int]: + ... + + async def resolve_float_details_async( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[float]: + ... + + async def resolve_object_details_async( + self, + flag_key: str, + default_value: Union[dict, list], + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[Union[dict, list]]: + ... + +``` + + > Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs! ### Develop a hook diff --git a/openfeature/provider/in_memory_provider.py b/openfeature/provider/in_memory_provider.py index 322f4ed6..d64a7735 100644 --- a/openfeature/provider/in_memory_provider.py +++ b/openfeature/provider/in_memory_provider.py @@ -76,6 +76,14 @@ def resolve_boolean_details( ) -> FlagResolutionDetails[bool]: return self._resolve(flag_key, evaluation_context) + async def resolve_boolean_details_async( + self, + flag_key: str, + default_value: bool, + evaluation_context: typing.Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[bool]: + return await self._resolve_async(flag_key, evaluation_context) + def resolve_string_details( self, flag_key: str, @@ -84,6 +92,14 @@ def resolve_string_details( ) -> FlagResolutionDetails[str]: return self._resolve(flag_key, evaluation_context) + async def resolve_string_details_async( + self, + flag_key: str, + default_value: str, + evaluation_context: typing.Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[str]: + return await self._resolve_async(flag_key, evaluation_context) + def resolve_integer_details( self, flag_key: str, @@ -92,6 +108,14 @@ def resolve_integer_details( ) -> FlagResolutionDetails[int]: return self._resolve(flag_key, evaluation_context) + async def resolve_integer_details_async( + self, + flag_key: str, + default_value: int, + evaluation_context: typing.Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[int]: + return await self._resolve_async(flag_key, evaluation_context) + def resolve_float_details( self, flag_key: str, @@ -100,6 +124,14 @@ def resolve_float_details( ) -> FlagResolutionDetails[float]: return self._resolve(flag_key, evaluation_context) + async def resolve_float_details_async( + self, + flag_key: str, + default_value: float, + evaluation_context: typing.Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[float]: + return await self._resolve_async(flag_key, evaluation_context) + def resolve_object_details( self, flag_key: str, @@ -108,6 +140,14 @@ def resolve_object_details( ) -> FlagResolutionDetails[typing.Union[dict, list]]: return self._resolve(flag_key, evaluation_context) + async def resolve_object_details_async( + self, + flag_key: str, + 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) + def _resolve( self, flag_key: str, @@ -117,3 +157,10 @@ def _resolve( if flag is None: raise FlagNotFoundError(f"Flag '{flag_key}' not found") return flag.resolve(evaluation_context) + + async def _resolve_async( + self, + flag_key: str, + evaluation_context: typing.Optional[EvaluationContext], + ) -> FlagResolutionDetails[V]: + return self._resolve(flag_key, evaluation_context) diff --git a/tests/provider/test_in_memory_provider.py b/tests/provider/test_in_memory_provider.py index 66d5239e..f3559363 100644 --- a/tests/provider/test_in_memory_provider.py +++ b/tests/provider/test_in_memory_provider.py @@ -17,16 +17,20 @@ def test_should_return_in_memory_provider_metadata(): assert metadata.name == "In-Memory Provider" -def test_should_handle_unknown_flags_correctly(): +@pytest.mark.asyncio +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) # Then -def test_calls_context_evaluator_if_present(): +@pytest.mark.asyncio +async def test_calls_context_evaluator_if_present(): # Given def context_evaluator(flag: InMemoryFlag, evaluation_context: dict): return FlagResolutionDetails( @@ -44,57 +48,81 @@ def context_evaluator(flag: InMemoryFlag, evaluation_context: dict): } ) # When - flag = provider.resolve_boolean_details(flag_key="Key", default_value=False) + flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=False) + flag_async = await provider.resolve_boolean_details_async( + flag_key="Key", default_value=False + ) # Then - assert flag is not None - assert flag.value is False - assert isinstance(flag.value, bool) - assert flag.reason == Reason.TARGETING_MATCH + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value is False + assert isinstance(flag.value, bool) + assert flag.reason == Reason.TARGETING_MATCH -def test_should_resolve_boolean_flag_from_in_memory(): +@pytest.mark.asyncio +async def test_should_resolve_boolean_flag_from_in_memory(): # Given provider = InMemoryProvider( {"Key": InMemoryFlag("true", {"true": True, "false": False})} ) # When - flag = provider.resolve_boolean_details(flag_key="Key", default_value=False) + flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=False) + flag_async = await provider.resolve_boolean_details_async( + flag_key="Key", default_value=False + ) # Then - assert flag is not None - assert flag.value is True - assert isinstance(flag.value, bool) - assert flag.variant == "true" + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value is True + assert isinstance(flag.value, bool) + assert flag.variant == "true" -def test_should_resolve_integer_flag_from_in_memory(): +@pytest.mark.asyncio +async def test_should_resolve_integer_flag_from_in_memory(): # Given provider = InMemoryProvider( {"Key": InMemoryFlag("hundred", {"zero": 0, "hundred": 100})} ) # When - flag = provider.resolve_integer_details(flag_key="Key", default_value=0) + flag_sync = provider.resolve_integer_details(flag_key="Key", default_value=0) + flag_async = await provider.resolve_integer_details_async( + flag_key="Key", default_value=0 + ) # Then - assert flag is not None - assert flag.value == 100 - assert isinstance(flag.value, Number) - assert flag.variant == "hundred" + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value == 100 + assert isinstance(flag.value, Number) + assert flag.variant == "hundred" -def test_should_resolve_float_flag_from_in_memory(): +@pytest.mark.asyncio +async def test_should_resolve_float_flag_from_in_memory(): # Given provider = InMemoryProvider( {"Key": InMemoryFlag("ten", {"zero": 0.0, "ten": 10.23})} ) # When - flag = provider.resolve_float_details(flag_key="Key", default_value=0.0) + flag_sync = provider.resolve_float_details(flag_key="Key", default_value=0.0) + flag_async = await provider.resolve_float_details_async( + flag_key="Key", default_value=0.0 + ) # Then - assert flag is not None - assert flag.value == 10.23 - assert isinstance(flag.value, Number) - assert flag.variant == "ten" + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value == 10.23 + assert isinstance(flag.value, Number) + assert flag.variant == "ten" -def test_should_resolve_string_flag_from_in_memory(): +@pytest.mark.asyncio +async def test_should_resolve_string_flag_from_in_memory(): # Given provider = InMemoryProvider( { @@ -105,29 +133,39 @@ def test_should_resolve_string_flag_from_in_memory(): } ) # When - flag = provider.resolve_string_details(flag_key="Key", default_value="Default") + flag_sync = provider.resolve_string_details(flag_key="Key", default_value="Default") + flag_async = await provider.resolve_string_details_async( + flag_key="Key", default_value="Default" + ) # Then - assert flag is not None - assert flag.value == "String" - assert isinstance(flag.value, str) - assert flag.variant == "stringVariant" + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value == "String" + assert isinstance(flag.value, str) + assert flag.variant == "stringVariant" -def test_should_resolve_list_flag_from_in_memory(): +@pytest.mark.asyncio +async def test_should_resolve_list_flag_from_in_memory(): # Given provider = InMemoryProvider( {"Key": InMemoryFlag("twoItems", {"empty": [], "twoItems": ["item1", "item2"]})} ) # When - flag = provider.resolve_object_details(flag_key="Key", default_value=[]) + flag_sync = provider.resolve_object_details(flag_key="Key", default_value=[]) + flag_async = provider.resolve_object_details(flag_key="Key", default_value=[]) # Then - assert flag is not None - assert flag.value == ["item1", "item2"] - assert isinstance(flag.value, list) - assert flag.variant == "twoItems" + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value == ["item1", "item2"] + assert isinstance(flag.value, list) + assert flag.variant == "twoItems" -def test_should_resolve_object_flag_from_in_memory(): +@pytest.mark.asyncio +async def test_should_resolve_object_flag_from_in_memory(): # Given return_value = { "String": "string", @@ -138,9 +176,12 @@ def test_should_resolve_object_flag_from_in_memory(): {"Key": InMemoryFlag("obj", {"obj": return_value, "empty": {}})} ) # When - flag = provider.resolve_object_details(flag_key="Key", default_value={}) + flag_sync = provider.resolve_object_details(flag_key="Key", default_value={}) + flag_async = provider.resolve_object_details(flag_key="Key", default_value={}) # Then - assert flag is not None - assert flag.value == return_value - assert isinstance(flag.value, dict) - assert flag.variant == "obj" + assert flag_sync == flag_async + for flag in [flag_sync, flag_async]: + assert flag is not None + assert flag.value == return_value + assert isinstance(flag.value, dict) + assert flag.variant == "obj"