From dd94a8562a486e29c69b656414862493977985e2 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 18 Aug 2025 18:35:17 +0100 Subject: [PATCH 01/11] feat(v7): JSONPath support --- flag_engine/segments/evaluator.py | 60 +++++++++++++++++++------------ flag_engine/segments/utils.py | 7 ++++ requirements.in | 3 +- requirements.txt | 29 ++++++++------- setup.py | 1 + 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/flag_engine/segments/evaluator.py b/flag_engine/segments/evaluator.py index f277bb3a..49c0669b 100644 --- a/flag_engine/segments/evaluator.py +++ b/flag_engine/segments/evaluator.py @@ -4,8 +4,9 @@ import typing import warnings from contextlib import suppress -from functools import partial, wraps +from functools import lru_cache, wraps +import jsonpath_rfc9535 import semver from flag_engine.context.mappers import map_environment_identity_to_context @@ -23,7 +24,7 @@ from flag_engine.segments import constants from flag_engine.segments.models import SegmentModel from flag_engine.segments.types import ConditionOperator -from flag_engine.segments.utils import get_matching_function +from flag_engine.segments.utils import escape_double_quotes, get_matching_function from flag_engine.utils.hashing import get_hashed_percentage_for_object_ids from flag_engine.utils.semver import is_semver from flag_engine.utils.types import SupportsStr, get_casting_function @@ -256,26 +257,11 @@ def context_matches_condition( ) -def _get_trait(context: EvaluationContext, trait_key: str) -> ContextValue: - return ( - identity_context["traits"][trait_key] - if (identity_context := context["identity"]) - else None - ) - - def get_context_value( context: EvaluationContext, property: str, ) -> ContextValue: - getter = CONTEXT_VALUE_GETTERS_BY_PROPERTY.get(property) or partial( - _get_trait, - trait_key=property, - ) - try: - return getter(context) - except KeyError: - return None + return _get_context_value_getter(property)(context) def _matches_context_value( @@ -385,8 +371,36 @@ def inner( } -CONTEXT_VALUE_GETTERS_BY_PROPERTY = { - "$.identity.identifier": lambda context: context["identity"]["identifier"], - "$.identity.key": lambda context: context["identity"]["key"], - "$.environment.name": lambda context: context["environment"]["name"], -} +@lru_cache +def _get_context_value_getter( + property: str, +) -> typing.Callable[[EvaluationContext], ContextValue]: + """ + Get a function to retrieve a context value based on property value, + assumed to be either a JSONPath string or a trait key. + + :param property: The property to retrieve the value for. + :return: A function that takes an EvaluationContext and returns the value. + """ + try: + compiled_query = jsonpath_rfc9535.compile(property) + except jsonpath_rfc9535.JSONPathSyntaxError: + compiled_query = jsonpath_rfc9535.compile( + f'$.identity.traits["{escape_double_quotes(property)}"]', + ) + + def getter(context: EvaluationContext) -> ContextValue: + try: + if result := compiled_query.find_one(context): + return result.value + return None + except jsonpath_rfc9535.JSONPathError: # pragma: no cover + # This is supposed to be unreachable, but if it happens, + # we log a warning and return None. + warnings.warn( + f"Failed to evaluate JSONPath query '{property}' in context: {context}", + RuntimeWarning, + ) + return None + + return getter diff --git a/flag_engine/segments/utils.py b/flag_engine/segments/utils.py index 3dc0ff24..98fdbaea 100644 --- a/flag_engine/segments/utils.py +++ b/flag_engine/segments/utils.py @@ -15,3 +15,10 @@ def get_matching_function( def none(iterable: typing.Iterable[object]) -> bool: return not any(iterable) + + +def escape_double_quotes(value: str) -> str: + """ + Escape double quotes in a string for JSONPath compatibility. + """ + return value.replace('"', '\\"') diff --git a/requirements.in b/requirements.in index cc7f4bab..6c77a6e3 100644 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,6 @@ annotated-types -semver +jsonpath-rfc9535 pydantic pydantic-collections +semver typing_extensions diff --git a/requirements.txt b/requirements.txt index 4fbdcbb8..ed4ebf43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,26 +1,31 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile -# -annotated-types==0.5.0 +# This file was autogenerated by uv via the following command: +# uv pip compile requirements.in --constraints requirements.txt +annotated-types==0.7.0 # via # -r requirements.in # pydantic -pydantic==2.4.0 +iregexp-check==0.1.4 + # via jsonpath-rfc9535 +jsonpath-rfc9535==0.1.5 + # via -r requirements.in +pydantic==2.11.7 # via # -r requirements.in # pydantic-collections -pydantic-collections==0.5.1 +pydantic-collections==0.6.0 # via -r requirements.in -pydantic-core==2.10.0 +pydantic-core==2.33.2 # via pydantic -semver==3.0.1 +regex==2025.7.34 + # via jsonpath-rfc9535 +semver==3.0.4 # via -r requirements.in -typing-extensions==4.8.0 +typing-extensions==4.14.1 # via # -r requirements.in # pydantic # pydantic-collections # pydantic-core + # typing-inspection +typing-inspection==0.4.1 + # via pydantic diff --git a/setup.py b/setup.py index 709ce28b..6de8a90b 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ long_description=open("README.md").read(), long_description_content_type="text/markdown", install_requires=[ + "jsonpath-rfc9535>=0.1.5,<1", "pydantic>=2.3.0,<3", "pydantic-collections>=0.5.1,<1", "semver>=3.0.1", From 5f4dd3bc9b96af0d51cc2720337e1a1fa99c5eca Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 18 Aug 2025 19:10:00 +0100 Subject: [PATCH 02/11] fix typing --- flag_engine/segments/evaluator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flag_engine/segments/evaluator.py b/flag_engine/segments/evaluator.py index 49c0669b..8e75a41c 100644 --- a/flag_engine/segments/evaluator.py +++ b/flag_engine/segments/evaluator.py @@ -6,7 +6,7 @@ from contextlib import suppress from functools import lru_cache, wraps -import jsonpath_rfc9535 +import jsonpath_rfc9535 # type: ignore[import-untyped] import semver from flag_engine.context.mappers import map_environment_identity_to_context From f6faf9f69547f31800a09ba64344bfaa16ae17f8 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Mon, 18 Aug 2025 19:30:01 +0100 Subject: [PATCH 03/11] fix typing, add type guard --- flag_engine/identities/traits/types.py | 8 ++++++++ flag_engine/segments/evaluator.py | 7 ++++--- .../unit/segments/test_segments_evaluator.py | 20 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/flag_engine/identities/traits/types.py b/flag_engine/identities/traits/types.py index c4a7afa9..6d49e08a 100644 --- a/flag_engine/identities/traits/types.py +++ b/flag_engine/identities/traits/types.py @@ -11,6 +11,14 @@ _UnconstrainedContextValue = Union[None, int, float, bool, str] +def is_trait_value(value: Any) -> TypeGuard[_UnconstrainedContextValue]: + """ + Check if the value is a valid trait value type. + This function is used to determine if a value can be treated as a trait value. + """ + return isinstance(value, get_args(_UnconstrainedContextValue)) + + def map_any_value_to_trait_value(value: Any) -> _UnconstrainedContextValue: """ Try to coerce a value of arbitrary type to a trait value type. diff --git a/flag_engine/segments/evaluator.py b/flag_engine/segments/evaluator.py index 8e75a41c..3962bb65 100644 --- a/flag_engine/segments/evaluator.py +++ b/flag_engine/segments/evaluator.py @@ -6,7 +6,7 @@ from contextlib import suppress from functools import lru_cache, wraps -import jsonpath_rfc9535 # type: ignore[import-untyped] +import jsonpath_rfc9535 # type: ignore[import] import semver from flag_engine.context.mappers import map_environment_identity_to_context @@ -19,7 +19,7 @@ ) from flag_engine.environments.models import EnvironmentModel from flag_engine.identities.models import IdentityModel -from flag_engine.identities.traits.types import ContextValue +from flag_engine.identities.traits.types import ContextValue, is_trait_value from flag_engine.result.types import EvaluationResult, FlagResult, SegmentResult from flag_engine.segments import constants from flag_engine.segments.models import SegmentModel @@ -392,7 +392,8 @@ def _get_context_value_getter( def getter(context: EvaluationContext) -> ContextValue: try: if result := compiled_query.find_one(context): - return result.value + if is_trait_value(value := result.value): + return value return None except jsonpath_rfc9535.JSONPathError: # pragma: no cover # This is supposed to be unreachable, but if it happens, diff --git a/tests/unit/segments/test_segments_evaluator.py b/tests/unit/segments/test_segments_evaluator.py index 32ec15fc..bb7c9eef 100644 --- a/tests/unit/segments/test_segments_evaluator.py +++ b/tests/unit/segments/test_segments_evaluator.py @@ -21,6 +21,7 @@ _matches_context_value, context_matches_condition, get_context_segments, + get_context_value, get_evaluation_result, get_flag_result_from_feature_context, get_identity_segments, @@ -1028,3 +1029,22 @@ def test_get_flag_result_from_feature_context__call_return_expected( expected_key, ] ) + + +@pytest.mark.parametrize( + "property", + [ + pytest.param("$.identity", id="jsonpath returns an object"), + 'trait key "with quotes"', + "$.leads.to.nowhere", + ], +) +def test_get_context_value__invalid_jsonpath__returns_expected( + context: EvaluationContext, + property: str, +) -> None: + # Given & When + result = get_context_value(context, property) + + # Then + assert result is None From 529d509d0c77d7652272d9e340fbbb260722c7a5 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 12:19:08 +0100 Subject: [PATCH 04/11] support python 3.8 --- requirements.txt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index ed4ebf43..d935f8a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,31 +1,29 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements.in --constraints requirements.txt +# uv pip compile requirements.in --constraints requirements.txt --python-version 3.8 annotated-types==0.7.0 # via # -r requirements.in # pydantic iregexp-check==0.1.4 # via jsonpath-rfc9535 -jsonpath-rfc9535==0.1.5 +jsonpath-rfc9535==0.1.6 # via -r requirements.in -pydantic==2.11.7 +pydantic==2.10.6 # via # -r requirements.in # pydantic-collections pydantic-collections==0.6.0 # via -r requirements.in -pydantic-core==2.33.2 +pydantic-core==2.27.2 # via pydantic -regex==2025.7.34 +regex==2024.11.6 # via jsonpath-rfc9535 semver==3.0.4 # via -r requirements.in -typing-extensions==4.14.1 +typing-extensions==4.13.2 # via # -r requirements.in + # annotated-types # pydantic # pydantic-collections # pydantic-core - # typing-inspection -typing-inspection==0.4.1 - # via pydantic From e8bbdef0818d0eccae65cd2f8274034e66ce829a Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 12:37:18 +0100 Subject: [PATCH 05/11] update requirements-dev.txt --- requirements-dev-3.8.txt | 8 ++-- requirements-dev.txt | 83 ++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 55 deletions(-) diff --git a/requirements-dev-3.8.txt b/requirements-dev-3.8.txt index d7148008..09d04b0c 100644 --- a/requirements-dev-3.8.txt +++ b/requirements-dev-3.8.txt @@ -2,7 +2,7 @@ # uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.8 absolufy-imports==0.3.1 # via -r requirements-dev.in -annotated-types==0.5.0 +annotated-types==0.7.0 # via # -c requirements.txt # pydantic @@ -74,11 +74,11 @@ pycodestyle==2.9.1 # via flake8 pycparser==2.22 # via cffi -pydantic==2.4.0 +pydantic==2.10.6 # via # -c requirements.txt # datamodel-code-generator -pydantic-core==2.10.0 +pydantic-core==2.27.2 # via # -c requirements.txt # pydantic @@ -118,7 +118,7 @@ tomli==2.2.1 # pytest types-setuptools==75.8.0.20250110 # via -r requirements-dev.in -typing-extensions==4.8.0 +typing-extensions==4.13.2 # via # -c requirements.txt # annotated-types diff --git a/requirements-dev.txt b/requirements-dev.txt index cd8a805d..09d04b0c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,18 +1,18 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.9 +# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.8 absolufy-imports==0.3.1 # via -r requirements-dev.in -annotated-types==0.5.0 +annotated-types==0.7.0 # via # -c requirements.txt # pydantic argcomplete==3.6.2 # via datamodel-code-generator -black==25.1.0 +black==24.8.0 # via # -r requirements-dev.in # datamodel-code-generator -build==1.3.0 +build==1.2.2.post1 # via pip-tools cffi==1.17.1 # via pytest-codspeed @@ -20,42 +20,35 @@ click==8.1.8 # via # black # pip-tools -coverage==7.10.4 +coverage==7.6.1 # via pytest-cov -datamodel-code-generator==0.33.0 +datamodel-code-generator==0.27.3 # via -r requirements-dev.in exceptiongroup==1.3.0 # via pytest -flake8==7.3.0 +filelock==3.16.1 + # via pytest-codspeed +flake8==5.0.4 # via -r requirements-dev.in genson==1.3.0 # via datamodel-code-generator -importlib-metadata==8.7.0 - # via - # build - # pytest-codspeed - # typeguard -inflect==7.5.0 +importlib-metadata==8.5.0 + # via build +inflect==5.6.2 # via datamodel-code-generator iniconfig==2.1.0 # via pytest -isort==6.0.1 +isort==5.13.2 # via # -r requirements-dev.in # datamodel-code-generator jinja2==3.1.6 # via datamodel-code-generator -markdown-it-py==3.0.0 - # via rich -markupsafe==3.0.2 +markupsafe==2.1.5 # via jinja2 mccabe==0.7.0 # via flake8 -mdurl==0.1.2 - # via markdown-it-py -more-itertools==10.7.0 - # via inflect -mypy==1.17.1 +mypy==1.14.1 # via -r requirements-dev.in mypy-extensions==1.1.0 # via @@ -68,51 +61,43 @@ packaging==25.0 # datamodel-code-generator # pytest pathspec==0.12.1 - # via - # black - # mypy -pip==25.2 + # via black +pip==25.0.1 # via pip-tools pip-tools==7.5.0 # via -r requirements-dev.in -platformdirs==4.3.8 +platformdirs==4.3.6 # via black -pluggy==1.6.0 - # via - # pytest - # pytest-cov -pycodestyle==2.14.0 +pluggy==1.5.0 + # via pytest +pycodestyle==2.9.1 # via flake8 pycparser==2.22 # via cffi -pydantic==2.4.0 +pydantic==2.10.6 # via # -c requirements.txt # datamodel-code-generator -pydantic-core==2.10.0 +pydantic-core==2.27.2 # via # -c requirements.txt # pydantic -pyflakes==3.4.0 +pyflakes==2.5.0 # via flake8 -pygments==2.19.2 - # via - # pytest - # rich pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.4.1 +pytest==8.3.5 # via # -r requirements-dev.in # pytest-codspeed # pytest-cov # pytest-lazy-fixtures # pytest-mock -pytest-codspeed==4.0.0 +pytest-codspeed==2.2.1 # via -r requirements-dev.in -pytest-cov==6.2.1 +pytest-cov==5.0.0 # via -r requirements-dev.in pytest-lazy-fixtures==1.3.4 # via -r requirements-dev.in @@ -120,9 +105,7 @@ pytest-mock==3.14.1 # via -r requirements-dev.in pyyaml==6.0.2 # via datamodel-code-generator -rich==14.1.0 - # via pytest-codspeed -setuptools==80.9.0 +setuptools==75.3.2 # via pip-tools tomli==2.2.1 # via @@ -133,20 +116,18 @@ tomli==2.2.1 # mypy # pip-tools # pytest -typeguard==4.2.0 - # via inflect -types-setuptools==80.9.0.20250809 +types-setuptools==75.8.0.20250110 # via -r requirements-dev.in -typing-extensions==4.8.0 +typing-extensions==4.13.2 # via # -c requirements.txt + # annotated-types # black # exceptiongroup # mypy # pydantic # pydantic-core - # typeguard wheel==0.45.1 # via pip-tools -zipp==3.23.0 +zipp==3.20.2 # via importlib-metadata From b33f15d4ec444db2d78a2538c82500df51652a63 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 12:37:35 +0100 Subject: [PATCH 06/11] fix typing --- flag_engine/segments/evaluator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flag_engine/segments/evaluator.py b/flag_engine/segments/evaluator.py index 3962bb65..4fada072 100644 --- a/flag_engine/segments/evaluator.py +++ b/flag_engine/segments/evaluator.py @@ -390,8 +390,13 @@ def _get_context_value_getter( ) def getter(context: EvaluationContext) -> ContextValue: + if typing.TYPE_CHECKING: # pragma: no cover + # Ugly hack to satisfy mypy :( + data = dict(context) + else: + data = context try: - if result := compiled_query.find_one(context): + if result := compiled_query.find_one(data): if is_trait_value(value := result.value): return value return None From dd2fcdfd846b8fc7f6462e33d42f17f566b174c0 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 12:41:06 +0100 Subject: [PATCH 07/11] fix more typing --- flag_engine/segments/evaluator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flag_engine/segments/evaluator.py b/flag_engine/segments/evaluator.py index 4fada072..bc6be582 100644 --- a/flag_engine/segments/evaluator.py +++ b/flag_engine/segments/evaluator.py @@ -6,7 +6,7 @@ from contextlib import suppress from functools import lru_cache, wraps -import jsonpath_rfc9535 # type: ignore[import] +import jsonpath_rfc9535 import semver from flag_engine.context.mappers import map_environment_identity_to_context From 4fb3b81c9893831e900629bad00a48e7c6021d57 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 12:59:09 +0100 Subject: [PATCH 08/11] try newer flake8 --- requirements-dev.txt | 75 +++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 09d04b0c..c046a62c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.8 +# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.9 absolufy-imports==0.3.1 # via -r requirements-dev.in annotated-types==0.7.0 @@ -8,11 +8,11 @@ annotated-types==0.7.0 # pydantic argcomplete==3.6.2 # via datamodel-code-generator -black==24.8.0 +black==25.1.0 # via # -r requirements-dev.in # datamodel-code-generator -build==1.2.2.post1 +build==1.3.0 # via pip-tools cffi==1.17.1 # via pytest-codspeed @@ -20,35 +20,42 @@ click==8.1.8 # via # black # pip-tools -coverage==7.6.1 +coverage==7.10.4 # via pytest-cov -datamodel-code-generator==0.27.3 +datamodel-code-generator==0.33.0 # via -r requirements-dev.in exceptiongroup==1.3.0 # via pytest -filelock==3.16.1 - # via pytest-codspeed -flake8==5.0.4 +flake8==7.3.0 # via -r requirements-dev.in genson==1.3.0 # via datamodel-code-generator -importlib-metadata==8.5.0 - # via build -inflect==5.6.2 +importlib-metadata==8.7.0 + # via + # build + # pytest-codspeed + # typeguard +inflect==7.5.0 # via datamodel-code-generator iniconfig==2.1.0 # via pytest -isort==5.13.2 +isort==6.0.1 # via # -r requirements-dev.in # datamodel-code-generator jinja2==3.1.6 # via datamodel-code-generator -markupsafe==2.1.5 +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 # via jinja2 mccabe==0.7.0 # via flake8 -mypy==1.14.1 +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.7.0 + # via inflect +mypy==1.17.1 # via -r requirements-dev.in mypy-extensions==1.1.0 # via @@ -61,16 +68,20 @@ packaging==25.0 # datamodel-code-generator # pytest pathspec==0.12.1 - # via black -pip==25.0.1 + # via + # black + # mypy +pip==25.2 # via pip-tools pip-tools==7.5.0 # via -r requirements-dev.in -platformdirs==4.3.6 +platformdirs==4.3.8 # via black -pluggy==1.5.0 - # via pytest -pycodestyle==2.9.1 +pluggy==1.6.0 + # via + # pytest + # pytest-cov +pycodestyle==2.14.0 # via flake8 pycparser==2.22 # via cffi @@ -82,22 +93,26 @@ pydantic-core==2.27.2 # via # -c requirements.txt # pydantic -pyflakes==2.5.0 +pyflakes==3.4.0 # via flake8 +pygments==2.19.2 + # via + # pytest + # rich pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.3.5 +pytest==8.4.1 # via # -r requirements-dev.in # pytest-codspeed # pytest-cov # pytest-lazy-fixtures # pytest-mock -pytest-codspeed==2.2.1 +pytest-codspeed==4.0.0 # via -r requirements-dev.in -pytest-cov==5.0.0 +pytest-cov==6.2.1 # via -r requirements-dev.in pytest-lazy-fixtures==1.3.4 # via -r requirements-dev.in @@ -105,7 +120,9 @@ pytest-mock==3.14.1 # via -r requirements-dev.in pyyaml==6.0.2 # via datamodel-code-generator -setuptools==75.3.2 +rich==14.1.0 + # via pytest-codspeed +setuptools==80.9.0 # via pip-tools tomli==2.2.1 # via @@ -116,18 +133,20 @@ tomli==2.2.1 # mypy # pip-tools # pytest -types-setuptools==75.8.0.20250110 +typeguard==4.4.2 + # via inflect +types-setuptools==80.9.0.20250809 # via -r requirements-dev.in typing-extensions==4.13.2 # via # -c requirements.txt - # annotated-types # black # exceptiongroup # mypy # pydantic # pydantic-core + # typeguard wheel==0.45.1 # via pip-tools -zipp==3.20.2 +zipp==3.23.0 # via importlib-metadata From 3ab45203fc644ad3edceb463156e22e21b59653c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 13:05:08 +0100 Subject: [PATCH 09/11] ugh --- .github/workflows/pull-request.yml | 9 ++-- requirements-dev.txt | 75 +++++++++++------------------- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 5b2be770..eba934aa 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -28,18 +28,17 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install Dependencies - if: ${{ matrix.python-version == '3.8' }} + if: ${{ matrix.python-version != '3.8' }} run: | python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-dev-3.8.txt + pip install -r requirements.txt -r requirements-dev.txt - name: Install Dependencies - if: ${{ matrix.python-version != '3.8' }} + if: ${{ matrix.python-version == '3.8' }} run: | python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-dev.txt + pip install -r requirements.txt -r requirements-dev-3.8.txt - name: Check Formatting run: black --check . diff --git a/requirements-dev.txt b/requirements-dev.txt index c046a62c..09d04b0c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.9 +# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.8 absolufy-imports==0.3.1 # via -r requirements-dev.in annotated-types==0.7.0 @@ -8,11 +8,11 @@ annotated-types==0.7.0 # pydantic argcomplete==3.6.2 # via datamodel-code-generator -black==25.1.0 +black==24.8.0 # via # -r requirements-dev.in # datamodel-code-generator -build==1.3.0 +build==1.2.2.post1 # via pip-tools cffi==1.17.1 # via pytest-codspeed @@ -20,42 +20,35 @@ click==8.1.8 # via # black # pip-tools -coverage==7.10.4 +coverage==7.6.1 # via pytest-cov -datamodel-code-generator==0.33.0 +datamodel-code-generator==0.27.3 # via -r requirements-dev.in exceptiongroup==1.3.0 # via pytest -flake8==7.3.0 +filelock==3.16.1 + # via pytest-codspeed +flake8==5.0.4 # via -r requirements-dev.in genson==1.3.0 # via datamodel-code-generator -importlib-metadata==8.7.0 - # via - # build - # pytest-codspeed - # typeguard -inflect==7.5.0 +importlib-metadata==8.5.0 + # via build +inflect==5.6.2 # via datamodel-code-generator iniconfig==2.1.0 # via pytest -isort==6.0.1 +isort==5.13.2 # via # -r requirements-dev.in # datamodel-code-generator jinja2==3.1.6 # via datamodel-code-generator -markdown-it-py==3.0.0 - # via rich -markupsafe==3.0.2 +markupsafe==2.1.5 # via jinja2 mccabe==0.7.0 # via flake8 -mdurl==0.1.2 - # via markdown-it-py -more-itertools==10.7.0 - # via inflect -mypy==1.17.1 +mypy==1.14.1 # via -r requirements-dev.in mypy-extensions==1.1.0 # via @@ -68,20 +61,16 @@ packaging==25.0 # datamodel-code-generator # pytest pathspec==0.12.1 - # via - # black - # mypy -pip==25.2 + # via black +pip==25.0.1 # via pip-tools pip-tools==7.5.0 # via -r requirements-dev.in -platformdirs==4.3.8 +platformdirs==4.3.6 # via black -pluggy==1.6.0 - # via - # pytest - # pytest-cov -pycodestyle==2.14.0 +pluggy==1.5.0 + # via pytest +pycodestyle==2.9.1 # via flake8 pycparser==2.22 # via cffi @@ -93,26 +82,22 @@ pydantic-core==2.27.2 # via # -c requirements.txt # pydantic -pyflakes==3.4.0 +pyflakes==2.5.0 # via flake8 -pygments==2.19.2 - # via - # pytest - # rich pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.4.1 +pytest==8.3.5 # via # -r requirements-dev.in # pytest-codspeed # pytest-cov # pytest-lazy-fixtures # pytest-mock -pytest-codspeed==4.0.0 +pytest-codspeed==2.2.1 # via -r requirements-dev.in -pytest-cov==6.2.1 +pytest-cov==5.0.0 # via -r requirements-dev.in pytest-lazy-fixtures==1.3.4 # via -r requirements-dev.in @@ -120,9 +105,7 @@ pytest-mock==3.14.1 # via -r requirements-dev.in pyyaml==6.0.2 # via datamodel-code-generator -rich==14.1.0 - # via pytest-codspeed -setuptools==80.9.0 +setuptools==75.3.2 # via pip-tools tomli==2.2.1 # via @@ -133,20 +116,18 @@ tomli==2.2.1 # mypy # pip-tools # pytest -typeguard==4.4.2 - # via inflect -types-setuptools==80.9.0.20250809 +types-setuptools==75.8.0.20250110 # via -r requirements-dev.in typing-extensions==4.13.2 # via # -c requirements.txt + # annotated-types # black # exceptiongroup # mypy # pydantic # pydantic-core - # typeguard wheel==0.45.1 # via pip-tools -zipp==3.23.0 +zipp==3.20.2 # via importlib-metadata From 48ec8537596ce3bdce3b1b867dcad1728a9c7fd3 Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Tue, 19 Aug 2025 19:07:18 +0100 Subject: [PATCH 10/11] flake8 --- requirements-dev.txt | 75 +++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 09d04b0c..c046a62c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.8 +# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.9 absolufy-imports==0.3.1 # via -r requirements-dev.in annotated-types==0.7.0 @@ -8,11 +8,11 @@ annotated-types==0.7.0 # pydantic argcomplete==3.6.2 # via datamodel-code-generator -black==24.8.0 +black==25.1.0 # via # -r requirements-dev.in # datamodel-code-generator -build==1.2.2.post1 +build==1.3.0 # via pip-tools cffi==1.17.1 # via pytest-codspeed @@ -20,35 +20,42 @@ click==8.1.8 # via # black # pip-tools -coverage==7.6.1 +coverage==7.10.4 # via pytest-cov -datamodel-code-generator==0.27.3 +datamodel-code-generator==0.33.0 # via -r requirements-dev.in exceptiongroup==1.3.0 # via pytest -filelock==3.16.1 - # via pytest-codspeed -flake8==5.0.4 +flake8==7.3.0 # via -r requirements-dev.in genson==1.3.0 # via datamodel-code-generator -importlib-metadata==8.5.0 - # via build -inflect==5.6.2 +importlib-metadata==8.7.0 + # via + # build + # pytest-codspeed + # typeguard +inflect==7.5.0 # via datamodel-code-generator iniconfig==2.1.0 # via pytest -isort==5.13.2 +isort==6.0.1 # via # -r requirements-dev.in # datamodel-code-generator jinja2==3.1.6 # via datamodel-code-generator -markupsafe==2.1.5 +markdown-it-py==3.0.0 + # via rich +markupsafe==3.0.2 # via jinja2 mccabe==0.7.0 # via flake8 -mypy==1.14.1 +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.7.0 + # via inflect +mypy==1.17.1 # via -r requirements-dev.in mypy-extensions==1.1.0 # via @@ -61,16 +68,20 @@ packaging==25.0 # datamodel-code-generator # pytest pathspec==0.12.1 - # via black -pip==25.0.1 + # via + # black + # mypy +pip==25.2 # via pip-tools pip-tools==7.5.0 # via -r requirements-dev.in -platformdirs==4.3.6 +platformdirs==4.3.8 # via black -pluggy==1.5.0 - # via pytest -pycodestyle==2.9.1 +pluggy==1.6.0 + # via + # pytest + # pytest-cov +pycodestyle==2.14.0 # via flake8 pycparser==2.22 # via cffi @@ -82,22 +93,26 @@ pydantic-core==2.27.2 # via # -c requirements.txt # pydantic -pyflakes==2.5.0 +pyflakes==3.4.0 # via flake8 +pygments==2.19.2 + # via + # pytest + # rich pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==8.3.5 +pytest==8.4.1 # via # -r requirements-dev.in # pytest-codspeed # pytest-cov # pytest-lazy-fixtures # pytest-mock -pytest-codspeed==2.2.1 +pytest-codspeed==4.0.0 # via -r requirements-dev.in -pytest-cov==5.0.0 +pytest-cov==6.2.1 # via -r requirements-dev.in pytest-lazy-fixtures==1.3.4 # via -r requirements-dev.in @@ -105,7 +120,9 @@ pytest-mock==3.14.1 # via -r requirements-dev.in pyyaml==6.0.2 # via datamodel-code-generator -setuptools==75.3.2 +rich==14.1.0 + # via pytest-codspeed +setuptools==80.9.0 # via pip-tools tomli==2.2.1 # via @@ -116,18 +133,20 @@ tomli==2.2.1 # mypy # pip-tools # pytest -types-setuptools==75.8.0.20250110 +typeguard==4.4.2 + # via inflect +types-setuptools==80.9.0.20250809 # via -r requirements-dev.in typing-extensions==4.13.2 # via # -c requirements.txt - # annotated-types # black # exceptiongroup # mypy # pydantic # pydantic-core + # typeguard wheel==0.45.1 # via pip-tools -zipp==3.20.2 +zipp==3.23.0 # via importlib-metadata From bfe1065ac26422cae97464d070b2a4565cebb49c Mon Sep 17 00:00:00 2001 From: Kim Gustyr Date: Wed, 20 Aug 2025 13:03:41 +0100 Subject: [PATCH 11/11] optimise for traits --- flag_engine/segments/evaluator.py | 9 ++++++++- tests/unit/segments/test_segments_evaluator.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/flag_engine/segments/evaluator.py b/flag_engine/segments/evaluator.py index bc6be582..e9edc98a 100644 --- a/flag_engine/segments/evaluator.py +++ b/flag_engine/segments/evaluator.py @@ -261,7 +261,12 @@ def get_context_value( context: EvaluationContext, property: str, ) -> ContextValue: - return _get_context_value_getter(property)(context) + if property.startswith("$."): + return _get_context_value_getter(property)(context) + if identity_context := context.get("identity"): + if traits := identity_context.get("traits"): + return traits.get(property) + return None def _matches_context_value( @@ -385,6 +390,8 @@ def _get_context_value_getter( try: compiled_query = jsonpath_rfc9535.compile(property) except jsonpath_rfc9535.JSONPathSyntaxError: + # This covers a rare case when a trait starting with "$.", + # but not a valid JSONPath, is used. compiled_query = jsonpath_rfc9535.compile( f'$.identity.traits["{escape_double_quotes(property)}"]', ) diff --git a/tests/unit/segments/test_segments_evaluator.py b/tests/unit/segments/test_segments_evaluator.py index bb7c9eef..9cb7bfb5 100644 --- a/tests/unit/segments/test_segments_evaluator.py +++ b/tests/unit/segments/test_segments_evaluator.py @@ -1048,3 +1048,19 @@ def test_get_context_value__invalid_jsonpath__returns_expected( # Then assert result is None + + +def test_get_context_value__jsonpath_like_trait__returns_expected( + context: EvaluationContext, +) -> None: + # Given + jsonpath_like_trait = '$. i am not" a valid jsonpath' + expected_result = "some_value" + assert context["identity"] + context["identity"]["traits"][jsonpath_like_trait] = expected_result + + # When + result = get_context_value(context, jsonpath_like_trait) + + # Then + assert result == expected_result