From a853984ad7a635f65b27550f3204ec5379d780a3 Mon Sep 17 00:00:00 2001 From: "riseproject-dev[bot]" <330740410+riseproject-dev[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:24:56 +0000 Subject: [PATCH 1/2] oai-statsig-python-core: Add version 0.30.0 Signed-off-by: riseproject-dev[bot] <330740410+riseproject-dev[bot]@users.noreply.github.com> --- docs/packages/oai-statsig-python-core.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/packages/oai-statsig-python-core.yaml b/docs/packages/oai-statsig-python-core.yaml index f63fc8a6f7..6936a927d5 100644 --- a/docs/packages/oai-statsig-python-core.yaml +++ b/docs/packages/oai-statsig-python-core.yaml @@ -9,3 +9,4 @@ versions: - filename: oai_statsig_python_core-0.29.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl sha256: 4013eb4b25041580420e101e7d0f73239d2c22d583a9e9e3d6cdf72ee85e8d09 requires-python: '>=3.10' +- version: 0.30.0 From 401bdde339b37ca3aa39c6e2ce50e08bec2470cd Mon Sep 17 00:00:00 2001 From: Ludovic Henry Date: Thu, 24 Sep 2026 07:10:12 +0000 Subject: [PATCH 2/2] oai-statsig-python-core: carry 0.29.0 patches forward to 0.30.0 The nightly-upgrade bot bumped docs/packages/oai-statsig-python-core.yaml to 0.30.0 but did not copy patches/oai-statsig-python-core/0.29.0/ forward, so the riscv64 build's "Apply patches" step failed instantly with 'No such file or directory' for the missing patches/oai-statsig-python-core/0.30.0/ directory. Verified all three patches (test_data_store.py sync waits, fork_runner.py timeout, statsig.py co_qualname fallback) apply cleanly, unmodified, against the real 0.30.0 sdist from PyPI -- none of the patched files changed between 0.29.0 and 0.30.0. --- ...ackground-specs-syncs-instead-of-ass.patch | 156 ++++++++++++++++++ ...runner.py-a-timeout-a-slow-machine-c.patch | 32 ++++ ..._qualname-only-where-it-exists-CPyth.patch | 51 ++++++ 3 files changed, 239 insertions(+) create mode 100644 patches/oai-statsig-python-core/0.30.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch create mode 100644 patches/oai-statsig-python-core/0.30.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch create mode 100644 patches/oai-statsig-python-core/0.30.0/0003-fix-read-code.co_qualname-only-where-it-exists-CPyth.patch diff --git a/patches/oai-statsig-python-core/0.30.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch b/patches/oai-statsig-python-core/0.30.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch new file mode 100644 index 0000000000..832832f3ec --- /dev/null +++ b/patches/oai-statsig-python-core/0.30.0/0001-tests-wait-for-background-specs-syncs-instead-of-ass.patch @@ -0,0 +1,156 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: wait for background specs syncs instead of assuming a + 1ms interval + +`StatsigOptions` rejects a `specs_sync_interval_ms` below `MIN_SYNC_INTERVAL` +(1000) and drops it back to the default, logging "Invalid +'specs_sync_interval_ms', value must be greater than 1000, received Some(1)". +Both fixtures in this file still ask for 1, so the six polling tests never get +the near-instant background sync they were written against and fail on every +platform -- reproduced here against the published +manylinux_2_17_x86_64 wheel, where 6 of the 7 tests in this file fail. + +Use the smallest accepted interval -- the same 1000 upstream already switched +`test_observability_client.py` to -- and wait for the condition each test is +really about rather than for a fixed duration. The waits return as soon as the +sync lands, so fast hardware pays nothing. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_data_store.py | 46 +++++++++++++++++---------- + 1 file changed, 30 insertions(+), 16 deletions(-) + +diff --git a/statsig-pyo3/tests/test_data_store.py b/statsig-pyo3/tests/test_data_store.py +index 9df3ac7..708c81b 100644 +--- a/statsig-pyo3/tests/test_data_store.py ++++ b/statsig-pyo3/tests/test_data_store.py +@@ -17,6 +17,12 @@ from utils import get_test_data_resource, get_test_data_resource_bytes + + known_lcut = 1767981029384 + ++# StatsigOptions rejects a specs_sync_interval_ms below this and falls back to the ++# default, so the polling tests below have to wait for a real sync to land. ++MIN_SYNC_INTERVAL_MS = 1000 ++SYNC_WAIT_ATTEMPTS = 200 ++SYNC_WAIT_INTERVAL_S = 0.05 ++ + dcs_content = get_test_data_resource("eval_proj_dcs.json") + json_data = json.loads(dcs_content) + eval_proj_protobuf = get_test_data_resource_bytes("eval_proj_dcs.pb.br") +@@ -169,7 +175,7 @@ def statsig_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -193,7 +199,7 @@ def statsig_bytes_setup(httpserver: HTTPServer): + specs_url=httpserver.url_for("/v2/download_config_specs"), + log_event_url=httpserver.url_for("/v1/log_event"), + data_store=data_store, +- specs_sync_interval_ms=1, ++ specs_sync_interval_ms=MIN_SYNC_INTERVAL_MS, + ) + + statsig = Statsig("secret-key", options) +@@ -223,10 +229,10 @@ def test_data_store_usage_get(statsig_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_called_count > 1 + +@@ -240,9 +246,13 @@ def test_data_store_usage_set(statsig_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True +@@ -265,10 +275,10 @@ def test_data_store_usage_get_bytes(statsig_bytes_setup): + assert gate.value == True + assert gate.details.lcut == known_lcut + +- for _ in range(5): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.set_bytes_called_count > 0: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 1 + assert data_store.get_called_count == 0 +@@ -283,10 +293,10 @@ def test_data_store_usage_get_bytes_request_has_since_time_after_initial_poll(st + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + +@@ -308,10 +318,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + gate = statsig.get_feature_gate(user, "test_public") + assert gate.details.reason == "Adapter(DataStore):Recognized" + +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count >= 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + assert data_store.get_bytes_called_count >= 2 + assert data_store.returned_no_update +@@ -325,10 +335,10 @@ def test_data_store_usage_get_bytes_request_checksum_match_returns_no_update(sta + assert request_with_checksum.since_time == data_store.stored_time + + # no second write should occur when server responds with {"has_updates": false} +- for _ in range(100): ++ for _ in range(SYNC_WAIT_ATTEMPTS): + if data_store.get_bytes_called_count > 2: + break +- sleep(0.05) ++ sleep(SYNC_WAIT_INTERVAL_S) + + + +@@ -341,9 +351,13 @@ def test_data_store_usage_set_bytes(statsig_bytes_setup): + + assert data_store.init_called + assert gate.details.reason == "Adapter(DataStore):Recognized" +- sleep(1) + +- gate_after = statsig.get_feature_gate(user, "test_public") ++ for _ in range(SYNC_WAIT_ATTEMPTS): ++ gate_after = statsig.get_feature_gate(user, "test_public") ++ if gate_after.details.lcut == known_lcut + 10: ++ break ++ sleep(SYNC_WAIT_INTERVAL_S) ++ + statsig.flush_events().wait() + + assert gate_after.value == True diff --git a/patches/oai-statsig-python-core/0.30.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch b/patches/oai-statsig-python-core/0.30.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch new file mode 100644 index 0000000000..82afe1f1a0 --- /dev/null +++ b/patches/oai-statsig-python-core/0.30.0/0002-tests-give-fork_runner.py-a-timeout-a-slow-machine-c.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Sun, 20 Sep 2026 22:48:16 +0000 +Subject: [PATCH] tests: give fork_runner.py a timeout a slow machine can meet + +`fork_runner.py` performs 50 full SDK initialisations -- ten iterations, each +followed by four nested forks that initialise, evaluate and shut down -- against +a local mock server. Ten seconds is enough on upstream's x86_64 CI and not on a +riscv64 runner, where the subprocess is SIGTERMed part way through and the test +fails with `assert -15 == 0`. + +`communicate()` returns as soon as the subprocess exits, so a larger budget +costs nothing where the old one was already sufficient. + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree, so this needs a maintainer discussion rather than a drive-by PR] +--- + statsig-pyo3/tests/test_forking.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/statsig-pyo3/tests/test_forking.py b/statsig-pyo3/tests/test_forking.py +index a5f27c4..8d2bd7d 100644 +--- a/statsig-pyo3/tests/test_forking.py ++++ b/statsig-pyo3/tests/test_forking.py +@@ -45,7 +45,7 @@ def test_forking(httpserver: HTTPServer): + env={**os.environ, "RUST_BACKTRACE": "full"}, + ) + try: +- proc.communicate(timeout=10) ++ proc.communicate(timeout=180) + except subprocess.TimeoutExpired: + proc.terminate() + proc.wait() diff --git a/patches/oai-statsig-python-core/0.30.0/0003-fix-read-code.co_qualname-only-where-it-exists-CPyth.patch b/patches/oai-statsig-python-core/0.30.0/0003-fix-read-code.co_qualname-only-where-it-exists-CPyth.patch new file mode 100644 index 0000000000..d5e71400e6 --- /dev/null +++ b/patches/oai-statsig-python-core/0.30.0/0003-fix-read-code.co_qualname-only-where-it-exists-CPyth.patch @@ -0,0 +1,51 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Ludovic Henry +Date: Mon, 21 Sep 2026 00:43:31 +0000 +Subject: [PATCH] fix: read code.co_qualname only where it exists (CPython >= + 3.11) + +`_find_exposure_callsite` reads `frame.f_code.co_qualname` unconditionally, but +that attribute was added in CPython 3.11, while this package declares +`requires-python = ">=3.10"` and ships a single `pyo3/abi3-py310` wheel. On +CPython 3.10 the access raises `AttributeError: 'code' object has no attribute +'co_qualname'`; `ErrorBoundary.wrap` swallows it, prints "Statsig SDK Error +(Python Bindings): _find_exposure_callsite", and returns None, so +`_get_exposure_callsite_metadata` falls back to its "unknown" branch and every +exposure event gets `exposure_source_file`/`exposure_source_function` of +"unknown" and a null `exposure_source_line`. Callsite logging is therefore +silently dead on the oldest interpreter the wheel supports, and the two +`test_exposure_logging.py` callsite tests fail. + +This is not architecture-specific: against upstream's own published +oai_statsig_python_core-0.29.0-cp310-abi3-manylinux_2_17_x86_64 wheel, both +tests fail on x86_64 under CPython 3.10 with the identical assertion and +warning, and both pass under 3.11. Our riscv64 CI sees it because the abi3 +wheel is built and tested with cp310, the lowest supported interpreter. + +Fall back to `co_name`, which every supported version has. For a module-level +function the two are identical; for a method `co_name` loses the enclosing +class prefix, which is a far better result on 3.10 than "unknown". + +Upstream-Status: To upstream [not yet submitted; the release is cut from a non-public tree -- public statsig-server-core stops at 0.23.0, which predates this callsite code entirely -- so this needs a maintainer discussion rather than a drive-by PR] +--- + py_src/statsig_python_core/statsig.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/py_src/statsig_python_core/statsig.py b/py_src/statsig_python_core/statsig.py +index 26b3e7a..d7cf090 100644 +--- a/py_src/statsig_python_core/statsig.py ++++ b/py_src/statsig_python_core/statsig.py +@@ -287,9 +287,11 @@ class Statsig(StatsigBasePy): + while frame is not None: + module_name = frame.f_globals.get("__name__", "") + if not self._is_exposure_callsite_module_ignored(module_name): ++ code = frame.f_code ++ # code.co_qualname is CPython >= 3.11; this package supports 3.10. + return ( +- Path(frame.f_code.co_filename).name, +- frame.f_code.co_qualname, ++ Path(code.co_filename).name, ++ getattr(code, "co_qualname", code.co_name), + frame.f_lineno, + ) + frame = frame.f_back