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
24 changes: 18 additions & 6 deletions .github/workflows/rust-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,24 @@ jobs:
env:
# The integration-test token is stored as a repository secret.
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
# Limit parallelism to be gentle on the shared test account.
run: cargo test --verbose -- --test-threads=4
# Limit parallelism to be gentle on the shared test account. The documentation example
# programs in `tests/examples.rs` (test names prefixed `example_`) are exercised by the
# standalone `Test examples` step below, so they are skipped here to keep the two
# concerns separate.
run: cargo test --lib --tests --verbose -- --skip example_ --test-threads=4

- name: Run documentation example tests
# Standalone CI step that verifies the documentation examples actually work. It runs the
# example programs from `examples/` end-to-end against the live API (via the `example_*`
# smoke tests in `tests/examples.rs`, each of which executes `cargo run --example <name>`)
# and runs the in-documentation code snippets as doctests (`cargo test --doc`, which
# compiles every fenced `rust` block in the README and the `docs/` pages and runs the
# runnable ones). Both are required by the documentation requirements: each documentation
# example has a CI test that actually runs the code.
- name: Test examples
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
# Match the integration-test thread cap so doctests that hit the live API stay gentle
# on the shared account.
run: cargo test --doc -- --test-threads=4
# Match the integration-test thread cap so the example programs and any doctests that hit
# the live API stay gentle on the shared account.
run: |
cargo test --test examples --verbose -- --test-threads=4
cargo test --doc --verbose -- --test-threads=4
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ All notable changes to the Rust Apify API client are documented here. The format
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
to [Semantic Versioning](https://semver.org/).

## [0.2.1] - 2026-06-19

Compliance fix for the updated client/test requirements (apify-client-orchestration PR #4).
No changes to the public interface; CI and documentation-testing only.

### Changed
- CI: added a standalone `Test examples` workflow step that verifies the documentation
examples actually work — it runs the `examples/` programs end-to-end against the live API
(the `example_*` smoke tests in `tests/examples.rs`, each invoking `cargo run --example`) and
runs the in-documentation code snippets as doctests (`cargo test --doc`). The example smoke
tests were previously executed as part of the `Run integration tests` step and the doctests in
a separate `Run documentation example tests` step; they are now consolidated under the
requirement-named `Test examples` step. `Run integration tests` now skips the `example_*`
tests (via `--skip example_`) so the two concerns stay separate.
- Documentation testing: the external `docs/` pages (`docs/README.md`, `docs/actors.md`,
`docs/misc.md`, `docs/storages.md`, `docs/runs.md`, `docs/builds.md`) are now compiled as
doctests via `#[doc = include_str!]` in `src/lib.rs`, so every in-documentation `rust` code
snippet is verified valid and runnable by `cargo test --doc`. Previously only the root
`README.md` snippets were doctest-checked.
- Documentation: added response-model field tables for the types the README Quick start and the
examples read but which were previously undocumented — `ActorRun` (incl. `id`, `status`,
`default_dataset_id`/`default_key_value_store_id`/`default_request_queue_id`) in
`docs/runs.md`; `Actor` (`id`, …) and `Build` (`id`, `status`, …) in `docs/actors.md` (with a
cross-reference from `docs/builds.md`); the shared storage-metadata fields of `Dataset` /
`KeyValueStore` / `RequestQueue` (incl. `id`) in `docs/storages.md`; and `User` (`id`,
`username`) in `docs/misc.md`. Each new section carries a runnable `no_run` doctest exercising
the documented fields.

## [0.2.0] - 2026-06-19

Updated to Apify OpenAPI specification `v2-2026-06-18T095846Z` (previously
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "apify-client"
version = "0.2.0"
version = "0.2.1"
authors = ["Apify Technologies <support@apify.com>"]
description = "The official Rust client for the Apify API (https://apify.com)."
license = "Apache-2.0"
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ It provides a resource-oriented, async interface that mirrors the official
- Transparent authentication, retries with exponential backoff, and timeouts.
- Resource clients for Actors, runs, builds, tasks, datasets, key-value stores, request
queues, schedules, webhooks, the Apify Store, users and logs.
- Convenience helpers: run/wait, log streaming (redirection), lazy Store iteration.
- Convenience helpers: run/wait, log streaming (redirection; needs the `futures-util` crate —
see [Installation](#installation)), lazy Store iteration.
- A replaceable HTTP transport for testing or custom runtimes.

## Installation
Expand All @@ -18,8 +19,16 @@ It provides a resource-oriented, async interface that mirrors the official
[dependencies]
apify-client = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1" # for the `serde_json::Value` responses used in the Quick start
```

The Quick start below reads dynamically-typed records with `serde_json::Value`, so a fresh
project needs `serde_json`. Two more dependencies are needed only for specific features:

- `futures-util = "0.3"` — to consume `LogClient::stream()` (log streaming/redirection); it
provides the `StreamExt` trait used by the [`log_redirection`](examples/log_redirection.rs)
example. See [`docs/misc.md`](docs/misc.md#logs--clientlogbuild_or_run_id).

By default the client uses the system TLS (`native-tls`). To use rustls instead:

```toml
Expand Down
27 changes: 21 additions & 6 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ apify-client = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Some snippets and examples need extra crates: add `serde_json = "1"` when you read
dynamically-typed responses as `serde_json::Value` (as the README Quick start does), and
`futures-util = "0.3"` to consume `LogClient::stream()` (log streaming — see
[Store, users and logs](misc.md#logs--clientlogbuild_or_run_id)).

Create a client and call a resource:

```rust,no_run
Expand All @@ -45,18 +50,28 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

## Imports

All public types you need day-to-day — the client, the builder, and every option struct
(`ActorListOptions`, `ListOptions`, `StorageListOptions`, `StoreListOptions`,
`RunListOptions`, `ActorStartOptions`, `DatasetListItemsOptions`, `DownloadItemsFormat`,
`GetRecordOptions`, …) — are re-exported at the crate root, so you can import them directly
The client, the builder, and every option/parameter struct (`ActorListOptions`, `ListOptions`,
`StorageListOptions`, `StoreListOptions`, `RunListOptions`, `ActorStartOptions`,
`DatasetListItemsOptions`, `DownloadItemsFormat`, `GetRecordOptions`, …), plus the common
container `PaginationList`, are re-exported at the crate root, so you can import them directly
from `apify_client`:

```rust,no_run
use apify_client::{ApifyClient, ActorListOptions, StoreListOptions, DownloadItemsFormat};
```

You do **not** need the longer `apify_client::clients::<module>::<Type>` paths shown by
`cargo doc`'s module tree — the short crate-root path is the supported way to import them.
`cargo doc`'s module tree for these option types — the short crate-root path is the supported
way to import them.

API resource/response **models** (`Actor`, `ActorRun`, `Build`, `Dataset`, `KeyValueStore`,
`RequestQueue`, `RequestQueueRequest`, `RequestQueueHead`, `RequestQueueOperationInfo`,
`KeyValueStoreKeysPage`, `ActorStoreListItem`, `User`, …) live in the [`apify_client::models`]
module and are imported from there:

```rust,no_run
use apify_client::models::RequestQueueRequest;
```

## `ApifyClient` and the builder

Expand Down Expand Up @@ -111,7 +126,7 @@ Each example in [`../examples`](../examples) is runnable with
|---|---|
| `run_store_actor` | Run a Store Actor, wait, read its default dataset. |
| `storages` | Create + write + read each storage type. |
| `get_account` | Fetch the current account. |
| `get_account` | Fetch the current account, plus its monthly usage (current cycle and for a specific date). |
| `create_build_run_actor` | Create an Actor, build, run, fetch the run log. |
| `run_and_last_run_storages` | Run an Actor, then read the last run's storages. |
| `iterate_store` | Lazily iterate Store Actors. |
Expand Down
57 changes: 57 additions & 0 deletions docs/actors.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,63 @@ println!("validated against latest build: {result}");
The `build` argument accepts a build **tag** (e.g. `"latest"`, `"beta"`) or a build **number**
(e.g. `"1.2.34"`); the referenced build must already exist for the API to resolve its schema.

## `Actor` fields

`Actor` (from `apify_client::models`) is returned by `get`, `create`, `update`, and the Actor
`list`. The commonly-used fields — including the `actor.id` read in the
[README error-handling example](../README.md#error-handling) and the `create_build_run_actor`
example:

| Field | Type | Description |
|---|---|---|
| `id` | `String` | Unique Actor ID (always present); used to build a `client.actor(&actor.id)` client. |
| `user_id` | `Option<String>` | ID of the user who owns the Actor. |
| `name` | `Option<String>` | Technical name used in API paths. |
| `username` | `Option<String>` | Username of the Actor's owner. |
| `title` | `Option<String>` | Human-readable title shown in the UI. |
| `description` | `Option<String>` | Description of what the Actor does. |
| `is_public` | `Option<bool>` | Whether the Actor is published in Apify Store. |
| `created_at` | `Option<DateTime<Utc>>` | When the Actor was created. |
| `modified_at` | `Option<DateTime<Utc>>` | When the Actor was last modified. |
| `extra` | `Extra` | Any other fields returned by the API. |

```rust,no_run
# use apify_client::ApifyClient;
# async fn run(client: ApifyClient) -> Result<(), Box<dyn std::error::Error>> {
if let Some(actor) = client.actor("apify~hello-world").get().await? {
println!("actor {} ({:?})", actor.id, actor.title.or(actor.name));
}
# Ok(())
# }
```

## `Build` fields

`Build` (from `apify_client::models`) is returned by `build`, `default_build` resolution, `get`,
`abort` and `wait_for_finish` (see also [builds](builds.md)). The fields the
`create_build_run_actor` example reads (`build.id`, `build.status`):

| Field | Type | Description |
|---|---|---|
| `id` | `String` | Unique build ID (always present); used to build a `client.build(&build.id)` client. |
| `act_id` | `Option<String>` | ID of the Actor that was built. |
| `status` | `Option<String>` | Current build status; the terminal values match the run statuses. |
| `started_at` | `Option<DateTime<Utc>>` | When the build started. |
| `finished_at` | `Option<DateTime<Utc>>` | When the build finished. |
| `build_number` | `Option<String>` | Build number, e.g. `0.1.2`. |
| `extra` | `Extra` | Any other fields returned by the API. |

`Build::is_terminal()` reports whether `status` is a terminal value, mirroring `ActorRun`.

```rust,no_run
# use apify_client::ApifyClient;
# async fn run(client: ApifyClient, build_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let build = client.build(build_id).wait_for_finish(Some(300)).await?;
println!("build {} status {:?}", build.id, build.status);
# Ok(())
# }
```

## Actor versions and environment variables

`ActorVersionClient`: `get`, `update`, `delete`, `env_var(name)`, `env_vars()`.
Expand Down
3 changes: 3 additions & 0 deletions docs/builds.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ collections are available via `actor.builds()`.
| `delete()` | — | `()` | Deletes the build. |
| `wait_for_finish(wait_secs)` | `Option<i64>` | `Build` | Polls until the build is terminal. |
| `log()` | — | `LogClient` | Access the build's log. |

The returned `Build` model's fields (`id`, `status`, `build_number`, …) are documented in
[actors.md → `Build` fields](actors.md#build-fields).
75 changes: 73 additions & 2 deletions docs/misc.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,36 @@
`StoreListOptions`: `offset`, `limit`, `search`, `sort_by`, `category`, `username`,
`pricing_model`.

`StoreActorIterator::next()` returns `Option<ActorStoreListItem>`, fetching the next page
on demand until the listing is exhausted.
`StoreActorIterator::next()` is `async` and fallible — it returns
`ApifyClientResult<Option<ActorStoreListItem>>` (i.e. `Result<Option<ActorStoreListItem>, ApifyClientError>`),
fetching the next page on demand and yielding `Ok(None)` once the listing is exhausted. Drive it
with `.await?`:

```rust,no_run
# use apify_client::{ApifyClient, StoreListOptions};
# async fn run(client: ApifyClient) -> Result<(), Box<dyn std::error::Error>> {
let mut iter = client.store().iterate(StoreListOptions::default());
while let Some(actor) = iter.next().await? {
// `title` is the human-readable name; fall back to the technical `name`.
println!("{}: {:?}", actor.id, actor.title.or(actor.name));
}
# Ok(())
# }
```

`ActorStoreListItem` (from `apify_client::models`) is the element type yielded by both `list`
and the iterator. Its fields:

| Field | Type | Description |
|---|---|---|
| `id` | `String` | Unique Actor ID (always present). |
| `name` | `Option<String>` | Technical name of the Actor. |
| `username` | `Option<String>` | Username of the Actor's owner. |
| `title` | `Option<String>` | Human-readable title. |
| `extra` | `Extra` | Any other fields returned by the API. |

`name`, `username` and `title` are optional, so a display routine typically prefers `title`
and falls back to `name` (e.g. `actor.title.or(actor.name)`).

## Users — `client.me()` / `client.user(id)`

Expand All @@ -32,6 +60,25 @@ The methods marked **(`me` only)** operate on the authenticated account and are
`Err(ApifyClientError::InvalidArgument(..))` without making a network request; `get()` is the only
method that works for both `me` and other users.

`get()` returns a `User` (from `apify_client::models`). Its fields — including the `user.id` and
`user.username` the [`get_account`](../examples/get_account.rs) example reads:

| Field | Type | Description |
|---|---|---|
| `id` | `String` | Unique user ID (always present). |
| `username` | `Option<String>` | Username. |
| `extra` | `Extra` | Any other fields returned by the API (more fields are present for `me` than for a public `user(id)`). |

```rust,no_run
# use apify_client::ApifyClient;
# async fn run(client: ApifyClient) -> Result<(), Box<dyn std::error::Error>> {
if let Some(user) = client.me().get().await? {
println!("account id {}, username {:?}", user.id, user.username);
}
# Ok(())
# }
```

`monthly_usage()` is shorthand for `monthly_usage_for_date(None)` (current cycle). The client
unwraps the API's `{ data: ... }` envelope, so the returned `Value` has the shape
`{ usageCycle: { startAt, endAt }, monthlyServiceUsage, dailyServiceUsages, ... }`. Billing
Expand Down Expand Up @@ -64,3 +111,27 @@ Also reachable via `run.log()` and `build.log()`.
|---|---|---|---|
| `get()` | — | `Option<String>` | The entire log as text. |
| `stream()` | — | `Stream<Item = Result<Vec<u8>>>` | Streams log chunks live (log redirection). |

Consuming `stream()` requires the [`futures_util::StreamExt`] trait (from the `futures-util`
crate) in scope to call `.next()` on the returned stream. Add it to your `Cargo.toml`:

```toml
[dependencies]
futures-util = "0.3"
```

Then redirect a run's log to stdout as it is produced:

```rust,no_run
use apify_client::ApifyClient;
use futures_util::StreamExt;

# async fn run(client: ApifyClient, run_id: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut stream = client.run(run_id).log().stream().await?;
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
print!("{}", String::from_utf8_lossy(&chunk));
}
# Ok(())
# }
```
49 changes: 49 additions & 0 deletions docs/runs.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,52 @@ collections are available via `actor.runs()` and `task.runs()`.
values are `READY`, `RUNNING`, `SUCCEEDED`, `FAILED`, `ABORTING`, `ABORTED`, `TIMING-OUT`, and
`TIMED-OUT`; the terminal ones (`SUCCEEDED`, `FAILED`, `ABORTED`, `TIMED-OUT`) are what
`is_terminal()` reports and what `wait_for_finish` polls for.

## `ActorRun` fields

`ActorRun` (from `apify_client::models`) is returned by `start`, `call`, `get`, `abort`,
`wait_for_finish`, and the run `list`. The fields most callers read — including the
`run.status` / `run.default_dataset_id` accessed in the [README Quick start](../README.md#quick-start):

| Field | Type | Description |
|---|---|---|
| `id` | `String` | Unique run ID (always present); used to build a `client.run(&run.id)` client. |
| `act_id` | `Option<String>` | ID of the Actor that produced the run. |
| `actor_task_id` | `Option<String>` | ID of the task that started the run, if any. |
| `user_id` | `Option<String>` | ID of the user who owns the run. |
| `status` | `Option<String>` | Current run status (see the status values above). |
| `status_message` | `Option<String>` | Optional human-readable status message. |
| `started_at` | `Option<DateTime<Utc>>` | When the run started. |
| `finished_at` | `Option<DateTime<Utc>>` | When the run finished (absent while running). |
| `build_id` | `Option<String>` | ID of the build used for the run. |
| `default_dataset_id` | `Option<String>` | Default dataset ID — pass to `client.dataset(..)` to read results. |
| `default_key_value_store_id` | `Option<String>` | Default key-value store ID for the run. |
| `default_request_queue_id` | `Option<String>` | Default request queue ID for the run. |
| `container_url` | `Option<String>` | URL of the run's container, while running. |
| `extra` | `Extra` | Any other fields returned by the API. |

The three `default_*_id` fields are `Option<String>` because they are only populated once the
run has its storages assigned; the storages are reachable directly via `run.dataset()`,
`run.key_value_store()` and `run.request_queue()` (see [storages](storages.md)).

```rust,no_run
use apify_client::ApifyClient;

# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let client = ApifyClient::new(std::env::var("APIFY_TOKEN")?);
let run = client
.actor("apify/hello-world")
.call::<serde_json::Value>(None, Default::default(), None)
.await?;

println!("run {} finished with status {:?}", run.id, run.status);
if let Some(dataset_id) = &run.default_dataset_id {
let items = client
.dataset(dataset_id)
.list_items::<serde_json::Value>(Default::default())
.await?;
println!("got {} item(s)", items.items.len());
}
# Ok(())
# }
```
Loading
Loading