Skip to content

Commit 2735fc4

Browse files
authored
feat: sync Go client with Apify OpenAPI spec v2-2026-07-10T105921Z; add lazy iteration helpers (#16)
1 parent aebb6cb commit 2735fc4

33 files changed

Lines changed: 1336 additions & 81 deletions

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,47 @@ All notable changes to the Apify Go client are documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.6.0] - 2026-07-10
9+
10+
### Added
11+
12+
- Lazy `Iterate` helpers on every list collection (`Actors`, `Runs`, `Builds`, `Tasks`,
13+
`Datasets`, `KeyValueStores`, `RequestQueues`, `Schedules`, `Webhooks`, `WebhookDispatches`,
14+
actor versions and env vars) plus dataset-item iteration (`DatasetClient.IterateItems` and the
15+
generic `IterateDatasetItems[T]`), backed by a new exported generic iterator type
16+
`ListIterator[T]`. As in the reference client's iterable `list()`, the options' `Limit` caps the
17+
total number of items yielded across all pages (unset means all), and the per-page size is a
18+
separate `chunkSize` argument (nil for the server default).
19+
- Cursor-based key iteration on the key-value store: `KeyValueStoreClient.IterateKeys` returns a
20+
`KeyValueStoreKeysIterator` that lazily walks all keys via `nextExclusiveStartKey`, matching the
21+
reference client's iterable `listKeys()`. `Limit` caps the total keys yielded and `chunkSize` is
22+
the page size.
23+
24+
### Fixed
25+
26+
- The `Iterate` helpers now honor a caller-set `Offset` on the list options as the starting point
27+
(iteration begins there and the cap counts from that offset), instead of silently discarding it.
28+
Matches the reference client's `options.offset` handling.
29+
30+
### Changed
31+
32+
- Bumped `APISpecVersion` to `v2-2026-07-10T105921Z`.
33+
- Bumped `ClientVersion` to `0.6.0`.
34+
- **Breaking:** `StoreCollectionClient.Iterate` now takes a second `chunkSize *int64` argument and
35+
treats the options' `Limit` as a total-item cap rather than the per-page size, to match the
36+
reference client's iterator semantics. `StoreActorIterator` is now an alias of
37+
`ListIterator[ActorStoreListItem]`.
38+
- Synced the `APISpecVersion` reference in the `README.md` "Versioning" section to match `version.go`.
39+
40+
### Documentation
41+
42+
- Documented how the client-side `Call`/`WaitForFinish` polling relates to `WithTimeout` (each
43+
poll asks the server to wait ≤60s, so the per-request timeout never cuts off a `nil` wait).
44+
- Added a pointer to where API tokens come from (Apify Console → Settings → Integrations).
45+
- Added the `IterateDatasetItems[T]` signature and a usage example to the storages guide.
46+
- Added `WithPublicBaseURL` to the `NewClientWithOptions` sample and a note distinguishing
47+
`client.Build(id)` from `Actor.Build(...)`.
48+
849
## [0.5.0] - 2026-07-09
950

1051
### Changed

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,13 @@ import (
3939
"context"
4040
"fmt"
4141
"log"
42+
"os"
4243

4344
apify "github.com/apify/apify-client-go"
4445
)
4546

4647
func main() {
47-
client := apify.NewClient("my-api-token")
48+
client := apify.NewClient(os.Getenv("APIFY_TOKEN"))
4849
ctx := context.Background()
4950

5051
// Start an Actor and wait for it to finish.
@@ -63,6 +64,11 @@ func main() {
6364
}
6465
```
6566

67+
Get your API token from the [Apify Console](https://console.apify.com/) under **Settings →
68+
Integrations** (the **Personal API tokens** section). The client never reads it from the
69+
environment itself: pass the token to `NewClient`/`WithToken` explicitly (the examples above
70+
read `APIFY_TOKEN` from the environment only as a convenience in `main`).
71+
6672
## Configuration
6773

6874
Use `NewClient(token)` for a token-only setup, or `NewClientWithOptions` with functional
@@ -71,8 +77,9 @@ options for full control:
7177
```go
7278
client := apify.NewClientWithOptions(
7379
apify.WithToken("my-api-token"),
74-
apify.WithBaseURL("https://api.apify.com"), // /v2 is appended automatically
75-
apify.WithMaxRetries(8), // default 8
80+
apify.WithBaseURL("https://api.apify.com"), // /v2 is appended automatically
81+
apify.WithPublicBaseURL("https://api.apify.com"), // base for signed, shareable URLs
82+
apify.WithMaxRetries(8), // default 8
7683
apify.WithMinDelayBetweenRetries(500*time.Millisecond),
7784
apify.WithTimeout(360*time.Second), // default 6 minutes
7885
apify.WithUserAgentSuffix("MyTool/1.0"),
@@ -179,7 +186,7 @@ func main() {
179186

180187
- `apify.ClientVersion` — the semantic version of this library.
181188
- `apify.APISpecVersion` — the Apify OpenAPI spec version this client was built against
182-
(`v2-2026-07-08T143931Z`).
189+
(`v2-2026-07-10T105921Z`).
183190

184191
### Releasing
185192

actor_collection.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@ func (c *ActorCollectionClient) List(ctx context.Context, options ActorListOptio
4040
return listResource[Actor](ctx, c.ctx, "", params)
4141
}
4242

43+
// Iterate returns a lazy iterator over the Actors matching the options, fetching pages on
44+
// demand. The options' Limit caps the total number of Actors yielded (unset means all); the
45+
// per-page size is chunkSize (nil for the server default). Mirrors the reference client's
46+
// iterable list().
47+
func (c *ActorCollectionClient) Iterate(options ActorListOptions, chunkSize *int64) *ListIterator[Actor] {
48+
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[Actor], error) {
49+
opts := options
50+
opts.Offset = &offset
51+
opts.Limit = pageLimitPtr(limit)
52+
return c.List(ctx, opts)
53+
})
54+
}
55+
4356
// Create creates a new Actor. actor is any JSON-serializable Actor definition.
4457
func (c *ActorCollectionClient) Create(ctx context.Context, actor any) (Actor, error) {
4558
return createResource[Actor](ctx, c.ctx, NewQueryParams(), actor)

actor_env_var.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ func (c *ActorEnvVarCollectionClient) List(ctx context.Context) (PaginationList[
1717
return listResource[ActorEnvVar](ctx, c.ctx, "", NewQueryParams())
1818
}
1919

20+
// Iterate returns a lazy iterator over the version's environment variables. Mirrors the
21+
// reference client's iterable list(). The env-vars endpoint is not offset-paginated (it
22+
// returns the full set in a single page), so there is no Limit/chunkSize control and the
23+
// closure ignores the offset/limit arguments; the iterator drains that one page.
24+
func (c *ActorEnvVarCollectionClient) Iterate() *ListIterator[ActorEnvVar] {
25+
return newListIterator(nil, nil, 0, func(ctx context.Context, _, _ int64) (PaginationList[ActorEnvVar], error) {
26+
return c.List(ctx)
27+
})
28+
}
29+
2030
// Create creates a new environment variable.
2131
func (c *ActorEnvVarCollectionClient) Create(ctx context.Context, envVar ActorEnvVar) (ActorEnvVar, error) {
2232
return createResource[ActorEnvVar](ctx, c.ctx, NewQueryParams(), envVar)

actor_version.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ func (c *ActorVersionCollectionClient) List(ctx context.Context, options ListOpt
1919
return listResource[ActorVersion](ctx, c.ctx, "", params)
2020
}
2121

22+
// Iterate returns a lazy iterator over the Actor's versions matching the options, fetching
23+
// pages on demand. The options' Limit caps the total number of versions yielded (unset means
24+
// all); the per-page size is chunkSize (nil for the server default). Mirrors the reference
25+
// client's iterable list().
26+
func (c *ActorVersionCollectionClient) Iterate(options ListOptions, chunkSize *int64) *ListIterator[ActorVersion] {
27+
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[ActorVersion], error) {
28+
opts := options
29+
opts.Offset = &offset
30+
opts.Limit = pageLimitPtr(limit)
31+
return c.List(ctx, opts)
32+
})
33+
}
34+
2235
// Create creates a new Actor version. version is any JSON-serializable version definition.
2336
func (c *ActorVersionCollectionClient) Create(ctx context.Context, version any) (ActorVersion, error) {
2437
return createResource[ActorVersion](ctx, c.ctx, NewQueryParams(), version)

build.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,19 @@ func (c *BuildCollectionClient) List(ctx context.Context, options ListOptions) (
2828
return listResource[Build](ctx, c.ctx, "", params)
2929
}
3030

31+
// Iterate returns a lazy iterator over the builds matching the options, fetching pages on
32+
// demand. The options' Limit caps the total number of builds yielded (unset means all); the
33+
// per-page size is chunkSize (nil for the server default). Mirrors the reference client's
34+
// iterable list().
35+
func (c *BuildCollectionClient) Iterate(options ListOptions, chunkSize *int64) *ListIterator[Build] {
36+
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[Build], error) {
37+
opts := options
38+
opts.Offset = &offset
39+
opts.Limit = pageLimitPtr(limit)
40+
return c.List(ctx, opts)
41+
})
42+
}
43+
3144
// BuildClient is a client for a specific Actor build (/v2/actor-builds/{buildId}).
3245
type BuildClient struct {
3346
ctx *resourceContext

dataset.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,12 @@ func (c *DatasetClient) Delete(ctx context.Context) error {
152152
return deleteResource(ctx, c.ctx, "")
153153
}
154154

155-
// ListItems lists items from the dataset.
155+
// ListDatasetItems lists a single page of items from the dataset, decoding each into T
156+
// (e.g. json.RawMessage or a struct).
156157
//
157158
// The dataset items endpoint returns a bare JSON array (not a data envelope) and reports
158159
// pagination via X-Apify-Pagination-* headers, which are surfaced in the returned
159-
// [PaginationList]. T is the item type to decode into (e.g. json.RawMessage or a struct).
160+
// [PaginationList].
160161
func ListDatasetItems[T any](ctx context.Context, c *DatasetClient, options DatasetListItemsOptions) (PaginationList[T], error) {
161162
var result PaginationList[T]
162163
params := NewQueryParams()
@@ -188,6 +189,34 @@ func (c *DatasetClient) ListItems(ctx context.Context, options DatasetListItemsO
188189
return ListDatasetItems[json.RawMessage](ctx, c, options)
189190
}
190191

192+
// IterateDatasetItems returns a lazy iterator over the dataset's items, decoding each into T
193+
// and fetching pages on demand. The options' Limit caps the total number of items yielded
194+
// (unset means all); the per-page size is chunkSize (nil for the server default). Mirrors the
195+
// reference client's iterable listItems().
196+
//
197+
// Caveat: offset-based iteration paginates using the item total reported in the
198+
// X-Apify-Pagination-Total header, and that header can lag right after items are pushed (the
199+
// count is updated asynchronously). Iterating immediately after a push may therefore stop early
200+
// (after one page) until the total settles. This matches the reference client's behaviour; wait
201+
// for the total to converge before iterating a just-written dataset if completeness matters.
202+
func IterateDatasetItems[T any](c *DatasetClient, options DatasetListItemsOptions, chunkSize *int64) *ListIterator[T] {
203+
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[T], error) {
204+
opts := options
205+
opts.Offset = &offset
206+
opts.Limit = pageLimitPtr(limit)
207+
return ListDatasetItems[T](ctx, c, opts)
208+
})
209+
}
210+
211+
// IterateItems returns a lazy iterator over the dataset's items, decoding each into a generic
212+
// json.RawMessage. For typed decoding use [IterateDatasetItems]. See IterateDatasetItems for
213+
// how the options' Limit (total cap) and chunkSize (page size) are interpreted, including the
214+
// caveat that the pagination-total header can lag right after a push and cause an immediate
215+
// iteration to stop after one page.
216+
func (c *DatasetClient) IterateItems(options DatasetListItemsOptions, chunkSize *int64) *ListIterator[json.RawMessage] {
217+
return IterateDatasetItems[json.RawMessage](c, options, chunkSize)
218+
}
219+
191220
// DownloadItems downloads dataset items serialized in the given format, returning the raw
192221
// bytes. Unlike ListItems (parsed items), this returns the items already serialized to JSON,
193222
// CSV, XLSX, XML, RSS or HTML — useful for exporting.

dataset_collection.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ func (c *DatasetCollectionClient) List(ctx context.Context, options StorageListO
1818
return listResource[Dataset](ctx, c.ctx, "", params)
1919
}
2020

21+
// Iterate returns a lazy iterator over the datasets matching the options, fetching pages on
22+
// demand. The options' Limit caps the total number of datasets yielded (unset means all); the
23+
// per-page size is chunkSize (nil for the server default). Mirrors the reference client's
24+
// iterable list().
25+
func (c *DatasetCollectionClient) Iterate(options StorageListOptions, chunkSize *int64) *ListIterator[Dataset] {
26+
return newListIterator(options.Limit, chunkSize, offsetVal(options.Offset), func(ctx context.Context, offset, limit int64) (PaginationList[Dataset], error) {
27+
opts := options
28+
opts.Offset = &offset
29+
opts.Limit = pageLimitPtr(limit)
30+
return c.List(ctx, opts)
31+
})
32+
}
33+
2134
// GetOrCreate gets the dataset with the given name, creating it if it does not exist. An
2235
// empty name creates a new unnamed dataset.
2336
func (c *DatasetCollectionClient) GetOrCreate(ctx context.Context, name string) (Dataset, error) {

docs/actors.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Actors are the programs that run on the Apify platform. Access the Actor collect
99
| Method | Description |
1010
| --- | --- |
1111
| `List(ctx, ActorListOptions) (PaginationList[Actor], error)` | List the account's Actors. |
12+
| `Iterate(ActorListOptions, chunkSize *int64) *ListIterator[Actor]` | Lazy iterator over matching Actors. `Limit` caps the total yielded; `chunkSize` is the page size. |
1213
| `Create(ctx, definition any) (Actor, error)` | Create a new Actor. |
1314

1415
`Create` takes a free-form definition (`any`) serialized to JSON, so the Actor's fields are
@@ -92,6 +93,11 @@ The `Actor` value returned by `Get`/`Create`/`Update` and listed by `List`:
9293
| `Version(n) *ActorVersionClient` / `Versions() *ActorVersionCollectionClient` | Versions. |
9394
| `Webhooks() *WebhookCollectionClient` | This Actor's webhooks. |
9495

96+
> **Note — two different `Build`s.** `Actor.Build(ctx, versionNumber, ActorBuildOptions)` here
97+
> *starts* a build of a version and returns the resulting `Build`. It is unrelated to the
98+
> top-level accessor `client.Build(id)`, which returns a `*BuildClient` for inspecting an
99+
> existing build by ID (see [builds.md](builds.md)). Same name, different jobs.
100+
95101
`ValidateInput` is equivalent to `ValidateInputForBuild(ctx, input, "")`: an empty `build`
96102
omits the parameter, so the API validates against the build tagged `latest` (per the API
97103
specification). Both return the raw JSON validation result from the API — a JSON object
@@ -167,15 +173,27 @@ run, err := client.Actor("apify/hello-world").Call(ctx,
167173
)
168174
```
169175

176+
> **How `Call`/`WaitForFinish` relate to `WithTimeout`.** The wait is done client-side by
177+
> *polling*: the client repeatedly re-fetches the run, each poll being a separate HTTP request
178+
> that asks the server to block for at most 60 seconds (the API's per-request wait cap). Because
179+
> every poll returns within that 60-second server cap — comfortably inside the client's
180+
> `WithTimeout` budget (default 360s, which bounds each individual request, not the total wait) —
181+
> the overall wait is **not** cut off at 360s. It continues across as many polls as needed until
182+
> the run reaches a terminal state. Passing `waitSecs == nil` therefore genuinely waits until the
183+
> run finishes (bounded only by a very large internal cap of ~11.5 days, or by cancelling the
184+
> `ctx` you pass in); a non-nil `waitSecs` bounds the total client-side wait instead. The distinct
185+
> `ActorStartOptions.WaitForFinish` field is unrelated: it only controls the single server-side
186+
> wait on the initial `Start` request (max 60s), not the client-side polling loop.
187+
170188
## Versions and environment variables
171189

172190
`client.Actor(id).Versions()` and `.Version(n)`:
173191

174192
| Method | Description |
175193
| --- | --- |
176-
| `Versions().List(ctx, ListOptions)` / `Versions().Create(ctx, def)` | List/create versions. |
194+
| `Versions().List(ctx, ListOptions)` / `Versions().Iterate(ListOptions, chunkSize *int64)` / `Versions().Create(ctx, def)` | List/iterate/create versions. |
177195
| `Version(n).Get/Update/Delete(ctx)` | Manage a single version. |
178-
| `Version(n).EnvVars().List(ctx)` / `.Create(ctx, ActorEnvVar)` | List/create env vars. |
196+
| `Version(n).EnvVars().List(ctx)` / `.Iterate()` / `.Create(ctx, ActorEnvVar)` | List/iterate/create env vars. |
179197
| `Version(n).EnvVar(name).Get/Update/Delete(ctx)` | Manage a single env var. |
180198

181199
`ActorEnvVar` fields:

docs/builds.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ single build with `client.Build(id)`, and an Actor's builds with `client.Actor(i
88
| Method | Description |
99
| --- | --- |
1010
| `List(ctx, ListOptions) (PaginationList[Build], error)` | List builds. |
11+
| `Iterate(ListOptions, chunkSize *int64) *ListIterator[Build]` | Lazy iterator over matching builds. `Limit` caps the total yielded; `chunkSize` is the page size. |
1112

1213
## Single build
1314

0 commit comments

Comments
 (0)