Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
aiter.py348 linesDownload Raw Back to utils
1"""Asynchronous iterator utilities.2 3Adapted from4https://github.com/maxfischer2781/asyncstdlib/blob/master/asyncstdlib/itertools.py5MIT License.6"""7 8from collections import deque9from collections.abc import (10    AsyncGenerator,11    AsyncIterable,12    AsyncIterator,13    Awaitable,14    Callable,15    Iterator,16)17from contextlib import AbstractAsyncContextManager18from types import TracebackType19from typing import (20    Any,21    Generic,22    TypeVar,23    cast,24    overload,25)26 27from typing_extensions import override28 29from langchain_core._api.deprecation import deprecated30 31T = TypeVar("T")32 33_no_default = object()34 35 36# https://github.com/python/cpython/blob/main/Lib/test/test_asyncgen.py#L5437@deprecated(since="1.1.2", removal="2.0.0")38def py_anext(39    iterator: AsyncIterator[T], default: T | Any = _no_default40) -> Awaitable[T | Any | None]:41    """Pure-Python implementation of `anext()` for testing purposes.42 43    Closely matches the builtin `anext()` C implementation.44 45    Can be used to compare the built-in implementation of the inner coroutines machinery46    to C-implementation of `__anext__()` and `send()` or `throw()` on the returned47    generator.48 49    Args:50        iterator: The async iterator to advance.51        default: The value to return if the iterator is exhausted.52 53            If not provided, a `StopAsyncIteration` exception is raised.54 55    Returns:56        The next value from the iterator, or the default value if the iterator is57            exhausted.58 59    Raises:60        TypeError: If the iterator is not an async iterator.61    """62    try:63        __anext__ = cast(64            "Callable[[AsyncIterator[T]], Awaitable[T]]", type(iterator).__anext__65        )66    except AttributeError as e:67        msg = f"{iterator!r} is not an async iterator"68        raise TypeError(msg) from e69 70    if default is _no_default:71        return __anext__(iterator)72 73    async def anext_impl() -> T | Any:74        try:75            # The C code is way more low-level than this, as it implements76            # all methods of the iterator protocol. In this implementation77            # we're relying on higher-level coroutine concepts, but that's78            # exactly what we want -- crosstest pure-Python high-level79            # implementation and low-level C anext() iterators.80            return await __anext__(iterator)81        except StopAsyncIteration:82            return default83 84    return anext_impl()85 86 87class NoLock:88    """Dummy lock that provides the proper interface but no protection."""89 90    async def __aenter__(self) -> None:91        """Do nothing."""92 93    async def __aexit__(94        self,95        exc_type: type[BaseException] | None,96        exc_val: BaseException | None,97        exc_tb: TracebackType | None,98    ) -> bool:99        """Return False, exception not suppressed."""100        return False101 102 103async def tee_peer(104    iterator: AsyncIterator[T],105    # the buffer specific to this peer106    buffer: deque[T],107    # the buffers of all peers, including our own108    peers: list[deque[T]],109    lock: AbstractAsyncContextManager[Any],110) -> AsyncGenerator[T, None]:111    """An individual iterator of a `tee`.112 113    This function is a generator that yields items from the shared iterator114    `iterator`. It buffers items until the least advanced iterator has yielded them as115    well.116 117    The buffer is shared with all other peers.118 119    Args:120        iterator: The shared iterator.121        buffer: The buffer for this peer.122        peers: The buffers of all peers.123        lock: The lock to synchronise access to the shared buffers.124 125    Yields:126        The next item from the shared iterator.127    """128    try:129        while True:130            if not buffer:131                async with lock:132                    # Another peer produced an item while we were waiting for the lock.133                    # Proceed with the next loop iteration to yield the item.134                    if buffer:135                        continue136                    try:137                        item = await anext(iterator)138                    except StopAsyncIteration:139                        break140                    else:141                        # Append to all buffers, including our own. We'll fetch our142                        # item from the buffer again, instead of yielding it directly.143                        # This ensures the proper item ordering if any of our peers144                        # are fetching items concurrently. They may have buffered their145                        # item already.146                        for peer_buffer in peers:147                            peer_buffer.append(item)148            yield buffer.popleft()149    finally:150        async with lock:151            # this peer is done - remove its buffer152            for idx, peer_buffer in enumerate(peers):  # pragma: no branch153                if peer_buffer is buffer:154                    peers.pop(idx)155                    break156            # if we are the last peer, try and close the iterator157            if not peers and hasattr(iterator, "aclose"):158                await iterator.aclose()159 160 161class Tee(Generic[T]):162    """Create `n` separate asynchronous iterators over `iterable`.163 164    This splits a single `iterable` into multiple iterators, each providing165    the same items in the same order.166 167    All child iterators may advance separately but share the same items from `iterable`168    -- when the most advanced iterator retrieves an item, it is buffered until the least169    advanced iterator has yielded it as well.170 171    A `tee` works lazily and can handle an infinite `iterable`, provided172    that all iterators advance.173 174    ```python175    async def derivative(sensor_data):176        previous, current = a.tee(sensor_data, n=2)177        await a.anext(previous)  # advance one iterator178        return a.map(operator.sub, previous, current)179    ```180 181    Unlike `itertools.tee`, `.tee` returns a custom type instead of a `tuple`. Like a182    tuple, it can be indexed, iterated and unpacked to get the child iterators. In183    addition, its `.tee.aclose` method immediately closes all children, and it can be184    used in an `async with` context for the same effect.185 186    If `iterable` is an iterator and read elsewhere, `tee` will *not* provide these187    items. Also, `tee` must internally buffer each item until the last iterator has188    yielded it; if the most and least advanced iterator differ by most data, using a189    `list` is more efficient (but not lazy).190 191    If the underlying iterable is concurrency safe (`anext` may be awaited concurrently)192    the resulting iterators are concurrency safe as well. Otherwise, the iterators are193    safe if there is only ever one single "most advanced" iterator.194 195    To enforce sequential use of `anext`, provide a `lock`196 197    - e.g. an `asyncio.Lock` instance in an `asyncio` application - and access is198        automatically synchronised.199 200    """201 202    def __init__(203        self,204        iterable: AsyncIterator[T],205        n: int = 2,206        *,207        lock: AbstractAsyncContextManager[Any] | None = None,208    ):209        """Create a `tee`.210 211        Args:212            iterable: The iterable to split.213            n: The number of iterators to create.214            lock: The lock to synchronise access to the shared buffers.215 216        """217        self._iterator = iterable.__aiter__()  # before 3.10 aiter() doesn't exist218        self._buffers: list[deque[T]] = [deque() for _ in range(n)]219        self._children = tuple(220            tee_peer(221                iterator=self._iterator,222                buffer=buffer,223                peers=self._buffers,224                lock=lock if lock is not None else NoLock(),225            )226            for buffer in self._buffers227        )228 229    def __len__(self) -> int:230        """Return the number of child iterators."""231        return len(self._children)232 233    @overload234    def __getitem__(self, item: int) -> AsyncIterator[T]: ...235 236    @overload237    def __getitem__(self, item: slice) -> tuple[AsyncIterator[T], ...]: ...238 239    def __getitem__(240        self, item: int | slice241    ) -> AsyncIterator[T] | tuple[AsyncIterator[T], ...]:242        """Return the child iterator(s) for the given index or slice."""243        return self._children[item]244 245    def __iter__(self) -> Iterator[AsyncIterator[T]]:246        """Iterate over the child iterators.247 248        Yields:249            The child iterators.250        """251        yield from self._children252 253    async def __aenter__(self) -> "Tee[T]":254        """Return the tee instance."""255        return self256 257    async def __aexit__(258        self,259        exc_type: type[BaseException] | None,260        exc_val: BaseException | None,261        exc_tb: TracebackType | None,262    ) -> bool:263        """Close all child iterators.264 265        Returns:266            `False`, exceptions not suppressed.267        """268        await self.aclose()269        return False270 271    async def aclose(self) -> None:272        """Async close all child iterators."""273        for child in self._children:274            await child.aclose()275 276 277atee = Tee278 279 280class aclosing(AbstractAsyncContextManager):  # noqa: N801281    """Async context manager to wrap an `AsyncGenerator` that has a `aclose()` method.282 283    Code like this:284 285    ```python286    async with aclosing(<module>.fetch(<arguments>)) as agen:287        <block>288    ```289 290    ...is equivalent to this:291 292    ```python293    agen = <module>.fetch(<arguments>)294    try:295        <block>296    finally:297        await agen.aclose()298 299    ```300    """301 302    def __init__(self, thing: AsyncGenerator[Any, Any] | AsyncIterator[Any]) -> None:303        """Create the context manager.304 305        Args:306            thing: The resource to wrap.307        """308        self.thing = thing309 310    @override311    async def __aenter__(self) -> AsyncGenerator[Any, Any] | AsyncIterator[Any]:312        return self.thing313 314    @override315    async def __aexit__(316        self,317        exc_type: type[BaseException] | None,318        exc_value: BaseException | None,319        traceback: TracebackType | None,320    ) -> None:321        if hasattr(self.thing, "aclose"):322            await self.thing.aclose()323 324 325async def abatch_iterate(326    size: int, iterable: AsyncIterable[T]327) -> AsyncIterator[list[T]]:328    """Utility batching function for async iterables.329 330    Args:331        size: The size of the batch.332        iterable: The async iterable to batch.333 334    Yields:335        The batches.336    """337    batch: list[T] = []338    async for element in iterable:339        if len(batch) < size:340            batch.append(element)341 342        if len(batch) >= size:343            yield batch344            batch = []345 346    if batch:347        yield batch348 
codekingpro/portable-devtools · Team Ai