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
76 changes: 76 additions & 0 deletions .github/workflows/rust-integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Rust integration tests

# Language-specific workflow: only runs for the Rust client. Triggers on PRs to master
# that touch Rust client or test code, and can be dispatched manually from any branch.
on:
pull_request:
branches: [master]
paths:
- 'src/**'
- 'tests/**'
- 'examples/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/rust-integration-tests.yml'
workflow_dispatch:

# Avoid concurrent runs of the same ref racing on the shared test account.
concurrency:
group: rust-integration-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Cache cargo registry and build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}

- name: Check formatting
run: cargo fmt --all -- --check

- name: Clippy (deny warnings)
run: cargo clippy --all-targets -- -D warnings

- name: Build
run: cargo build --verbose

# Fail fast if the integration-test secret is missing or empty. Without this guard the
# integration tests silently "pass" (they early-return when APIFY_TOKEN is unset), so a
# green run would not prove the API logic actually executed.
- name: Require APIFY_TOKEN secret
env:
APIFY_TOKEN: ${{ secrets.APIFY_TOKEN }}
run: |
if [ -z "${APIFY_TOKEN}" ]; then
echo "::error::APIFY_TOKEN secret is empty or missing; integration tests would not run against the API."
exit 1
fi

- name: Run integration tests
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

- name: Run documentation example tests
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
22 changes: 2 additions & 20 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
debug
target

# These are backup files generated by rustfmt
/target/
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
Cargo.lock
117 changes: 117 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Changelog

All notable changes to the Rust Apify API client are documented here. The format is
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
to [Semantic Versioning](https://semver.org/).

## [0.1.0] - 2026-06-18

Initial release of the official Rust client for the Apify API.

### Added
- Resource-oriented async client (`ApifyClient` + `ApifyClientBuilder`) mirroring the
official JavaScript and Python clients.
- Resource clients for Actors, Actor builds, Actor runs, Actor tasks, datasets, key-value
stores, request queues, schedules, webhooks, webhook dispatches, the Apify Store, users,
Actor versions and environment variables, and logs.
- Convenience helpers: `actor.call` / `task.call` (start + wait + run), `run.wait_for_finish`
and `build.wait_for_finish`, dataset push/list items, key-value store records, request
queue operations, log retrieval and streaming (log redirection), and lazy iteration of
Apify Store Actors (`store().iterate()`).
- Replaceable HTTP transport via the `HttpBackend` trait with a default reqwest backend.
- Automatic Bearer authentication, the mandated `User-Agent` header, and transparent
retries with exponential backoff on `429`/`5xx`/network errors.
- Public version constants `CLIENT_VERSION` and `API_SPEC_VERSION`
(`v2-2026-06-16T064758Z`).
- Request-queue lock lifecycle: `list_requests`, `list_and_lock_head`, `prolong_request_lock`,
`delete_request_lock`, `unlock_requests`, plus `batch_add_requests` / `batch_delete_requests`.
- Dataset `download_items` (JSON/CSV/XLSX/XML/RSS/HTML export) and `get_statistics`.
- `KeyValueStoreClient::get_records` — downloads all records from a store as a ZIP archive
(`GET /v2/key-value-stores/{storeId}/records`), with `collection`/`prefix`/`signature`
filtering via `GetRecordsOptions`. Reachable on run/task default stores via the nested
`key_value_store()` accessor.
- Build `get_openapi_definition`.
- `ApifyClient::set_status_message` for setting the current run's status from inside an Actor.
- Public, shareable resource URLs with HMAC-SHA256 signing for private resources:
`DatasetClient::create_items_public_url`, `KeyValueStoreClient::get_record_public_url` /
`create_keys_public_url`, plus a configurable `public_base_url`.
- Format-specific dataset export options via `DatasetDownloadOptions`
(`attachment`, `bom`, `delimiter`, `skip_header_row`, `xml_root`, `xml_row`).
- Full parameter coverage on list/start endpoints (`my`/`sortBy` for Actors; `webhooks`,
`restartOnError`, `forcePermissionLevel` for run start; `unwind`/`flatten`/`view`/etc. for
dataset items; `includeUnrunnableActors` for store; `attachment` for KVS records).
- Offline unit tests (mock `HttpBackend`) covering retry counting, 429/5xx retry vs. 4xx
no-retry, network-error retry, backoff, error-envelope parsing and 404→None mapping.
- Integration test suite covering simple GETs and full CRUD flows for each resource.
- GitHub Actions workflow running formatting, clippy, build and integration tests.

### Fixed
- Percent-encode URL path segments (key-value-store record keys, request-queue request IDs)
so keys containing `/`, `?`, `#`, spaces or non-ASCII no longer produce malformed URLs.
- `User-Agent` `isAtHome` flag now reads **both** the platform variable `APIFY_IS_AT_HOME`
(matching the JS reference) and the bare `isAtHome` name from `client_requirements.md`; either
being set marks the client "at home". These two same-priority requirements conflicted, so the
client honours both (consistent with the Go sibling). The flag is rendered lowercase
(`true`/`false`) to stay byte-consistent with the JS reference.
- `get_record` now sends `attachment=true`, matching the reference client's `getRecord`
(which sends `attachment=true` unconditionally). `get_record_with_options` takes a
`GetRecordOptions { attachment, signature }`; `attachment` defaults to `true` when unset.
- Request-queue request pagination (`paginate_requests` / `RequestQueueRequestsIterator`) now
feeds the opaque `nextCursor` back as the `cursor` query parameter on subsequent pages
(matching the JS reference) instead of misusing it as `exclusiveStartId`. Previously
pagination broke past the first page (duplicate/missing items or premature stop).
- `User-Agent` `{language version}` now reports the real compiler version captured at build
time (via `build.rs` running `rustc --version`) instead of rendering `Rust/unknown` (it
previously read the unset MSRV field `CARGO_PKG_RUST_VERSION`).

### Changed
- Added previously-missing spec query parameters: dataset items `outputFields` (list/download)
and `feedTitle`/`feedDescription` (download); key-value-store keys `collection`/`signature`;
key-value-store record `signature` (via `GetRecordOptions`); request-queue requests
`cursor`/`filter` (via `ListRequestsOptions`); storage collection list `unnamed`/`ownership`
(via `StorageListOptions`); run collection list `startedAfter`/`startedBefore` (via
`RunListOptions`).
- Added the `signature` query parameter to dataset items (`DatasetListItemsOptions`, inherited
by `DatasetDownloadOptions`), so `list_items` / `download_items` can fetch items from a
private dataset using a pre-shared signature (spec + JS reference both expose it).
- `get_record_with_options` signature changed to take `GetRecordOptions` (was `attachment: bool`),
exposing the spec `signature` param for reading records from private stores.
- `ActorClient::default_build` now takes a `wait_for_finish: Option<i64>` argument (matching the
reference client's `defaultBuild(options)`), optionally bounding how long the API waits for the
default build to finish.
- Backoff doubling factor extracted to a named constant.
- Cross-client consistency with the JS reference and the OpenAPI spec (aligning with the Go
sibling):
- `RunClient::charge` now takes `RunChargeOptions { event_name, count, idempotency_key }` and
always sends an `idempotency-key` header (auto-generated as
`{runId}-{eventName}-{millis}-{random}` when omitted), so a transport-retried charge is
applied at most once. (Was `charge(event_name, count)` with no idempotency key.)
- `RunClient::metamorph` now takes `RunMetamorphOptions { build, content_type }`, letting the
caller set the input body content type (defaults to `application/json`). (Was
`metamorph(target, input, build)`.)
- `RunResurrectOptions` gained `max_items`, `max_total_charge_usd`, and `restart_on_error`
(all declared by `POST /v2/actor-runs/{runId}/resurrect` and supported by the JS reference).
- `RunListOptions::status` is now `Vec<String>` (sent comma-separated), so multiple run
statuses can be filtered in one call, matching the spec's array `status` parameter. (Was a
single `Option<String>`.)
- `RunClient::abort` now takes `gracefully: Option<bool>` instead of `bool`. Passing `None`
omits the `gracefully` query parameter (letting the server apply its default, immediate
abort), matching the reference client's optional `gracefully` option and the Go sibling.
- `RequestQueueClient::batch_add_requests` now splits inputs larger than the API's 25-per-call
limit into chunks and merges the per-chunk `processedRequests`/`unprocessedRequests` results,
matching the reference client's client-side chunking. (Was a single raw POST of the whole
slice, which could exceed API limits for large batches.)

### Notes
- A few documented endpoints are intentionally not exposed (matching the JS reference):
synchronous run endpoints, `/tools/*`, `/browser-info`, and the keyed-`POST` create variants
for Actor versions and version env-vars (creation is via `POST` to the collection, upsert via
`PUT` on the keyed path).
- The JS `listItems` `chunkSize` option is intentionally not exposed: it is a client-side
hint controlling the per-request page size of the JS async-iterator, not an API query
parameter. The Rust `list_items` returns a single `PaginationList` page (the caller controls
the page size via `limit`), so `chunkSize` has no analogue here.
- The `POST` store-record alias `POST /v2/key-value-stores/{storeId}/records/{recordKey}` is
intentionally not exposed: the spec defines it as behaving identically to the covered `PUT`
variant, and the reference client stores records via `PUT` only. Records are stored with
`set_record_raw` / `set_record_json`.
34 changes: 34 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "apify-client"
version = "0.1.0"
authors = ["Apify Technologies <support@apify.com>"]
description = "The official Rust client for the Apify API (https://apify.com)."
license = "Apache-2.0"
edition = "2021"
repository = "https://github.com/apify/apify-client-rust"
documentation = "https://docs.rs/apify-client"
readme = "README.md"
keywords = ["apify", "api", "client", "scraping", "automation"]
categories = ["api-bindings", "web-programming::http-client"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream"] }
chrono = { version = "0.4", features = ["serde"] }
thiserror = "2.0"
async-trait = "0.1"
futures-util = "0.3"
tokio = { version = "1", features = ["time"] }
base64 = "0.22"
hmac = "0.12"
sha2 = "0.10"

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
uuid = { version = "1", features = ["v4"] }

[features]
default = ["native-tls"]
native-tls = ["reqwest/native-tls"]
rustls = ["reqwest/rustls-tls"]
Loading
Loading