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: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to the Apify Java client are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.1] - 2026-07-14

### Added

- Build-time version-drift guard test asserting the Maven project `<version>` (read from a filtered
build resource) equals `Version.CLIENT_VERSION`, so the two can no longer diverge on a release bump.

### Changed

- Verified the client against OpenAPI specification version `v2-2026-07-13T092445Z` and bumped
`Version.API_SPEC_VERSION`. The spec change only added already-handled error responses (`402`
Payment Required on actor and run-resurrect run-sync endpoints, `408` Request Timeout on the
task run-sync endpoint) and relaxed `required` constraints on run/build/key-value-store/webhook
stats counters; both are already covered by the client's generic status-code error handling and
its forward-compatible model deserialization, so no interface or behavior change was needed.

### Fixed

- Integration tests: create-then-iterate collection assertions now tolerate collection LIST-endpoint
eventual consistency via a bounded retry that rebuilds the iterator between attempts, fixing an
intermittent `IterationIntegrationTest.iterateRequestQueues` failure.
- Documentation: `docs/examples.md` fragments now match their standalone example files (Store
iteration output, the named-storage suffix scheme, and the account example's email line).

## [0.3.0] - 2026-07-11

### Added
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Maven (Maven Central is a default repository, so no extra configuration is neede
<dependency>
<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.3.0</version>
<version>0.3.1</version>
</dependency>
```

Expand All @@ -35,7 +35,7 @@ repositories {
}

dependencies {
implementation 'com.apify:apify-client:0.3.0'
implementation 'com.apify:apify-client:0.3.1'
}
```

Expand All @@ -57,7 +57,7 @@ A complete, copy-pasteable first program (save as `HelloApify.java`). First scaf
<dependency>
<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.3.0</version>
<version>0.3.1</version>
</dependency>
</dependencies>
</project>
Expand Down Expand Up @@ -189,9 +189,9 @@ try {
The public `com.apify.client.Version` class (`import com.apify.client.Version;`) exposes two
constants:

- `Version.CLIENT_VERSION` — the semantic version of this client (`0.3.0`).
- `Version.CLIENT_VERSION` — the semantic version of this client (`0.3.1`).
- `Version.API_SPEC_VERSION` — the Apify OpenAPI specification version this client was verified
against (`v2-2026-07-10T105921Z`).
against (`v2-2026-07-13T092445Z`).

Changes to the public interface other than additive ones are considered breaking changes and follow
[Semantic Versioning](https://semver.org/).
Expand Down
24 changes: 15 additions & 9 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ System.out.println("Items in this page: " + items.getCount());
```java
// Named storages persist on your account; each block deletes its storage in a finally so the
// example does not leak them. A unique suffix keeps the names from colliding across parallel runs.
String suffix = "-" + System.currentTimeMillis();
String suffix = Long.toString(System.currentTimeMillis());

// Dataset: create, push items, read them back.
Dataset dataset = client.datasets().getOrCreate("example-ds" + suffix);
Dataset dataset = client.datasets().getOrCreate("java-example-ds-" + suffix);
try {
client.dataset(dataset.getId()).pushItems(List.of(Map.of("hello", "world")));
PaginationList<JsonNode> items = client.dataset(dataset.getId()).listItems(new DatasetListItemsOptions());
Expand All @@ -39,7 +39,7 @@ try {
}

// Key-value store: create, set a record, read it back.
KeyValueStore store = client.keyValueStores().getOrCreate("example-kvs" + suffix);
KeyValueStore store = client.keyValueStores().getOrCreate("java-example-kvs-" + suffix);
try {
client.keyValueStore(store.getId()).setRecordJson("OUTPUT", Map.of("answer", 42));
Optional<KeyValueStoreRecord> record = client.keyValueStore(store.getId()).getRecord("OUTPUT");
Expand All @@ -49,7 +49,7 @@ try {
}

// Request queue: create, add a request, read the head.
RequestQueue queue = client.requestQueues().getOrCreate("example-rq" + suffix);
RequestQueue queue = client.requestQueues().getOrCreate("java-example-rq-" + suffix);
try {
client.requestQueue(queue.getId()).addRequest(new RequestQueueRequest("https://example.com", "example"), false);
RequestQueueHead head = client.requestQueue(queue.getId()).listHead(10L);
Expand All @@ -63,7 +63,12 @@ try {

```java
Optional<User> user = client.me().get();
user.ifPresent(u -> System.out.println("Account " + u.getId() + " / " + u.getUsername()));
user.ifPresent(u -> {
System.out.println("Account ID: " + u.getId());
System.out.println("Username: " + u.getUsername());
// Fields not modelled on User (email, plan, ...) live in the untyped extras map.
System.out.println("Email: " + u.getExtra().get("email"));
});
```

## Create a new Actor, build it, run it, wait, and print the finished run log
Expand Down Expand Up @@ -108,10 +113,11 @@ if (last.isPresent()) {

```java
Iterator<ActorStoreListItem> it = client.store().iterate(new StoreListOptions().limit(10L), 10L);
int shown = 0;
while (shown < 5 && it.hasNext()) {
System.out.println(it.next().getName());
shown++;
int count = 0;
while (count < 5 && it.hasNext()) {
ActorStoreListItem item = it.next();
System.out.println((count + 1) + ". " + item.getUsername() + "/" + item.getName());
count++;
}
```

Expand Down
16 changes: 15 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.apify</groupId>
<artifactId>apify-client</artifactId>
<version>0.3.0</version>
<version>0.3.1</version>
<packaging>jar</packaging>

<name>Apify Java Client</name>
Expand Down Expand Up @@ -105,6 +105,20 @@
</dependencies>

<build>
<!-- Filter apify-client-build.properties so ${project.version} is substituted at build
time. The version-drift test reads the filtered value and asserts it equals
Version.CLIENT_VERSION, guarding against pom.xml and the constant drifting apart on a
release bump. Kept in a dedicated directory so filtering never touches other resources. -->
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
<testResource>
<directory>src/test/resources-filtered</directory>
<filtering>true</filtering>
</testResource>
</testResources>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/apify/client/Version.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ public final class Version {
* The semantic version of this client library (see <a href="https://semver.org/">SemVer</a>).
* Changes to the public interface other than additive ones are considered breaking changes.
*/
public static final String CLIENT_VERSION = "0.3.0";
public static final String CLIENT_VERSION = "0.3.1";

/**
* The version of the Apify OpenAPI specification this client was generated and verified against.
* Corresponds to the {@code info.version} field of the Apify OpenAPI document.
*/
public static final String API_SPEC_VERSION = "v2-2026-07-10T105921Z";
public static final String API_SPEC_VERSION = "v2-2026-07-13T092445Z";

private Version() {}
}
31 changes: 31 additions & 0 deletions src/test/java/com/apify/client/ClientMetaTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import org.junit.jupiter.api.Test;

/** Offline tests for version constants, User-Agent formatting, and base-URL resolution. */
Expand All @@ -16,6 +20,33 @@ void versionConstants() {
assertTrue(Version.API_SPEC_VERSION.endsWith("Z"), Version.API_SPEC_VERSION);
}

@Test
void clientVersionMatchesBuildVersion() throws IOException {
// The Maven <version> in pom.xml and Version.CLIENT_VERSION must be bumped in lockstep on every
// release. Nothing else fails the build if they diverge, and version-bump PRs are exactly where
// that slips through, so this test compares Version.CLIENT_VERSION against the built
// project.version (substituted into a filtered resource during the build). Hermetic: reads a
// classpath resource, no network.
Properties props = new Properties();
try (InputStream in =
ClientMetaTest.class.getResourceAsStream("/apify-client-build.properties")) {
assertNotNull(
in,
"apify-client-build.properties missing from the test classpath; check the pom's filtered"
+ " testResource");
props.load(in);
}
String buildVersion = props.getProperty("project.version");
assertNotNull(buildVersion, "project.version not present in apify-client-build.properties");
assertFalse(
buildVersion.contains("${"),
"project.version was not filtered (still a Maven placeholder): " + buildVersion);
assertEquals(
buildVersion,
Version.CLIENT_VERSION,
"pom.xml <version> and Version.CLIENT_VERSION must match; bump them together");
}

@Test
void userAgentFormat() {
String ua = ApifyClient.create("token").getUserAgent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;

/**
Expand All @@ -42,6 +43,14 @@
*/
class IterationIntegrationTest extends IntegrationBase {

/**
* Attempts and backoff bounding how long {@link #findsAllEventually} waits for a freshly created
* resource to surface in its collection listing. Budget: {@code (ATTEMPTS - 1) * BACKOFF} = ~15s.
*/
private static final int ITER_FIND_ATTEMPTS = 16;

private static final long ITER_FIND_BACKOFF_MILLIS = 1000L;

/** Iterates until every target id is seen (lazily; stops early) or the iterator is exhausted. */
private static <T> boolean findsAll(
Iterator<T> it, Function<T, String> idOf, Set<String> targets) {
Expand All @@ -53,6 +62,35 @@ private static <T> boolean findsAll(
return remaining.isEmpty();
}

/**
* Like {@link #findsAll}, but tolerant of collection LIST-endpoint eventual consistency. A
* resource created through a write endpoint is not always immediately reflected in its
* collection's LIST response — the write and the list index converge asynchronously on the
* server, so a create-then-iterate assertion that scans exactly once races that convergence and
* flakes when the just-created entity has not yet propagated. This helper rebuilds a fresh
* iterator via {@code newIterator} and re-scans up to {@link #ITER_FIND_ATTEMPTS} times, sleeping
* {@link #ITER_FIND_BACKOFF_MILLIS} between attempts, returning as soon as every target id is
* seen. An already-consistent account matches on the first pass with no sleeping. Mirrors the
* sibling Go and Rust clients' tolerance for this exact intermittent failure.
*/
private static <T> boolean findsAllEventually(
Supplier<Iterator<T>> newIterator, Function<T, String> idOf, Set<String> targets) {
for (int attempt = 0; attempt < ITER_FIND_ATTEMPTS; attempt++) {
if (findsAll(newIterator.get(), idOf, targets)) {
return true;
}
if (attempt + 1 < ITER_FIND_ATTEMPTS) {
try {
Thread.sleep(ITER_FIND_BACKOFF_MILLIS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
return false;
}

@Test
void iterateDatasetItems() {
ApifyClient client = requireClient();
Expand Down Expand Up @@ -128,8 +166,12 @@ void iterateDatasets() {
}
// Page size 1 forces the offset iterator across multiple pages; desc puts the fresh ones
// first.
Iterator<Dataset> it = client.datasets().iterate(new StorageListOptions().desc(true), 1L);
assertTrue(findsAll(it, Dataset::getId, ids), "iterate should find every created dataset");
assertTrue(
findsAllEventually(
() -> client.datasets().iterate(new StorageListOptions().desc(true), 1L),
Dataset::getId,
ids),
"iterate should find every created dataset");
} finally {
for (String id : ids) {
client.dataset(id).delete();
Expand All @@ -145,9 +187,11 @@ void iterateKeyValueStores() {
for (int i = 0; i < 2; i++) {
ids.add(client.keyValueStores().getOrCreate(uniqueName("it-kvs-" + i)).getId());
}
Iterator<KeyValueStore> it =
client.keyValueStores().iterate(new StorageListOptions().desc(true), 1L);
assertTrue(findsAll(it, KeyValueStore::getId, ids));
assertTrue(
findsAllEventually(
() -> client.keyValueStores().iterate(new StorageListOptions().desc(true), 1L),
KeyValueStore::getId,
ids));
} finally {
for (String id : ids) {
client.keyValueStore(id).delete();
Expand All @@ -163,9 +207,11 @@ void iterateRequestQueues() {
for (int i = 0; i < 2; i++) {
ids.add(client.requestQueues().getOrCreate(uniqueName("it-rq-" + i)).getId());
}
Iterator<RequestQueue> it =
client.requestQueues().iterate(new StorageListOptions().desc(true), 1L);
assertTrue(findsAll(it, RequestQueue::getId, ids));
assertTrue(
findsAllEventually(
() -> client.requestQueues().iterate(new StorageListOptions().desc(true), 1L),
RequestQueue::getId,
ids));
} finally {
for (String id : ids) {
client.requestQueue(id).delete();
Expand All @@ -182,8 +228,9 @@ void iterateTasks() {
ids.add(
client.tasks().create(TaskIntegrationTest.taskDef(uniqueName("it-task-" + i))).getId());
}
Iterator<Task> it = client.tasks().iterate(new ListOptions().desc(true), 1L);
assertTrue(findsAll(it, Task::getId, ids));
assertTrue(
findsAllEventually(
() -> client.tasks().iterate(new ListOptions().desc(true), 1L), Task::getId, ids));
} finally {
for (String id : ids) {
client.task(id).delete();
Expand All @@ -203,8 +250,11 @@ void iterateSchedules() {
.create(ScheduleIntegrationTest.scheduleDef(uniqueName("it-sch-" + i)))
.getId());
}
Iterator<Schedule> it = client.schedules().iterate(new ListOptions().desc(true), 1L);
assertTrue(findsAll(it, Schedule::getId, ids));
assertTrue(
findsAllEventually(
() -> client.schedules().iterate(new ListOptions().desc(true), 1L),
Schedule::getId,
ids));
} finally {
for (String id : ids) {
client.schedule(id).delete();
Expand All @@ -224,8 +274,11 @@ void iterateWebhooks() {
.create(WebhookIntegrationTest.webhookDef("https://example.com/it-wh-" + i))
.getId());
}
Iterator<Webhook> it = client.webhooks().iterate(new ListOptions().desc(true), 1L);
assertTrue(findsAll(it, Webhook::getId, ids));
assertTrue(
findsAllEventually(
() -> client.webhooks().iterate(new ListOptions().desc(true), 1L),
Webhook::getId,
ids));
} finally {
for (String id : ids) {
client.webhook(id).delete();
Expand All @@ -246,8 +299,11 @@ void iterateActors() {
.getId());
}
// Restrict to the current user's Actors so iteration finds the freshly-created ones quickly.
Iterator<Actor> it = client.actors().iterate(new ActorListOptions().my(true).desc(true), 1L);
assertTrue(findsAll(it, Actor::getId, ids));
assertTrue(
findsAllEventually(
() -> client.actors().iterate(new ActorListOptions().my(true).desc(true), 1L),
Actor::getId,
ids));
} finally {
for (String id : ids) {
client.actor(id).delete();
Expand Down
4 changes: 4 additions & 0 deletions src/test/resources-filtered/apify-client-build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by Maven resource filtering during the build. The value is substituted from the
# project's <version> in pom.xml, so the version-drift test can compare it against
# Version.CLIENT_VERSION without hardcoding either side. Do not edit by hand.
project.version=${project.version}
Loading