Skip to content
Draft
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
38 changes: 32 additions & 6 deletions projects/fal/src/fal/sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,19 +1136,45 @@ def list_applications(
application_name: str | None = None,
*,
environment_name: str | None = None,
page_size: int | None = None,
) -> list[ApplicationInfo]:
"""List application revisions, following pagination transparently.

The server caps how many revisions a single response may carry, so this
walks ``next_page_token`` until it is unset. Callers keep the previous
"everything" semantics; ``page_size`` only tunes the request size.
"""
full_application_name = (
construct_alias(application_name, environment_name)
if application_name
else None
)

request = isolate_proto.ListApplicationsRequest(
application_name=full_application_name,
environment_name=environment_name,
)
res: isolate_proto.ListApplicationsResult = self.stub.ListApplications(request)
return [from_grpc(app) for app in res.applications]
applications: list[ApplicationInfo] = []
page_token: str | None = None
while True:
request = isolate_proto.ListApplicationsRequest(
application_name=full_application_name,
environment_name=environment_name,
page_size=page_size,
page_token=page_token,
)
res: isolate_proto.ListApplicationsResult = self.stub.ListApplications(
request
)
applications.extend(from_grpc(app) for app in res.applications)

# Absence is the only end-of-pages signal -- a short page does not
# mean the last page.
if not res.HasField("next_page_token"):
return applications

# Guard against a server that keeps handing back the same cursor;
# without this a bug there becomes an infinite loop here.
if res.next_page_token == page_token:
return applications

page_token = res.next_page_token

def delete_application(
self,
Expand Down
74 changes: 74 additions & 0 deletions projects/fal/tests/unit/test_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,77 @@ def test_register_rejects_empty_retry_config():
retry_config=RetryConfig(),
)
)


class PagingListApplicationsStub:
"""Serves a fixed sequence of pages and records the requests it received."""

def __init__(self, pages):
self._pages = list(pages)
self.requests = []

def ListApplications(self, request):
self.requests.append(request)
return self._pages[len(self.requests) - 1]


def _page(application_ids, next_page_token=None):
result = isolate_proto.ListApplicationsResult(
applications=[
isolate_proto.ApplicationInfo(application_id=app_id)
for app_id in application_ids
]
)
if next_page_token is not None:
result.next_page_token = next_page_token
return result


def test_list_applications_follows_pagination_until_token_absent():
connection = FalServerlessConnection("api.alpha.fal.ai", MagicMock())
stub = PagingListApplicationsStub(
[
_page(["a", "b"], next_page_token="cursor-1"),
# A short page is not the last page -- only an absent token is.
_page(["c"], next_page_token="cursor-2"),
_page(["d"]),
]
)
connection._stub = stub # type: ignore[assignment]

apps = connection.list_applications()

assert [app.application_id for app in apps] == ["a", "b", "c", "d"]
assert len(stub.requests) == 3
assert stub.requests[0].HasField("page_token") is False
assert stub.requests[1].page_token == "cursor-1"
assert stub.requests[2].page_token == "cursor-2"


def test_list_applications_forwards_page_size_and_omits_it_when_unset():
connection = FalServerlessConnection("api.alpha.fal.ai", MagicMock())
stub = PagingListApplicationsStub([_page(["a"]), _page(["a"])])
connection._stub = stub # type: ignore[assignment]

connection.list_applications(page_size=250)
assert stub.requests[0].page_size == 250

connection.list_applications()
assert stub.requests[1].HasField("page_size") is False


def test_list_applications_stops_when_server_repeats_the_cursor():
connection = FalServerlessConnection("api.alpha.fal.ai", MagicMock())
stub = PagingListApplicationsStub(
[
_page(["a"], next_page_token="stuck"),
# A server that keeps returning the same cursor must not spin us.
_page(["b"], next_page_token="stuck"),
]
)
connection._stub = stub # type: ignore[assignment]

apps = connection.list_applications()

assert [app.application_id for app in apps] == ["a", "b"]
assert len(stub.requests) == 2
Loading