From 4ceef10e81a36771e21425c7158e5605bf8b8633 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Thu, 29 Oct 2026 17:16:19 -0300 Subject: [PATCH] feat(CI): Set up CodSpeed Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/codspeed.yml | 38 ++++++++++ .gitmodules | 4 ++ Makefile | 9 +++ benchmarks/test_sdk.py | 96 ++++++++++++++++++++++++++ poetry.lock | 122 ++++++++++++++++++++++++++++++--- pyproject.toml | 9 +++ tests/data/sdk-benchmarks | 1 + 7 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/codspeed.yml create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 benchmarks/test_sdk.py create mode 160000 tests/data/sdk-benchmarks diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml new file mode 100644 index 0000000..f465d92 --- /dev/null +++ b/.github/workflows/codspeed.yml @@ -0,0 +1,38 @@ +name: CodSpeed + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +permissions: + contents: read # Check out the repository and its submodules + id-token: write # Authenticate result uploads to CodSpeed via OIDC + +jobs: + benchmarks: + name: Run benchmarks + runs-on: codspeed-macro + + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + submodules: recursive + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: '3.13' + + - name: Install Poetry + run: pip install poetry + + - name: Run benchmarks + uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0 + with: + mode: walltime + run: make benchmark diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..d134e5b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "tests/data/sdk-benchmarks"] + path = tests/data/sdk-benchmarks + url = https://github.com/Flagsmith/sdk-benchmarks.git + branch = 0.0.2 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f26e5d2 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +SDK_BENCHMARKS := tests/data/sdk-benchmarks + +$(SDK_BENCHMARKS)/environments: + git submodule update --init -- $(SDK_BENCHMARKS) + +.PHONY: benchmark +benchmark: | $(SDK_BENCHMARKS)/environments + poetry install --with dev,benchmark + SDK_BENCHMARKS_ENVIRONMENTS_DIR=$(SDK_BENCHMARKS)/environments poetry run pytest benchmarks --codspeed diff --git a/benchmarks/test_sdk.py b/benchmarks/test_sdk.py new file mode 100644 index 0000000..40f4fa8 --- /dev/null +++ b/benchmarks/test_sdk.py @@ -0,0 +1,96 @@ +""" +Benchmarks a locally evaluating SDK client against environment documents tiered by size. +""" + +import os +import typing +from pathlib import Path + +import pytest +import responses +from flag_engine.engine import ContextValue + +from flagsmith import Flagsmith + +API_URL = "http://flagsmith.benchmark/api/v1/" +ENVIRONMENTS_DIR = Path(os.environ["SDK_BENCHMARKS_ENVIRONMENTS_DIR"]) +IDENTITY_OVERRIDES_IDENTIFIER = "identity-override-1" +IDENTIFIER = "user-1" +TRAITS: typing.Mapping[str, ContextValue] = { + "plan": "plan-7", + "age": 30, + "country": "GB", + "version": "2.5.1", + "email": "jane@example.com", + "user_id": 12, + "beta": True, + "score": 95, + "tier": "pro", + "device": "iPhone 15", +} + + +@pytest.fixture(scope="module", params=["small", "medium", "large"]) +def client(request: pytest.FixtureRequest) -> Flagsmith: + """ + Build a locally evaluating client serving the environment document from memory. + """ + environment_document = (ENVIRONMENTS_DIR / f"{request.param}.json").read_bytes() + + with responses.RequestsMock() as mocked_responses: + mocked_responses.get( + f"{API_URL}environment-document/", + body=environment_document, + content_type="application/json", + ) + client = Flagsmith( + environment_key="ser.benchmark", + api_url=API_URL, + enable_local_evaluation=True, + ) + client.environment_data_polling_manager_thread.stop() # Refreshed once, at startup only + client.environment_data_polling_manager_thread.join() + + return client + + +@pytest.mark.benchmark +def test_get_environment_flags(client: Flagsmith) -> None: + """ + Get a feature value from the environment flags. + """ + client.get_environment_flags().get_feature_value("feature_1") + + +@pytest.mark.benchmark +def test_get_identity_flags_with_identity_overrides(client: Flagsmith) -> None: + """ + Check a feature for an identity with identity overrides. + """ + client.get_identity_flags(IDENTITY_OVERRIDES_IDENTIFIER).is_feature_enabled( + "feature_8" + ) + + +@pytest.mark.benchmark +def test_get_identity_flags_no_traits(client: Flagsmith) -> None: + """ + Get a feature value for an identity without traits. + """ + client.get_identity_flags(IDENTIFIER).get_feature_value("feature_1") + + +@pytest.mark.benchmark +def test_get_identity_flags_with_traits(client: Flagsmith) -> None: + """ + Get a feature value for an identity with traits. + """ + client.get_identity_flags(IDENTIFIER, TRAITS).get_feature_value("feature_1") + + +@pytest.mark.benchmark +def test_get_identity_segments_with_traits(client: Flagsmith) -> None: + """ + List the segments of an identity with traits. + """ + client.get_identity_segments(IDENTIFIER, TRAITS) diff --git a/poetry.lock b/poetry.lock index 325dd62..48d1483 100644 --- a/poetry.lock +++ b/poetry.lock @@ -169,7 +169,7 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] +groups = ["benchmark", "dev"] markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, @@ -301,7 +301,7 @@ version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["benchmark", "dev"] markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, @@ -379,7 +379,7 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["benchmark", "dev"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -540,6 +540,42 @@ files = [ {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["benchmark"] +files = [ + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["benchmark"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "mypy" version = "1.20.2" @@ -642,7 +678,7 @@ version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["benchmark", "dev"] files = [ {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, @@ -683,7 +719,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["benchmark", "dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -730,7 +766,7 @@ version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["benchmark", "dev"] files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, @@ -745,7 +781,7 @@ version = "9.0.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["benchmark", "dev"] files = [ {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, @@ -763,6 +799,52 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-codspeed" +version = "5.0.3" +description = "Pytest plugin to create CodSpeed benchmarks" +optional = false +python-versions = ">=3.9" +groups = ["benchmark"] +files = [ + {file = "pytest_codspeed-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:005348ea52ace3ede2e2f595913912ad2564cca7b124211a88dc78a9cb1fca63"}, + {file = "pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbe6a4a00b449b6ba2771f644cbc38bdf55acf5c812e60e5659110e19dd9f510"}, + {file = "pytest_codspeed-5.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac4344f34bbcdd17f6f8c30dbac3da2f80d223dd112e568fd7f7c2cd4cbc693"}, + {file = "pytest_codspeed-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f56d0339cd98d26f6e561987be25bdd2761a5d53d8f73493b1ebe02d0d451093"}, + {file = "pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c682f6645d4eb472f3bd95dbda1805e3af4243610572cb7d6bf94a88e8a0b6c"}, + {file = "pytest_codspeed-5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f852bee785a7a124cb1720b1915670c6742af87747dc4d838f3ffdbd365ce9d9"}, + {file = "pytest_codspeed-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2eeb25fb1ac3f73c4de50e739e78fea396b89782bdb740bf2a7cd2df21f8d4ee"}, + {file = "pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73c5c9d98a3372a42611989ccfa437cce3842431ac6d6b9ab42c4f0e59c070f7"}, + {file = "pytest_codspeed-5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2e0ab65df73e837666d12357280ca50ff6d6ac03ea5266703be518b68170edf"}, + {file = "pytest_codspeed-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6524c57fec279a22ffef6112af404036afc71b4704758ae9f0abda429b8478d4"}, + {file = "pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c383c9121deb58a69f174188e9e4488ffc0daced0ed276abf87747182511901"}, + {file = "pytest_codspeed-5.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4bcdb4b6522738152885ef067e0c8524d5699828d780fb6f464cdb3db44369c"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25464363c7f9b9bd5022e969c0addba616fa40ac9b8f0fc9e030c4538863b32d"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efd43f82ea03ced8488a767ded9473f050791ab7783ea8654107e1e0ac66af40"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:782f9985b6f6b45b8bc20152d206d3a52b56dd088ba81cb70a71f0b39841be9e"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9aa0815b90196f3c20d736ea8691381e97f12bbe8c7d87af10a351e434b452cb"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85505c96a3477c346ec2d2b7dced8478f4c651e2b1666ee102d53a832b511853"}, + {file = "pytest_codspeed-5.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20eba63765be9d1b6cacbbfad84b87d49eb04b357a7045a0899880da181f81e3"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ec9fa6f0af0a9feb0e0bd517fb59ef28f806fbd50c0c6900ac26cbb4d080eba5"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8df77b3409f54f4a268f77f3ff74992fe1d995cdbaf2cecf8ad74d32db217ce7"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5d8695a227ea1c3a41d25db5b3fe720bf1b4808bd38862be811a4efd902c792"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bf4cc4178cbace8f4d2bd240408276bc4da3850ac5fcb5fb5f8a74ab417615bb"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abe793da40f87295d33988673d34f06ea569848b44490b847552cd416816258a"}, + {file = "pytest_codspeed-5.0.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3a9ed38dfa776443b86f4b49a982e8443d0953db4974bd2673d63cc904ae1ad"}, + {file = "pytest_codspeed-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bce0a6ea93a5b19658f713312bb67554c19283ab15b454a1e3e55a13e78130f8"}, + {file = "pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a2097247985f5d915a94b80c5552d10979ca858c859fc3edef1bf2baa5c9b7a"}, + {file = "pytest_codspeed-5.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e192905a2230f9956e6160732f76577836953a4a1fb2b1e7be74e51ac7b2a0"}, + {file = "pytest_codspeed-5.0.3-py3-none-any.whl", hash = "sha256:fe2ea83c924c2250675b75686c3ee456b8cf0208d83d552e182a195fdf467378"}, + {file = "pytest_codspeed-5.0.3.tar.gz", hash = "sha256:91afef90e6a96b013495e4702ef5d6358614a449e71008cdc194ef668778b92f"}, +] + +[package.dependencies] +pytest = ">=3.8" +rich = ">=13.8.1" + +[package.extras] +compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] + [[package]] name = "pytest-cov" version = "4.1.0" @@ -1087,6 +1169,25 @@ urllib3 = ">=1.25.10,<3.0" [package.extras] tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] +[[package]] +name = "rich" +version = "15.0.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.9.0" +groups = ["benchmark"] +files = [ + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "semver" version = "3.0.4" @@ -1116,7 +1217,7 @@ version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["benchmark", "dev"] markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, @@ -1189,11 +1290,12 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main", "benchmark", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] +markers = {benchmark = "python_version == \"3.10\""} [[package]] name = "urllib3" @@ -1235,4 +1337,4 @@ typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\"" [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "079238a62ea14b7bbf4852682e950390da201cd67fecc602c16144faef8b8ad2" +content-hash = "7a1ee8aaa27e20a95880eae1b2322cdfd3ec4a9ed72da08bd4e83461975afbfb" diff --git a/pyproject.toml b/pyproject.toml index 4a24f3f..a9d628d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,15 @@ responses = "^0.24.1" types-requests = "^2.32" pyfakefs = "^5.9.2" +[tool.poetry.group.benchmark] +optional = true + +[tool.poetry.group.benchmark.dependencies] +pytest-codspeed = "^5.0.3" + +[tool.pytest.ini_options] +testpaths = ["tests"] + [tool.mypy] exclude = ["example/*"] diff --git a/tests/data/sdk-benchmarks b/tests/data/sdk-benchmarks new file mode 160000 index 0000000..7da2be4 --- /dev/null +++ b/tests/data/sdk-benchmarks @@ -0,0 +1 @@ +Subproject commit 7da2be42ec7c58a597892e41141d531bd1dd100c