Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions examples/sandbox_11_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,27 @@ async def async_demo() -> None:

paged_ids: list[list[str]] = []
found_ids: set[str] = set()
async for page in pager.iter_pages():
page = first_page
while page is not None:
page_ids = [listed.id for listed in page.snapshots]
paged_ids.append(page_ids)
found_ids.update(page_ids)
if all(snapshot_id in found_ids for snapshot_id in async_snapshot_ids):
break
page = await page.get_next_page()

print(f" Visited pages: {paged_ids}")
assert all(snapshot_id in found_ids for snapshot_id in async_snapshot_ids), (
"Did not find all async snapshots in AsyncSnapshot.list() results"
)

item_ids: list[str] = []
async for listed in AsyncSnapshot.list(limit=10, since=since).iter_items():
item_ids.append(listed.id)
page = await AsyncSnapshot.list(limit=10, since=since)
while page is not None:
item_ids.extend(listed.id for listed in page)
if all(snapshot_id in item_ids for snapshot_id in async_snapshot_ids):
break
page = await page.get_next_page()

print(f" Iterated snapshot IDs: {item_ids}")
assert all(snapshot_id in item_ids for snapshot_id in async_snapshot_ids), (
Expand Down Expand Up @@ -252,23 +256,27 @@ def sync_demo() -> None:

paged_ids: list[list[str]] = []
found_ids: set[str] = set()
for page in first_page.iter_pages():
page = first_page
while page is not None:
page_ids = [listed.id for listed in page.snapshots]
paged_ids.append(page_ids)
found_ids.update(page_ids)
if all(snapshot_id in found_ids for snapshot_id in sync_snapshot_ids):
break
page = page.get_next_page()

print(f" Visited pages: {paged_ids}")
assert all(snapshot_id in found_ids for snapshot_id in sync_snapshot_ids), (
"Did not find all sync snapshots in Snapshot.list() pages"
)

item_ids: list[str] = []
for listed in Snapshot.list(limit=10, since=since).iter_items():
item_ids.append(listed.id)
page = Snapshot.list(limit=10, since=since)
while page is not None:
item_ids.extend(listed.id for listed in page)
if all(snapshot_id in item_ids for snapshot_id in sync_snapshot_ids):
break
page = page.get_next_page()

print(f" Iterated snapshot IDs: {item_ids}")
assert all(snapshot_id in item_ids for snapshot_id in sync_snapshot_ids), (
Expand Down
129 changes: 34 additions & 95 deletions examples/sandbox_15_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

This example demonstrates how to:
1. Create several sandboxes concurrently with ``AsyncSandbox.create()``
2. Exercise the async pager APIs, including direct async iteration
3. Exercise the sync page APIs
2. Fetch sandbox pages explicitly with ``list()`` and ``get_next_page()``
3. Iterate directly over the sandboxes owned by each page

The list API returns typed pages. Use a small ``limit`` to paginate through
the recent results and inspect only the first few pages.
the recent results one page at a time.
"""

from __future__ import annotations
Expand All @@ -26,7 +26,6 @@
PAGE_SIZE = 2
MAX_PAGES = 3
TIMEOUT_MS = 120_000
MAX_ITEMS = PAGE_SIZE * MAX_PAGES
PROJECT_ID = os.environ["VERCEL_PROJECT_ID"]


Expand All @@ -35,12 +34,7 @@ def _print_page(prefix: str, page_number: int, sandbox_ids: list[str]) -> None:


def _print_page_state(prefix: str, page) -> None:
next_page_info = page.next_page_info()
next_until = None if next_page_info is None else next_page_info.until
print(
f"{prefix} has_next={page.has_next_page()} "
f"count={page.pagination.count} next_until={next_until}"
)
print(f"{prefix} count={page.pagination.count} next_until={page.pagination.next}")


def _summarize_created(prefix: str, sandbox_ids: list[str], created_ids: set[str]) -> None:
Expand All @@ -67,74 +61,36 @@ async def async_demo(since: datetime) -> list[AsyncSandbox]:
created_ids = {sandbox.sandbox_id for sandbox in sandboxes}
print(f"Created {len(created_ids)} sandboxes concurrently")

print("\n[1] Await the pager to get the first page")
pager = AsyncSandbox.list(
print("\n[1] Await the first page")
first_page = await AsyncSandbox.list(
project_id=PROJECT_ID,
limit=PAGE_SIZE,
since=since,
)
first_page = await pager
_print_page_state("async first page:", first_page)
_print_page("async", 1, [sandbox.id for sandbox in first_page.sandboxes])

print("\n[2] Fetch the next page explicitly with get_next_page()")
next_page = await first_page.get_next_page()
if next_page is None:
print("async next page: none")
else:
_print_page_state("async next page:", next_page)
_print_page("async", 2, [sandbox.id for sandbox in next_page.sandboxes])
print("\n[2] Iterate the sandboxes in the first page")
current_page_ids = [sandbox.id for sandbox in first_page]
print(f"async current page iteration: {current_page_ids}")
_summarize_created("async current page iteration", current_page_ids, created_ids)

print("\n[3] Iterate pages from the pager with iter_pages()")
print("\n[3] Walk forward with get_next_page()")
paged_ids: list[str] = []
page_number = 0
async for page in AsyncSandbox.list(
project_id=PROJECT_ID,
limit=PAGE_SIZE,
since=since,
).iter_pages():
page_number += 1
page_ids = [sandbox.id for sandbox in page.sandboxes]
_print_page("async iter_pages", page_number, page_ids)
page_number = 1
page = first_page
while True:
page_ids = [sandbox.id for sandbox in page]
_print_page("async paged", page_number, page_ids)
paged_ids.extend(page_ids)
if page_number >= MAX_PAGES:
break
_summarize_created("async iter_pages", paged_ids, created_ids)

print("\n[4] Iterate items from the first page with page.iter_items()")
iter_item_ids: list[str] = []
async for sandbox in first_page.iter_items():
iter_item_ids.append(sandbox.id)
if len(iter_item_ids) >= MAX_ITEMS:
break
print(f"async iter_items: {iter_item_ids}")
_summarize_created("async iter_items", iter_item_ids, created_ids)

print("\n[5] Iterate items from the pager with pager.iter_items()")
pager_item_ids: list[str] = []
async for sandbox in AsyncSandbox.list(
project_id=PROJECT_ID,
limit=PAGE_SIZE,
since=since,
).iter_items():
pager_item_ids.append(sandbox.id)
if len(pager_item_ids) >= MAX_ITEMS:
next_page = await page.get_next_page()
if next_page is None:
break
print(f"async pager.iter_items: {pager_item_ids}")
_summarize_created("async pager.iter_items", pager_item_ids, created_ids)

print("\n[6] Iterate items directly from the pager")
direct_item_ids: list[str] = []
async for sandbox in AsyncSandbox.list(
project_id=PROJECT_ID,
limit=PAGE_SIZE,
since=since,
):
direct_item_ids.append(sandbox.id)
if len(direct_item_ids) >= MAX_ITEMS:
break
print(f"async direct iteration: {direct_item_ids}")
_summarize_created("async direct iteration", direct_item_ids, created_ids)
page_number += 1
page = next_page
_summarize_created("async paged", paged_ids, created_ids)

return sandboxes

Expand All @@ -158,39 +114,22 @@ def sync_demo(since: datetime) -> None:
_print_page_state("sync first page:", first_page)
_print_page("sync", 1, [sandbox.id for sandbox in first_page.sandboxes])

print("\n[2] Fetch the next page explicitly with get_next_page()")
next_page = first_page.get_next_page()
if next_page is None:
print("sync next page: none")
else:
_print_page_state("sync next page:", next_page)
_print_page("sync", 2, [sandbox.id for sandbox in next_page.sandboxes])

print("\n[3] Iterate pages with iter_pages()")
page_number = 0
for current_page in Sandbox.list(
project_id=PROJECT_ID,
limit=PAGE_SIZE,
since=since,
).iter_pages():
page_number += 1
_print_page(
"sync iter_pages", page_number, [sandbox.id for sandbox in current_page.sandboxes]
)
print("\n[2] Iterate the sandboxes in the first page")
current_page_ids = [sandbox.id for sandbox in first_page]
print(f"sync current page iteration: {current_page_ids}")

print("\n[3] Walk forward with get_next_page()")
page_number = 1
page = first_page
while True:
_print_page("sync paged", page_number, [sandbox.id for sandbox in page])
if page_number >= MAX_PAGES:
break

print("\n[4] Iterate items with iter_items()")
iter_item_ids: list[str] = []
for sandbox in Sandbox.list(
project_id=PROJECT_ID,
limit=PAGE_SIZE,
since=since,
).iter_items():
iter_item_ids.append(sandbox.id)
if len(iter_item_ids) >= MAX_ITEMS:
next_page = page.get_next_page()
if next_page is None:
break
print(f"sync iter_items: {iter_item_ids}")
page_number += 1
page = next_page


if __name__ == "__main__":
Expand Down
75 changes: 9 additions & 66 deletions src/vercel/_internal/pagination.py
Original file line number Diff line number Diff line change
@@ -1,78 +1,21 @@
from __future__ import annotations

from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator, Sequence
from collections.abc import Iterator
from dataclasses import dataclass
from typing import Generic, TypeVar

from vercel._internal.iter_coroutine import iter_coroutine
from vercel._internal.sandbox.models import Pagination

PageT = TypeVar("PageT")
ItemT = TypeVar("ItemT")
PageInfoT = TypeVar("PageInfoT")


@dataclass(frozen=True)
class PageController(Generic[PageT, ItemT, PageInfoT]):
get_items: Callable[[PageT], Sequence[ItemT]]
get_next_page_info: Callable[[PageT], PageInfoT | None]
fetch_next_page: Callable[[PageInfoT], Awaitable[PageT]]
@dataclass(slots=True)
class Page(Generic[ItemT]):
items: list[ItemT]
pagination: Pagination

def has_next_page(self, page: PageT) -> bool:
return self.get_next_page_info(page) is not None
def __iter__(self) -> Iterator[ItemT]:
return iter(self.items)

def next_page_info(self, page: PageT) -> PageInfoT | None:
return self.get_next_page_info(page)

async def get_next_page(self, page: PageT) -> PageT | None:
next_page_info = self.get_next_page_info(page)
if next_page_info is None:
return None
return await self.fetch_next_page(next_page_info)

def get_next_page_sync(self, page: PageT) -> PageT | None:
return iter_coroutine(self.get_next_page(page))

async def iter_pages(self, initial_page: PageT) -> AsyncGenerator[PageT, None]:
page = initial_page
while True:
yield page
next_page = await self.get_next_page(page)
if next_page is None:
return
page = next_page

async def iter_items(self, initial_page: PageT) -> AsyncGenerator[ItemT, None]:
async for page in self.iter_pages(initial_page):
for item in self.get_items(page):
yield item

def iter_pages_sync(self, initial_page: PageT) -> Iterator[PageT]:
iterator = self.iter_pages(initial_page)
try:
while True:
try:
yield iter_coroutine(iterator.__anext__())
except StopAsyncIteration:
return
finally:
try:
iter_coroutine(iterator.aclose())
except RuntimeError:
pass

def iter_items_sync(self, initial_page: PageT) -> Iterator[ItemT]:
iterator = self.iter_items(initial_page)
try:
while True:
try:
yield iter_coroutine(iterator.__anext__())
except StopAsyncIteration:
return
finally:
try:
iter_coroutine(iterator.aclose())
except RuntimeError:
pass


__all__ = ["PageController"]
__all__ = ["Page"]
28 changes: 0 additions & 28 deletions src/vercel/_internal/sandbox/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
from dataclasses import dataclass
from datetime import datetime, timezone

from vercel._internal.sandbox.models import Pagination


@dataclass(frozen=True, slots=True)
class SandboxPageInfo:
until: int


@dataclass(frozen=True, slots=True, init=False)
class _BaseListParams:
Expand Down Expand Up @@ -42,17 +35,6 @@ def with_until(self, until: int) -> SandboxListParams:
)


def next_sandbox_page_info(pagination: Pagination) -> SandboxPageInfo | None:
if pagination.next is None:
return None
return SandboxPageInfo(until=pagination.next)


@dataclass(frozen=True, slots=True)
class SnapshotPageInfo:
until: int


@dataclass(frozen=True, slots=True, init=False)
class SnapshotListParams(_BaseListParams):
def with_until(self, until: int) -> SnapshotListParams:
Expand All @@ -64,12 +46,6 @@ def with_until(self, until: int) -> SnapshotListParams:
)


def next_snapshot_page_info(pagination: Pagination) -> SnapshotPageInfo | None:
if pagination.next is None:
return None
return SnapshotPageInfo(until=pagination.next)


def normalize_list_timestamp(value: datetime | int | None) -> int | None:
if value is None:
return None
Expand All @@ -84,10 +60,6 @@ def normalize_list_timestamp(value: datetime | int | None) -> int | None:

__all__ = [
"SandboxListParams",
"SandboxPageInfo",
"SnapshotListParams",
"SnapshotPageInfo",
"next_sandbox_page_info",
"next_snapshot_page_info",
"normalize_list_timestamp",
]
Loading
Loading