-
Notifications
You must be signed in to change notification settings - Fork 23
fix(flagd): improve targeting and fix fractional issue(#92) #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
beeme1mr
merged 1 commit into
open-feature:main
from
open-feature-forking:feat/91-cancel-fractional-execution
Nov 21, 2024
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...ture-provider-flagd/src/openfeature/contrib/provider/flagd/resolvers/process/targeting.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import time | ||
| import typing | ||
|
|
||
| from json_logic import builtins, jsonLogic # type: ignore[import-untyped] | ||
| from json_logic.types import JsonValue # type: ignore[import-untyped] | ||
|
|
||
| from openfeature.evaluation_context import EvaluationContext | ||
|
|
||
| from .custom_ops import ( | ||
| ends_with, | ||
| fractional, | ||
| sem_ver, | ||
| starts_with, | ||
| ) | ||
|
|
||
| OPERATORS = { | ||
| **builtins.BUILTINS, | ||
| "fractional": fractional, | ||
| "starts_with": starts_with, | ||
| "ends_with": ends_with, | ||
| "sem_ver": sem_ver, | ||
| } | ||
|
|
||
|
|
||
| def targeting( | ||
| key: str, | ||
| targeting: dict, | ||
| evaluation_context: typing.Optional[EvaluationContext] = None, | ||
| ) -> JsonValue: | ||
| json_logic_context = evaluation_context.attributes if evaluation_context else {} | ||
| json_logic_context["$flagd"] = {"flagKey": key, "timestamp": int(time.time())} | ||
| json_logic_context["targetingKey"] = ( | ||
| evaluation_context.targeting_key if evaluation_context else None | ||
| ) | ||
| return jsonLogic(targeting, json_logic_context, OPERATORS) |
250 changes: 250 additions & 0 deletions
250
providers/openfeature-provider-flagd/tests/test_targeting.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| import time | ||
| import unittest | ||
| from math import floor | ||
|
|
||
| import pytest | ||
| from json_logic import builtins, jsonLogic # type: ignore[import-untyped] | ||
|
|
||
| from openfeature.contrib.provider.flagd.resolvers.process.custom_ops import ( | ||
| ends_with, | ||
| fractional, | ||
| sem_ver, | ||
| starts_with, | ||
| ) | ||
| from openfeature.contrib.provider.flagd.resolvers.process.targeting import targeting | ||
| from openfeature.evaluation_context import EvaluationContext | ||
|
|
||
| OPERATORS = { | ||
| **builtins.BUILTINS, | ||
| "fractional": fractional, | ||
| "starts_with": starts_with, | ||
| "ends_with": ends_with, | ||
| "sem_ver": sem_ver, | ||
| } | ||
|
|
||
| flag_key = "flagKey" | ||
|
|
||
|
|
||
| class BasicTests(unittest.TestCase): | ||
| def test_should_inject_flag_key_as_a_property(self): | ||
| rule = {"===": [{"var": "$flagd.flagKey"}, flag_key]} | ||
|
|
||
| result = targeting(flag_key, rule) | ||
|
|
||
| assert result | ||
|
|
||
| def test_should_inject_current_timestamp_as_a_property(self): | ||
| ts = floor(time.time() / 1000) | ||
|
|
||
| rule = {">=": [{"var": "$flagd.timestamp"}, ts]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_override_injected_properties_if_already_present_in_context(self): | ||
| rule = {"===": [{"var": "$flagd.flagKey"}, flag_key]} | ||
|
|
||
| ctx = { | ||
| "$flagd": { | ||
| "flagKey": "someOtherFlag", | ||
| }, | ||
| } | ||
|
|
||
| assert targeting(flag_key, rule, EvaluationContext(attributes=ctx)) | ||
|
|
||
|
|
||
| class StringComparisonOperator(unittest.TestCase): | ||
| def test_should_evaluate_starts_with_calls(self): | ||
| rule = {"starts_with": [{"var": "email"}, "admin"]} | ||
| context = {"email": "[email protected]"} | ||
|
|
||
| assert targeting(flag_key, rule, EvaluationContext(attributes=context)) | ||
|
|
||
| def test_should_evaluate_ends_with_calls(self): | ||
| rule = {"ends_with": [{"var": "email"}, "abc.com"]} | ||
| context = {"email": "[email protected]"} | ||
|
|
||
| assert targeting(flag_key, rule, EvaluationContext(attributes=context)) | ||
|
|
||
| def test_missing_targeting(self): | ||
| rule = {"starts_with": [{"var": "email"}]} | ||
| context = {"email": "[email protected]"} | ||
|
|
||
| assert not targeting(flag_key, rule, EvaluationContext(attributes=context)) | ||
|
|
||
| def test_non_string_variable(self): | ||
| rule = {"ends_with": [{"var": "number"}, "abc.com"]} | ||
| context = {"number": 11111} | ||
|
|
||
| assert not targeting(flag_key, rule, EvaluationContext(attributes=context)) | ||
|
|
||
| def test_non_string_comparator(self): | ||
| rule = {"ends_with": [{"var": "email"}, 111111]} | ||
| context = {"email": "[email protected]"} | ||
|
|
||
| assert not targeting(flag_key, rule, EvaluationContext(attributes=context)) | ||
|
|
||
|
|
||
| @pytest.mark.skip( | ||
| "semvers are not working as expected, 'v' prefix is not valid within current implementation" | ||
| ) | ||
| class SemVerOperator(unittest.TestCase): | ||
| def test_should_support_equal_operator(self): | ||
| rule = {"sem_ver": ["v1.2.3", "=", "1.2.3"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_neq_operator(self): | ||
| rule = {"sem_ver": ["v1.2.3", "!=", "1.2.4"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_lt_operator(self): | ||
| rule = {"sem_ver": ["v1.2.3", "<", "1.2.4"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_lte_operator(self): | ||
| rule = {"sem_ver": ["v1.2.3", "<=", "1.2.3"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_gte_operator(self): | ||
| rule = {"sem_ver": ["v1.2.3", ">=", "1.2.3"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_gt_operator(self): | ||
| rule = {"sem_ver": ["v1.2.4", ">", "1.2.3"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_major_comparison_operator(self): | ||
| rule = {"sem_ver": ["v1.2.3", "^", "v1.0.0"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_support_minor_comparison_operator(self): | ||
| rule = {"sem_ver": ["v5.0.3", "~", "v5.0.8"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_handle_unknown_operator(self): | ||
| rule = {"sem_ver": ["v1.0.0", "-", "v1.0.0"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
| def test_should_handle_invalid_targetings(self): | ||
| rule = {"sem_ver": ["myVersion_1", "=", "myVersion_1"]} | ||
|
|
||
| assert not targeting(flag_key, rule) | ||
|
|
||
| def test_should_validate_targetings(self): | ||
| rule = {"sem_ver": ["myVersion_2", "+", "myVersion_1", "myVersion_1"]} | ||
|
|
||
| assert targeting(flag_key, rule) | ||
|
|
||
|
|
||
| class FractionalOperator(unittest.TestCase): | ||
| def test_should_evaluate_valid_rule(self): | ||
| rule = { | ||
| "fractional": [ | ||
| {"cat": [{"var": "$flagd.flagKey"}, {"var": "key"}]}, | ||
| ["red", 50], | ||
| ["blue", 50], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting( | ||
| "flagA", rule, EvaluationContext(attributes={"key": "bucketKeyA"}) | ||
| ) | ||
| assert logic == "red" | ||
|
|
||
| def test_should_evaluate_valid_rule2(self): | ||
| rule = { | ||
| "fractional": [ | ||
| {"cat": [{"var": "$flagd.flagKey"}, {"var": "key"}]}, | ||
| ["red", 50], | ||
| ["blue", 50], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting( | ||
| "flagA", rule, EvaluationContext(attributes={"key": "bucketKeyB"}) | ||
| ) | ||
| assert logic == "blue" | ||
|
|
||
| def test_should_evaluate_valid_rule_with_targeting_key(self): | ||
| rule = { | ||
| "fractional": [ | ||
| ["red", 50], | ||
| ["blue", 50], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting("flagA", rule, EvaluationContext(targeting_key="bucketKeyB")) | ||
| assert logic == "blue" | ||
|
|
||
| def test_should_evaluate_valid_rule_with_targeting_key_although_one_does_not_have_a_fraction( | ||
| self, | ||
| ): | ||
| rule = { | ||
| "fractional": [["red", 1], ["blue"]], | ||
| } | ||
|
|
||
| logic = targeting("flagA", rule, EvaluationContext(targeting_key="bucketKeyB")) | ||
| assert logic == "blue" | ||
|
|
||
| def test_should_return_null_if_targeting_key_is_missing(self): | ||
| rule = { | ||
| "fractional": [ | ||
| ["red", 1], | ||
| ["blue", 1], | ||
| ], | ||
| } | ||
|
|
||
| logic = jsonLogic(rule, {}, OPERATORS) | ||
| assert logic is None | ||
|
|
||
| def test_bucket_sum_with_sum_bigger_than_100(self): | ||
| rule = { | ||
| "fractional": [ | ||
| ["red", 55], | ||
| ["blue", 55], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting("flagA", rule, EvaluationContext(targeting_key="key")) | ||
| assert logic == "blue" | ||
|
|
||
| def test_bucket_sum_with_sum_lower_than_100(self): | ||
| rule = { | ||
| "fractional": [ | ||
| ["red", 45], | ||
| ["blue", 45], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting("flagA", rule, EvaluationContext(targeting_key="key")) | ||
| assert logic == "blue" | ||
|
|
||
| def test_buckets_properties_to_have_variant_and_fraction(self): | ||
| rule = { | ||
| "fractional": [ | ||
| ["red", 50], | ||
| [100, 50], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting("flagA", rule, EvaluationContext(targeting_key="key")) | ||
| assert logic is None | ||
|
|
||
| def test_buckets_properties_to_have_variant_and_fraction2(self): | ||
| rule = { | ||
| "fractional": [ | ||
| ["red", 45, 1256], | ||
| ["blue", 4, 455], | ||
| ], | ||
| } | ||
|
|
||
| logic = targeting("flagA", rule, EvaluationContext(targeting_key="key")) | ||
| assert logic is None |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.