Merge pull request 'added optional semphore' (#5) from aioweb-migration into main

Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
TPV committed 2026-07-21 03:05:50 +00:00
commit 9af7e7adcf
2 files changed
+33 -7

No files matched your search

+10 -7
View File
@@ -1,6 +1,7 @@
import asyncio import asyncio
import logging import logging
from collections.abc import Awaitable, Callable, Iterable from collections.abc import Awaitable, Callable, Iterable
from contextlib import nullcontext
import aiohttp import aiohttp
from aioproxies import AioProxies from aioproxies import AioProxies
@@ -305,7 +306,7 @@ async def execute_pagination(
func: Callable[..., Awaitable[None]], func: Callable[..., Awaitable[None]],
session: aiohttp.ClientSession | AsyncSession, session: aiohttp.ClientSession | AsyncSession,
*, *,
semaphore: asyncio.Semaphore, semaphore: asyncio.Semaphore | None = None,
start_offset: int = 0, start_offset: int = 0,
pagination_mode: str = "offset", pagination_mode: str = "offset",
**kwargs, **kwargs,
@@ -323,10 +324,12 @@ async def execute_pagination(
func: Awaitable task invoked per page; receives session plus either func: Awaitable task invoked per page; receives session plus either
offset or page_number depending on pagination_mode. offset or page_number depending on pagination_mode.
session: HTTP session forwarded to each func call. session: HTTP session forwarded to each func call.
semaphore: Required bound on concurrent page fetches. Pass one shared semaphore: Optional bound on concurrent page fetches. Omit it to let a
semaphore across all categories so the limit caps total fan-out; a monitor with few pages fan out unbounded; pass one to cap monitors
per-call semaphore would instead bound each category in isolation, with many pages. Pass a single shared semaphore across all categories
making the effective limit this value times the category count. so the limit caps total fan-out; a per-call semaphore would instead
bound each category in isolation, making the effective limit this
value times the category count.
start_offset: Additive base applied to every offset, for APIs whose start_offset: Additive base applied to every offset, for APIs whose
offset space does not begin at zero. Page N requests offset offset space does not begin at zero. Page N requests offset
start_offset + (N - 1) * products_per_page. start_offset + (N - 1) * products_per_page.
@@ -347,8 +350,8 @@ async def execute_pagination(
page_range = range(2, total_pages + 1) page_range = range(2, total_pages + 1)
async def fetch_page(page_index: int): async def fetch_page(page_index: int):
"""Fetch a single page or offset under the shared semaphore.""" """Fetch a single page or offset, optionally under the shared semaphore."""
async with semaphore: async with (semaphore or nullcontext()):
if stop_event.is_set(): if stop_event.is_set():
return return
+23
View File
@@ -82,3 +82,26 @@ def test_single_call_respects_its_semaphore():
asyncio.run(run()) asyncio.run(run())
assert state["peak"] <= limit, f"peak {state['peak']} over limit {limit}" assert state["peak"] <= limit, f"peak {state['peak']} over limit {limit}"
def test_omitted_semaphore_runs_unbounded():
"""Omitting the semaphore lets all pages fan out concurrently.
With no semaphore the only bound is the page count; pages 2..10 (nine pages)
should all be in flight at once, so peak concurrency reaches nine.
"""
state = {"live": 0, "peak": 0}
func = _make_tracking_func(state)
async def run():
await execute_pagination(
"cat",
total_products=300,
products_per_page=30,
func=func,
session=None,
)
asyncio.run(run())
assert state["peak"] == 9, f"expected all 9 pages concurrent, got {state['peak']}"