Skip to content
Merged
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
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ outputs. Each entry has a stable `id`, a `kind`, a `format`, and a `version`
token that only changes when the bytes change, so a synchronizing client can
skip work it has already done:

`size_bytes` is populated for stored files and is `None` for tables, camera
poses, and packages rendered on demand; downloading those assets is the first
time their final byte size is known.

```python
for output in kanopy.list_job_outputs(job_id):
if output["kind"] != "merged_point_cloud":
Expand Down Expand Up @@ -202,8 +206,16 @@ re-authentication.

## Pagination

List methods return a `Page`. Offset pagination is used by default. Pass
`cursor=""` to start keyset pagination, then use `page.next_cursor`:
List methods return a `Page`. Offset pagination is used by default. For a full
scan, the iterator helpers handle keyset cursors automatically:

```python
for job in kanopy.iter_jobs(limit=100):
print(job["id"], job["status"])
```

Pass `cursor=""` directly when you need page boundaries or pagination
metadata, then use `page.next_cursor`:

```python
page = kanopy.list_jobs(cursor="", limit=100)
Expand Down
3 changes: 2 additions & 1 deletion scripts/check_dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ def sdist_names(path: Path) -> set[str]:

for required in REQUIRED_LICENSE_FILES:
if not any(
name.endswith(f".dist-info/licenses/{required}") for name in wheel_contents
name.endswith((f".dist-info/{required}", f".dist-info/licenses/{required}"))
for name in wheel_contents
):
raise SystemExit(f"wheel is missing {required}")

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = kanopy-ai
version = 0.4.0
version = 0.5.0
description = Python SDK for the Kanopy infrastructure inspection API
long_description = file: README.md
long_description_content_type = text/markdown
Expand Down
2 changes: 1 addition & 1 deletion src/kanopy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
from .models import Page

__all__ = ["DEFAULT_BASE_URL", "Kanopy", "KanopyError", "KanopyUploadError", "Page"]
__version__ = "0.4.0"
__version__ = "0.5.0"
40 changes: 39 additions & 1 deletion src/kanopy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import json
import math
import time
from collections.abc import Callable, Iterable, Mapping, Sequence
from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
from concurrent.futures import ThreadPoolExecutor, as_completed
from contextlib import ExitStack
from os import PathLike
Expand Down Expand Up @@ -237,6 +237,20 @@ def list_projects(
)
return self._page(response)

def iter_projects(self, *, limit: int = 100) -> Iterator[JsonObject]:
"""Yield every accessible project using stable cursor pagination."""
cursor = ""
seen_cursors: set[str] = set()
while True:
page = self.list_projects(cursor=cursor, limit=limit)
yield from page.items
if not page.next_cursor:
return
if page.next_cursor in seen_cursors:
raise RuntimeError("Kanopy API returned a repeated project cursor")
seen_cursors.add(page.next_cursor)
cursor = page.next_cursor

def create_project(
self, *, name: str, description: str | None = None
) -> JsonObject:
Expand Down Expand Up @@ -271,12 +285,36 @@ def list_jobs(
limit: int = 50,
cursor: str | None = None,
project_id: str | None = None,
skip_count: bool = False,
) -> Page[JsonObject]:
params = self._pagination_params(skip=skip, limit=limit, cursor=cursor)
if project_id is not None:
params["project_id"] = project_id
if skip_count:
params["skip_count"] = True
return self._page(self._request("GET", "/jobs", params=params))

def iter_jobs(
self, *, limit: int = 100, project_id: str | None = None
) -> Iterator[JsonObject]:
"""Yield every accessible job using stable cursor pagination."""
cursor = ""
seen_cursors: set[str] = set()
while True:
page = self.list_jobs(
cursor=cursor,
limit=limit,
project_id=project_id,
skip_count=bool(cursor),
)
yield from page.items
if not page.next_cursor:
return
if page.next_cursor in seen_cursors:
raise RuntimeError("Kanopy API returned a repeated job cursor")
seen_cursors.add(page.next_cursor)
cursor = page.next_cursor

def list_project_jobs(
self, project_id: str, *, skip: int = 0, limit: int = 50
) -> Page[JsonObject]:
Expand Down
Loading