diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6ea49758..5b2be770 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -30,6 +30,13 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Dependencies + if: ${{ matrix.python-version == '3.8' }} + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev-3.8.txt + + - name: Install Dependencies + if: ${{ matrix.python-version != '3.8' }} run: | python -m pip install --upgrade pip pip install -r requirements.txt -r requirements-dev.txt @@ -57,3 +64,10 @@ jobs: minimum_coverage: 100 fail_below_threshold: true show_missing: true + + - name: Run Benchmarks + if: ${{ matrix.python-version == '3.12' }} + uses: CodSpeedHQ/action@v3 + with: + token: ${{ secrets.CODSPEED_TOKEN }} + run: pytest --codspeed diff --git a/flag_engine/identities/models.py b/flag_engine/identities/models.py index 051289f2..3cbf4fec 100644 --- a/flag_engine/identities/models.py +++ b/flag_engine/identities/models.py @@ -47,7 +47,7 @@ class IdentityModel(BaseModel): dashboard_alias: typing.Optional[str] = None - @computed_field # type: ignore[misc] + @computed_field # type: ignore[prop-decorator] @property def composite_key(self) -> str: return self.generate_composite_key(self.environment_api_key, self.identifier) diff --git a/requirements-dev-3.8.txt b/requirements-dev-3.8.txt new file mode 100644 index 00000000..d7148008 --- /dev/null +++ b/requirements-dev-3.8.txt @@ -0,0 +1,133 @@ +# 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 +absolufy-imports==0.3.1 + # via -r requirements-dev.in +annotated-types==0.5.0 + # via + # -c requirements.txt + # pydantic +argcomplete==3.6.2 + # via datamodel-code-generator +black==24.8.0 + # via + # -r requirements-dev.in + # datamodel-code-generator +build==1.2.2.post1 + # via pip-tools +cffi==1.17.1 + # via pytest-codspeed +click==8.1.8 + # via + # black + # pip-tools +coverage==7.6.1 + # via pytest-cov +datamodel-code-generator==0.27.3 + # via -r requirements-dev.in +exceptiongroup==1.3.0 + # via pytest +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.5.0 + # via build +inflect==5.6.2 + # via datamodel-code-generator +iniconfig==2.1.0 + # via pytest +isort==5.13.2 + # via + # -r requirements-dev.in + # datamodel-code-generator +jinja2==3.1.6 + # via datamodel-code-generator +markupsafe==2.1.5 + # via jinja2 +mccabe==0.7.0 + # via flake8 +mypy==1.14.1 + # via -r requirements-dev.in +mypy-extensions==1.1.0 + # via + # black + # mypy +packaging==25.0 + # via + # black + # build + # datamodel-code-generator + # pytest +pathspec==0.12.1 + # via black +pip==25.0.1 + # via pip-tools +pip-tools==7.5.0 + # via -r requirements-dev.in +platformdirs==4.3.6 + # via black +pluggy==1.5.0 + # via pytest +pycodestyle==2.9.1 + # via flake8 +pycparser==2.22 + # via cffi +pydantic==2.4.0 + # via + # -c requirements.txt + # datamodel-code-generator +pydantic-core==2.10.0 + # via + # -c requirements.txt + # pydantic +pyflakes==2.5.0 + # via flake8 +pyproject-hooks==1.2.0 + # via + # build + # pip-tools +pytest==8.3.5 + # via + # -r requirements-dev.in + # pytest-codspeed + # pytest-cov + # pytest-lazy-fixtures + # pytest-mock +pytest-codspeed==2.2.1 + # via -r requirements-dev.in +pytest-cov==5.0.0 + # via -r requirements-dev.in +pytest-lazy-fixtures==1.3.4 + # via -r requirements-dev.in +pytest-mock==3.14.1 + # via -r requirements-dev.in +pyyaml==6.0.2 + # via datamodel-code-generator +setuptools==75.3.2 + # via pip-tools +tomli==2.2.1 + # via + # black + # build + # coverage + # datamodel-code-generator + # mypy + # pip-tools + # pytest +types-setuptools==75.8.0.20250110 + # via -r requirements-dev.in +typing-extensions==4.8.0 + # via + # -c requirements.txt + # annotated-types + # black + # exceptiongroup + # mypy + # pydantic + # pydantic-core +wheel==0.45.1 + # via pip-tools +zipp==3.20.2 + # via importlib-metadata diff --git a/requirements-dev.in b/requirements-dev.in index 38a47bff..9e5d65c9 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -2,11 +2,11 @@ pytest black flake8 isort +pytest-codspeed pytest-mock -pytest-lazy-fixture +pytest-lazy-fixtures pytest-cov pip-tools -types-pytest-lazy-fixture types-setuptools mypy absolufy-imports diff --git a/requirements-dev.txt b/requirements-dev.txt index ce7d4d53..cd8a805d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,100 +1,90 @@ # This file was autogenerated by uv via the following command: -# uv pip compile requirements-dev.in --constraints requirements-dev.txt --constraints requirements.txt -o requirements-dev.txt +# uv pip compile requirements-dev.in --constraints requirements.txt -o requirements-dev.txt --python-version 3.9 absolufy-imports==0.3.1 - # via - # -c requirements-dev.txt - # -r requirements-dev.in + # via -r requirements-dev.in annotated-types==0.5.0 # via # -c requirements.txt # pydantic argcomplete==3.6.2 # via datamodel-code-generator -black==24.3.0 +black==25.1.0 # via - # -c requirements-dev.txt # -r requirements-dev.in # datamodel-code-generator -build==0.10.0 - # via - # -c requirements-dev.txt - # pip-tools -click==8.1.7 +build==1.3.0 + # via pip-tools +cffi==1.17.1 + # via pytest-codspeed +click==8.1.8 # via - # -c requirements-dev.txt # black # pip-tools -coverage==7.3.0 - # via - # -c requirements-dev.txt - # pytest-cov -datamodel-code-generator==0.27.3 +coverage==7.10.4 + # via pytest-cov +datamodel-code-generator==0.33.0 # via -r requirements-dev.in exceptiongroup==1.3.0 # via pytest -flake8==6.1.0 - # via - # -c requirements-dev.txt - # -r requirements-dev.in +flake8==7.3.0 + # via -r requirements-dev.in genson==1.3.0 # via datamodel-code-generator -inflect==5.6.2 - # via datamodel-code-generator -iniconfig==2.0.0 +importlib-metadata==8.7.0 # via - # -c requirements-dev.txt - # pytest -isort==5.12.0 + # build + # pytest-codspeed + # typeguard +inflect==7.5.0 + # via datamodel-code-generator +iniconfig==2.1.0 + # via pytest +isort==6.0.1 # via - # -c requirements-dev.txt # -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 +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 - # -c requirements-dev.txt - # flake8 -mypy==1.5.1 - # via - # -c requirements-dev.txt - # -r requirements-dev.in -mypy-extensions==1.0.0 - # via - # -c requirements-dev.txt # black # mypy -packaging==23.1 +packaging==25.0 # via - # -c requirements-dev.txt # black # build # datamodel-code-generator # pytest -pathspec==0.11.2 +pathspec==0.12.1 # via - # -c requirements-dev.txt # black -pip==25.0.1 + # mypy +pip==25.2 # via pip-tools -pip-tools==7.3.0 - # via - # -c requirements-dev.txt - # -r requirements-dev.in -platformdirs==3.10.0 - # via - # -c requirements-dev.txt - # black -pluggy==1.2.0 +pip-tools==7.5.0 + # via -r requirements-dev.in +platformdirs==4.3.8 + # via black +pluggy==1.6.0 # via - # -c requirements-dev.txt # pytest -pycodestyle==2.11.0 - # via - # -c requirements-dev.txt - # flake8 + # pytest-cov +pycodestyle==2.14.0 + # via flake8 +pycparser==2.22 + # via cffi pydantic==2.4.0 # via # -c requirements.txt @@ -103,36 +93,36 @@ pydantic-core==2.10.0 # via # -c requirements.txt # pydantic -pyflakes==3.1.0 +pyflakes==3.4.0 + # via flake8 +pygments==2.19.2 # via - # -c requirements-dev.txt - # flake8 -pyproject-hooks==1.0.0 + # pytest + # rich +pyproject-hooks==1.2.0 # via - # -c requirements-dev.txt # build -pytest==7.4.0 + # pip-tools +pytest==8.4.1 # via - # -c requirements-dev.txt # -r requirements-dev.in + # pytest-codspeed # pytest-cov - # pytest-lazy-fixture + # pytest-lazy-fixtures # pytest-mock -pytest-cov==4.1.0 - # via - # -c requirements-dev.txt - # -r requirements-dev.in -pytest-lazy-fixture==0.6.3 - # via - # -c requirements-dev.txt - # -r requirements-dev.in -pytest-mock==3.11.1 - # via - # -c requirements-dev.txt - # -r requirements-dev.in +pytest-codspeed==4.0.0 + # via -r requirements-dev.in +pytest-cov==6.2.1 + # via -r requirements-dev.in +pytest-lazy-fixtures==1.3.4 + # via -r requirements-dev.in +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 @@ -142,27 +132,21 @@ tomli==2.2.1 # datamodel-code-generator # mypy # pip-tools - # pyproject-hooks # pytest -types-pytest-lazy-fixture==0.6.3.4 - # via - # -c requirements-dev.txt - # -r requirements-dev.in -types-setuptools==68.2.0.0 - # via - # -c requirements-dev.txt - # -r requirements-dev.in +typeguard==4.2.0 + # via inflect +types-setuptools==80.9.0.20250809 + # via -r requirements-dev.in typing-extensions==4.8.0 # via - # -c requirements-dev.txt # -c requirements.txt - # annotated-types # black # exceptiongroup # mypy # pydantic # pydantic-core -wheel==0.41.2 - # via - # -c requirements-dev.txt - # pip-tools + # typeguard +wheel==0.45.1 + # via pip-tools +zipp==3.23.0 + # via importlib-metadata diff --git a/tests/engine_tests/test_engine.py b/tests/engine_tests/test_engine.py index 4cc2b1d2..9c926b77 100644 --- a/tests/engine_tests/test_engine.py +++ b/tests/engine_tests/test_engine.py @@ -3,10 +3,15 @@ import pytest from pydantic import BaseModel +from pytest_codspeed import ( # type: ignore[import-untyped,unused-ignore] + BenchmarkFixture, +) +from flag_engine.context.mappers import map_environment_identity_to_context from flag_engine.engine import get_identity_feature_states from flag_engine.environments.models import EnvironmentModel from flag_engine.identities.models import IdentityModel +from flag_engine.segments.evaluator import get_evaluation_result MODULE_PATH = Path(__file__).parent.resolve() @@ -51,11 +56,14 @@ def _extract_test_cases( ] +TEST_CASES = _extract_test_cases( + MODULE_PATH / "engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json" +) + + @pytest.mark.parametrize( "environment_model, identity_model, api_response", - _extract_test_cases( - MODULE_PATH / "engine-test-data/data/environment_n9fbf9h3v4fFgH3U3ngWhb.json" - ), + TEST_CASES, ) def test_engine( environment_model: EnvironmentModel, @@ -79,3 +87,21 @@ def test_engine( fs.feature.name: fs.get_value(identity_model.django_id) for fs in engine_response } == {flag["feature"]["name"]: flag["feature_state_value"] for flag in api_flags} + + +@pytest.mark.benchmark +def test_engine_benchmark(benchmark: BenchmarkFixture) -> None: # type: ignore[no-any-unimported,unused-ignore] + contexts = [] + for environment_model, identity_model, _ in TEST_CASES: + contexts.append( + map_environment_identity_to_context( + environment=environment_model, + identity=identity_model, + override_traits=None, + ) + ) + + @benchmark # type: ignore[misc,unused-ignore] + def __() -> None: + for context in contexts: + get_evaluation_result(context) diff --git a/tests/unit/segments/test_segments_evaluator.py b/tests/unit/segments/test_segments_evaluator.py index 9a103793..32ec15fc 100644 --- a/tests/unit/segments/test_segments_evaluator.py +++ b/tests/unit/segments/test_segments_evaluator.py @@ -1,7 +1,7 @@ import typing import pytest -from pytest_lazyfixture import lazy_fixture +from pytest_lazy_fixtures import lf from pytest_mock import MockerFixture import flag_engine.segments.evaluator @@ -367,8 +367,8 @@ def test_context_in_segment_percentage_split__trait_value__calls_expected( @pytest.mark.parametrize( "operator, property_, expected_result", ( - (constants.IS_SET, lazy_fixture("segment_condition_property"), True), - (constants.IS_NOT_SET, lazy_fixture("segment_condition_property"), False), + (constants.IS_SET, lf("segment_condition_property"), True), + (constants.IS_NOT_SET, lf("segment_condition_property"), False), (constants.IS_SET, "random_property", False), (constants.IS_NOT_SET, "random_property", True), ),