diff --git a/README.md b/README.md
index 4516805..8708489 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ and offers both synchronous and asynchronous clients powered by [httpx](https://
## Documentation
-The REST API documentation can be found on [autorender.mintlify.app](https://autorender.mintlify.app/). The full API of this library can be found in [api.md](api.md).
+The REST API documentation can be found on [autorender.io](https://autorender.io/docs). The full API of this library can be found in [api.md](api.md).
## Installation
@@ -30,10 +30,10 @@ client = Autorender(
api_key=os.environ.get("AUTORENDER_API_KEY"), # This is the default and can be omitted
)
-files = client.files.list(
+page = client.files.list(
limit=10,
)
-print(files.files)
+print(page.files)
```
While you can provide an `api_key` keyword argument,
@@ -56,10 +56,10 @@ client = AsyncAutorender(
async def main() -> None:
- files = await client.files.list(
+ page = await client.files.list(
limit=10,
)
- print(files.files)
+ print(page.files)
asyncio.run(main())
@@ -92,10 +92,10 @@ async def main() -> None:
api_key=os.environ.get("AUTORENDER_API_KEY"), # This is the default and can be omitted
http_client=DefaultAioHttpClient(),
) as client:
- files = await client.files.list(
+ page = await client.files.list(
limit=10,
)
- print(files.files)
+ print(page.files)
asyncio.run(main())
@@ -110,6 +110,69 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ
Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`.
+## Pagination
+
+List methods in the Autorender API are paginated.
+
+This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually:
+
+```python
+from autorender import Autorender
+
+client = Autorender()
+
+all_files = []
+# Automatically fetches more pages as needed.
+for file in client.files.list():
+ # Do something with file here
+ all_files.append(file)
+print(all_files)
+```
+
+Or, asynchronously:
+
+```python
+import asyncio
+from autorender import AsyncAutorender
+
+client = AsyncAutorender()
+
+
+async def main() -> None:
+ all_files = []
+ # Iterate through items across all pages, issuing requests as needed.
+ async for file in client.files.list():
+ all_files.append(file)
+ print(all_files)
+
+
+asyncio.run(main())
+```
+
+Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages:
+
+```python
+first_page = await client.files.list()
+if first_page.has_next_page():
+ print(f"will fetch next page using these details: {first_page.next_page_info()}")
+ next_page = await first_page.get_next_page()
+ print(f"number of items we just fetched: {len(next_page.files)}")
+
+# Remove `await` for non-async usage.
+```
+
+Or just work directly with the returned data:
+
+```python
+first_page = await client.files.list()
+
+print(f"page number: {first_page.meta.page}") # => "page number: 1"
+for file in first_page.files:
+ print(file.id)
+
+# Remove `await` for non-async usage.
+```
+
## File uploads
Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`.
diff --git a/api.md b/api.md
index 2d1092f..520b981 100644
--- a/api.md
+++ b/api.md
@@ -22,7 +22,7 @@ from autorender.types import FileRetrieveResponse, FileListResponse, FileRenameR
Methods:
- client.files.retrieve(file_no) -> FileRetrieveResponse
-- client.files.list(\*\*params) -> FileListResponse
+- client.files.list(\*\*params) -> SyncPagePagination[FileListResponse]
- client.files.delete(file_no) -> None
- client.files.rename(file_no, \*\*params) -> FileRenameResponse
diff --git a/src/autorender/pagination.py b/src/autorender/pagination.py
index c8e897c..0f8ca09 100644
--- a/src/autorender/pagination.py
+++ b/src/autorender/pagination.py
@@ -3,38 +3,50 @@
from typing import List, Generic, TypeVar, Optional
from typing_extensions import override
+from pydantic import Field as FieldInfo
+
+from ._models import BaseModel
from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage
-__all__ = ["SyncPagePagination", "AsyncPagePagination"]
+__all__ = ["PagePaginationMeta", "SyncPagePagination", "AsyncPagePagination"]
_T = TypeVar("_T")
+class PagePaginationMeta(BaseModel):
+ has_next: Optional[bool] = FieldInfo(alias="hasNext", default=None)
+
+ page: Optional[int] = None
+
+
class SyncPagePagination(BaseSyncPage[_T], BasePage[_T], Generic[_T]):
- data: List[_T]
- current_page: Optional[int] = None
- has_next: Optional[bool] = None
- total_results: Optional[int] = None
+ files: List[_T]
+ meta: Optional[PagePaginationMeta] = None
@override
def _get_page_items(self) -> List[_T]:
- data = self.data
- if not data:
+ files = self.files
+ if not files:
return []
- return data
+ return files
@override
def has_next_page(self) -> bool:
- items = self._get_page_items()
- if not items:
+ has_next = None
+ if self.meta is not None:
+ if self.meta.has_next is not None:
+ has_next = self.meta.has_next
+ if has_next is not None and has_next is False:
return False
- if self.has_next is not None:
- return self.has_next
- return self.next_page_info() is not None
+
+ return super().has_next_page()
@override
def next_page_info(self) -> Optional[PageInfo]:
- current_page = self.current_page
+ current_page = None
+ if self.meta is not None:
+ if self.meta.page is not None:
+ current_page = self.meta.page
if current_page is None:
current_page = 1
@@ -42,30 +54,33 @@ def next_page_info(self) -> Optional[PageInfo]:
class AsyncPagePagination(BaseAsyncPage[_T], BasePage[_T], Generic[_T]):
- data: List[_T]
- current_page: Optional[int] = None
- has_next: Optional[bool] = None
- total_results: Optional[int] = None
+ files: List[_T]
+ meta: Optional[PagePaginationMeta] = None
@override
def _get_page_items(self) -> List[_T]:
- data = self.data
- if not data:
+ files = self.files
+ if not files:
return []
- return data
+ return files
@override
def has_next_page(self) -> bool:
- items = self._get_page_items()
- if not items:
+ has_next = None
+ if self.meta is not None:
+ if self.meta.has_next is not None:
+ has_next = self.meta.has_next
+ if has_next is not None and has_next is False:
return False
- if self.has_next is not None:
- return self.has_next
- return self.next_page_info() is not None
+
+ return super().has_next_page()
@override
def next_page_info(self) -> Optional[PageInfo]:
- current_page = self.current_page
+ current_page = None
+ if self.meta is not None:
+ if self.meta.page is not None:
+ current_page = self.meta.page
if current_page is None:
current_page = 1
diff --git a/src/autorender/resources/files.py b/src/autorender/resources/files.py
index b0d1ac8..8627b02 100644
--- a/src/autorender/resources/files.py
+++ b/src/autorender/resources/files.py
@@ -17,7 +17,8 @@
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
-from .._base_client import make_request_options
+from ..pagination import SyncPagePagination, AsyncPagePagination
+from .._base_client import AsyncPaginator, make_request_options
from ..types.file_list_response import FileListResponse
from ..types.file_rename_response import FileRenameResponse
from ..types.file_retrieve_response import FileRetrieveResponse
@@ -95,7 +96,7 @@ def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> FileListResponse:
+ ) -> SyncPagePagination[FileListResponse]:
"""
List/search files with pagination, filtering, and sorting.
@@ -112,8 +113,9 @@ def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return self._get(
+ return self._get_api_list(
"/api/v1/files",
+ page=SyncPagePagination[FileListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -130,7 +132,7 @@ def list(
file_list_params.FileListParams,
),
),
- cast_to=FileListResponse,
+ model=FileListResponse,
)
def delete(
@@ -260,7 +262,7 @@ async def retrieve(
cast_to=FileRetrieveResponse,
)
- async def list(
+ def list(
self,
*,
folder_no: str | Omit = omit,
@@ -275,7 +277,7 @@ async def list(
extra_query: Query | None = None,
extra_body: Body | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> FileListResponse:
+ ) -> AsyncPaginator[FileListResponse, AsyncPagePagination[FileListResponse]]:
"""
List/search files with pagination, filtering, and sorting.
@@ -292,14 +294,15 @@ async def list(
timeout: Override the client-level default timeout for this request, in seconds
"""
- return await self._get(
+ return self._get_api_list(
"/api/v1/files",
+ page=AsyncPagePagination[FileListResponse],
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
- query=await async_maybe_transform(
+ query=maybe_transform(
{
"folder_no": folder_no,
"limit": limit,
@@ -310,7 +313,7 @@ async def list(
file_list_params.FileListParams,
),
),
- cast_to=FileListResponse,
+ model=FileListResponse,
)
async def delete(
diff --git a/src/autorender/types/file_list_response.py b/src/autorender/types/file_list_response.py
index d8e0b7c..1c32786 100644
--- a/src/autorender/types/file_list_response.py
+++ b/src/autorender/types/file_list_response.py
@@ -3,14 +3,12 @@
from typing import Dict, List, Optional
from datetime import datetime
-from pydantic import Field as FieldInfo
-
from .._models import BaseModel
-__all__ = ["FileListResponse", "File", "Meta"]
+__all__ = ["FileListResponse"]
-class File(BaseModel):
+class FileListResponse(BaseModel):
id: str
created_at: datetime
@@ -44,23 +42,3 @@ class File(BaseModel):
url: str
width: Optional[int] = None
-
-
-class Meta(BaseModel):
- has_next: bool = FieldInfo(alias="hasNext")
-
- has_prev: bool = FieldInfo(alias="hasPrev")
-
- limit: int
-
- page: int
-
- total: int
-
-
-class FileListResponse(BaseModel):
- """Files list"""
-
- files: List[File]
-
- meta: Meta
diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py
index 8dff053..cc949a7 100644
--- a/tests/api_resources/test_files.py
+++ b/tests/api_resources/test_files.py
@@ -14,6 +14,7 @@
FileRenameResponse,
FileRetrieveResponse,
)
+from autorender.pagination import SyncPagePagination, AsyncPagePagination
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@@ -62,7 +63,7 @@ def test_path_params_retrieve(self, client: Autorender) -> None:
@parametrize
def test_method_list(self, client: Autorender) -> None:
file = client.files.list()
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(SyncPagePagination[FileListResponse], file, path=["response"])
@parametrize
def test_method_list_with_all_params(self, client: Autorender) -> None:
@@ -73,7 +74,7 @@ def test_method_list_with_all_params(self, client: Autorender) -> None:
search="search",
sort="name_asc",
)
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(SyncPagePagination[FileListResponse], file, path=["response"])
@parametrize
def test_raw_response_list(self, client: Autorender) -> None:
@@ -82,7 +83,7 @@ def test_raw_response_list(self, client: Autorender) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
file = response.parse()
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(SyncPagePagination[FileListResponse], file, path=["response"])
@parametrize
def test_streaming_response_list(self, client: Autorender) -> None:
@@ -91,7 +92,7 @@ def test_streaming_response_list(self, client: Autorender) -> None:
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
file = response.parse()
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(SyncPagePagination[FileListResponse], file, path=["response"])
assert cast(Any, response.is_closed) is True
@@ -222,7 +223,7 @@ async def test_path_params_retrieve(self, async_client: AsyncAutorender) -> None
@parametrize
async def test_method_list(self, async_client: AsyncAutorender) -> None:
file = await async_client.files.list()
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(AsyncPagePagination[FileListResponse], file, path=["response"])
@parametrize
async def test_method_list_with_all_params(self, async_client: AsyncAutorender) -> None:
@@ -233,7 +234,7 @@ async def test_method_list_with_all_params(self, async_client: AsyncAutorender)
search="search",
sort="name_asc",
)
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(AsyncPagePagination[FileListResponse], file, path=["response"])
@parametrize
async def test_raw_response_list(self, async_client: AsyncAutorender) -> None:
@@ -242,7 +243,7 @@ async def test_raw_response_list(self, async_client: AsyncAutorender) -> None:
assert response.is_closed is True
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
file = await response.parse()
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(AsyncPagePagination[FileListResponse], file, path=["response"])
@parametrize
async def test_streaming_response_list(self, async_client: AsyncAutorender) -> None:
@@ -251,7 +252,7 @@ async def test_streaming_response_list(self, async_client: AsyncAutorender) -> N
assert response.http_request.headers.get("X-Stainless-Lang") == "python"
file = await response.parse()
- assert_matches_type(FileListResponse, file, path=["response"])
+ assert_matches_type(AsyncPagePagination[FileListResponse], file, path=["response"])
assert cast(Any, response.is_closed) is True