diff --git a/.dagger/uv.lock b/.dagger/uv.lock index e99a3a3..16f4860 100644 --- a/.dagger/uv.lock +++ b/.dagger/uv.lock @@ -4,14 +4,15 @@ requires-python = ">=3.13" [[package]] name = "anyio" -version = "4.13.0" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -157,20 +158,6 @@ requires-dist = [ { name = "yarl", specifier = "!=1.24.1" }, ] -[package.metadata.requires-dev] -dev = [ - { name = "aiohttp", specifier = ">=3.9.3" }, - { name = "codegen", editable = "sdk/codegen" }, - { name = "mypy", specifier = ">=1.8.0" }, - { name = "pytest", specifier = ">=8.0.2" }, - { name = "pytest-httpx", specifier = ">=0.30.0" }, - { name = "pytest-mock", specifier = ">=3.12.0" }, - { name = "pytest-subprocess", specifier = ">=1.5.0" }, - { name = "ruff", specifier = ">=0.3.4" }, - { name = "sphinx", specifier = ">=7.2.6" }, - { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, -] - [[package]] name = "exceptiongroup" version = "1.3.1" @@ -647,11 +634,11 @@ requires-dist = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] diff --git a/github/src/github/status_monitor/api.py b/github/src/github/status_monitor/api.py index d39f22c..3257800 100644 --- a/github/src/github/status_monitor/api.py +++ b/github/src/github/status_monitor/api.py @@ -1,7 +1,7 @@ """GitHub commit-status and check-run fetching.""" import asyncio -from collections.abc import AsyncIterator, Mapping +from collections.abc import AsyncIterator, Iterable, Mapping import httpx @@ -95,17 +95,26 @@ async def poll_snapshots( statuses_url: str | None, check_runs_url: str | None, interval: int, + status_names: Iterable[str] = (), + check_run_names: Iterable[str] = (), ) -> AsyncIterator[Mapping[str, Status]]: """Yield successive merged snapshots from both commit statuses and check runs. The first snapshot is yielded immediately; the sleep happens after each yield, so a consumer that breaks out of the loop never sleeps unnecessarily. """ + collisions = set(status_names) & set(check_run_names) while True: merged: dict[str, Status] = {} if statuses_url: - merged.update(await fetch_statuses_snapshot(client, statuses_url)) + statuses = await fetch_statuses_snapshot(client, statuses_url) + for name, status in statuses.items(): + key = f"status:{name}" if name in collisions else name + merged[key] = status if check_runs_url: - merged.update(await fetch_check_runs_snapshot(client, check_runs_url)) + check_runs = await fetch_check_runs_snapshot(client, check_runs_url) + for name, status in check_runs.items(): + key = f"check_run:{name}" if name in collisions else name + merged[key] = status yield merged await asyncio.sleep(interval) diff --git a/github/src/github/status_monitor/main.py b/github/src/github/status_monitor/main.py index 5150798..9015056 100644 --- a/github/src/github/status_monitor/main.py +++ b/github/src/github/status_monitor/main.py @@ -128,7 +128,10 @@ async def _wait( ) -> str: console = Console(force_terminal=True) - expected = sorted(set(status_names + check_run_names)) + collisions = set(status_names) & set(check_run_names) + expected_statuses = [f"status:{name}" if name in collisions else name for name in status_names] + expected_check_runs = [f"check_run:{name}" if name in collisions else name for name in check_run_names] + expected = sorted(set(expected_statuses + expected_check_runs)) if not expected: render.empty(console) return "no checks to wait for" @@ -155,6 +158,8 @@ async def _wait( statuses_url=s_url, check_runs_url=cr_url, interval=poll_interval, + status_names=status_names, + check_run_names=check_run_names, ): match watcher.step(snapshot): case Step.SUCCEEDED: diff --git a/github/tests/test_github.py b/github/tests/test_github.py index 511d9a5..643c519 100644 --- a/github/tests/test_github.py +++ b/github/tests/test_github.py @@ -3,7 +3,7 @@ import asyncio import pytest -from github.status_monitor.api import _check_run_state, fetch_check_runs_snapshot +from github.status_monitor.api import _check_run_state, fetch_check_runs_snapshot, poll_snapshots from github.status_monitor.types import Status # ---- _check_run_state mapping ---- @@ -88,3 +88,61 @@ def test_fetch_check_runs_snapshot_keeps_first_per_name(): client = FakeClient([page]) result = asyncio.run(fetch_check_runs_snapshot(client, "https://api.github.com/test")) assert result == {"a": Status(state="success")} + + +def test_poll_snapshots_distinct_names_remain_unmodified(): + class MultiEndpointClient: + async def get(self, url, params=None): + if "statuses" in str(url): + return FakeResponse([{"context": "legacy-build", "state": "success"}]) + elif "check-runs" in str(url): + return FakeResponse( + {"check_runs": [{"name": "actions-build", "status": "completed", "conclusion": "success"}]} + ) + return FakeResponse({}) + + async def _run(): + client = MultiEndpointClient() + async for snapshot in poll_snapshots( + client, + statuses_url="https://api.github.com/statuses", + check_runs_url="https://api.github.com/check-runs", + interval=0, + status_names=["legacy-build"], + check_run_names=["actions-build"], + ): + assert snapshot == { + "legacy-build": Status(state="success"), + "actions-build": Status(state="success"), + } + break + + asyncio.run(_run()) + + +def test_poll_snapshots_disambiguates_colliding_status_and_check_run_names(): + class CollidingClient: + async def get(self, url, params=None): + if "statuses" in str(url): + return FakeResponse([{"context": "build", "state": "failure"}]) + elif "check-runs" in str(url): + return FakeResponse({"check_runs": [{"name": "build", "status": "completed", "conclusion": "success"}]}) + return FakeResponse({}) + + async def _run(): + client = CollidingClient() + async for snapshot in poll_snapshots( + client, + statuses_url="https://api.github.com/statuses", + check_runs_url="https://api.github.com/check-runs", + interval=0, + status_names=["build"], + check_run_names=["build"], + ): + assert snapshot == { + "status:build": Status(state="failure"), + "check_run:build": Status(state="success"), + } + break + + asyncio.run(_run()) diff --git a/github/uv.lock b/github/uv.lock index 83dae45..184f98e 100644 --- a/github/uv.lock +++ b/github/uv.lock @@ -4,14 +4,15 @@ requires-python = ">=3.13" [[package]] name = "anyio" -version = "4.13.0" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -146,6 +147,7 @@ dependencies = [ { name = "platformdirs" }, { name = "rich" }, { name = "typing-extensions" }, + { name = "yarl" }, ] [package.metadata] @@ -162,20 +164,7 @@ requires-dist = [ { name = "platformdirs", specifier = ">=2.6.2" }, { name = "rich", specifier = ">=10.11.0" }, { name = "typing-extensions", specifier = ">=4.13.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "aiohttp", specifier = ">=3.9.3" }, - { name = "codegen", editable = "sdk/codegen" }, - { name = "mypy", specifier = ">=1.8.0" }, - { name = "pytest", specifier = ">=8.0.2" }, - { name = "pytest-httpx", specifier = ">=0.30.0" }, - { name = "pytest-mock", specifier = ">=3.12.0" }, - { name = "pytest-subprocess", specifier = ">=1.5.0" }, - { name = "ruff", specifier = ">=0.3.4" }, - { name = "sphinx", specifier = ">=7.2.6" }, - { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, + { name = "yarl", specifier = "!=1.24.1" }, ] [[package]] @@ -698,11 +687,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] diff --git a/ruff/uv.lock b/ruff/uv.lock index 2a3732d..5129846 100644 --- a/ruff/uv.lock +++ b/ruff/uv.lock @@ -4,14 +4,15 @@ requires-python = ">=3.13" [[package]] name = "anyio" -version = "4.13.0" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -178,20 +179,6 @@ requires-dist = [ { name = "yarl", specifier = "!=1.24.1" }, ] -[package.metadata.requires-dev] -dev = [ - { name = "aiohttp", specifier = ">=3.9.3" }, - { name = "codegen", editable = "sdk/codegen" }, - { name = "mypy", specifier = ">=1.8.0" }, - { name = "pytest", specifier = ">=8.0.2" }, - { name = "pytest-httpx", specifier = ">=0.30.0" }, - { name = "pytest-mock", specifier = ">=3.12.0" }, - { name = "pytest-subprocess", specifier = ">=1.5.0" }, - { name = "ruff", specifier = ">=0.3.4" }, - { name = "sphinx", specifier = ">=7.2.6" }, - { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, -] - [[package]] name = "deepmerge" version = "2.0" @@ -881,11 +868,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] diff --git a/uv/uv.lock b/uv/uv.lock index dca8ca4..0c62462 100644 --- a/uv/uv.lock +++ b/uv/uv.lock @@ -4,14 +4,15 @@ requires-python = ">=3.13" [[package]] name = "anyio" -version = "4.13.0" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -178,20 +179,6 @@ requires-dist = [ { name = "yarl", specifier = "!=1.24.1" }, ] -[package.metadata.requires-dev] -dev = [ - { name = "aiohttp", specifier = ">=3.9.3" }, - { name = "codegen", editable = "sdk/codegen" }, - { name = "mypy", specifier = ">=1.8.0" }, - { name = "pytest", specifier = ">=8.0.2" }, - { name = "pytest-httpx", specifier = ">=0.30.0" }, - { name = "pytest-mock", specifier = ">=3.12.0" }, - { name = "pytest-subprocess", specifier = ">=1.5.0" }, - { name = "ruff", specifier = ">=0.3.4" }, - { name = "sphinx", specifier = ">=7.2.6" }, - { name = "sphinx-rtd-theme", specifier = ">=2.0.0" }, -] - [[package]] name = "deepmerge" version = "2.0" @@ -850,11 +837,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]]