diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9051fdd2..42805473 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,18 +1,24 @@
name: CI
on:
push:
- branches:
- - main
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
pull_request:
- branches:
- - main
- - next
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
- timeout-minutes: 10
+ timeout-minutes: 15
name: lint
- runs-on: ubuntu-latest
+ runs-on: ${{ github.repository == 'stainless-sdks/braintrust-sdk-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+
steps:
- uses: actions/checkout@v4
@@ -22,7 +28,7 @@ jobs:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
@@ -30,10 +36,36 @@ jobs:
- name: Run lints
run: ./scripts/lint
+
+ build:
+ timeout-minutes: 15
+ name: build
+ runs-on: ${{ github.repository == 'stainless-sdks/braintrust-sdk-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build SDK
+ run: ./scripts/build
+
test:
- timeout-minutes: 10
+ timeout-minutes: 15
name: test
- runs-on: ubuntu-latest
+ runs-on: ${{ github.repository == 'stainless-sdks/braintrust-sdk-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
steps:
- uses: actions/checkout@v4
@@ -43,7 +75,7 @@ jobs:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
index bab4aea6..bb52d80e 100755
--- a/.github/workflows/publish-sonatype.yml
+++ b/.github/workflows/publish-sonatype.yml
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v4
- name: Set up Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
diff --git a/.gitignore b/.gitignore
index 4e81838d..b1346e6d 100755
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,6 @@
.gradle
.idea
.kotlin
-build
+build/
codegen.log
kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 6d78745c..091cfb12 100755
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.9.0"
+ ".": "0.10.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9d47d1d9..7e2ce2ee 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,90 @@
# Changelog
+## 0.10.0 (2025-08-22)
+
+Full Changelog: [v0.9.0...v0.10.0](https://github.com/braintrustdata/braintrust-java/compare/v0.9.0...v0.10.0)
+
+### ⚠ BREAKING CHANGES
+
+* **client:** extract auto pagination to shared classes
+* **client:** **Migration:** - If you were referencing the `AutoPager` class on a specific `*Page` or `*PageAsync` type, then you should instead reference the shared `AutoPager` and `AutoPagerAsync` types, under the `core` package
+ - `AutoPagerAsync` now has different usage. You can call `.subscribe(...)` on the returned object instead to get called back each page item. You can also call `onCompleteFuture()` to get a future that completes when all items have been processed. Finally, you can call `.close()` on the returned object to stop auto-paginating early
+ - If you were referencing `getNextPage` or `getNextPageParams`:
+ - Swap to `nextPage()` and `nextPageParams()`
+ - Note that these both now return non-optional types (use `hasNextPage()` before calling these, since they will throw if it's impossible to get another page)
+
+### Features
+
+* add retryable exception ([d8e7b05](https://github.com/braintrustdata/braintrust-java/commit/d8e7b05c0d314fe4ec457f92c60df6580af5c2e6))
+* **client:** add `{QueryParams,Headers}#put(String, JsonValue)` methods ([2ed0bd5](https://github.com/braintrustdata/braintrust-java/commit/2ed0bd5ddc28eddf3d9f034544683d0b1eaf41a5))
+* **client:** add a `withOptions` method ([0fd4355](https://github.com/braintrustdata/braintrust-java/commit/0fd435598218e1d60f93cdc056a007b7c42a77ed))
+* **client:** add https config options ([6730e8b](https://github.com/braintrustdata/braintrust-java/commit/6730e8b831a69ca8932e33749b3c4f91b90c17da))
+* **client:** allow configuring env via system properties ([66d1af9](https://github.com/braintrustdata/braintrust-java/commit/66d1af9d64fc2ec585dc56b0e5cc2020ca9fe0f0))
+* **client:** allow providing some params positionally ([decbdbc](https://github.com/braintrustdata/braintrust-java/commit/decbdbcb64f695324a84e992c1c5abec39d01061))
+* **client:** ensure compat with proguard ([976cf03](https://github.com/braintrustdata/braintrust-java/commit/976cf03afcba30e713b8a3412a146df71662896f))
+* **client:** extract auto pagination to shared classes ([e9248c0](https://github.com/braintrustdata/braintrust-java/commit/e9248c0543f113372cafa9518da4afa861154ec8))
+* **client:** implement per-endpoint base URL support ([729a979](https://github.com/braintrustdata/braintrust-java/commit/729a9797b5eb5f0031ff4f9ecc5d23e750bb58a3))
+
+
+### Bug Fixes
+
+* **ci:** release-doctor — report correct token name ([76e5e15](https://github.com/braintrustdata/braintrust-java/commit/76e5e15496845894737ef7cce7a4b5f45b4c9627))
+* **client:** accidental mutability of some classes ([95b2515](https://github.com/braintrustdata/braintrust-java/commit/95b25154cb5f8305654068b5f933f483e2356e07))
+* **client:** bump max requests per host to max requests (5 -> 64) ([ce8a40c](https://github.com/braintrustdata/braintrust-java/commit/ce8a40c62024ba63ed82b36c6f5c5f40cbc45486))
+* **client:** don't close client on `withOptions` usage when original is gc'd ([c17580b](https://github.com/braintrustdata/braintrust-java/commit/c17580bd1187eee832f3d01a0632e8a343eee909))
+* **client:** ensure error handling always occurs ([53fa1ca](https://github.com/braintrustdata/braintrust-java/commit/53fa1ca6ab6c5328dd4e96e0f208a550f0eabd91))
+* **client:** r8 support ([f06bc35](https://github.com/braintrustdata/braintrust-java/commit/f06bc35d0f3cba5ddaadfcb8d67ec27443ec03a9))
+* **client:** remove `@MustBeClosed` for future returning methods ([5d19342](https://github.com/braintrustdata/braintrust-java/commit/5d19342d1f83132910b16047a39629b08576a201))
+* update singularization rules ([fd5a6f2](https://github.com/braintrustdata/braintrust-java/commit/fd5a6f2e0790d55e01b52ade42d0874034554bdb))
+
+
+### Performance Improvements
+
+* **internal:** make formatting faster ([80067f7](https://github.com/braintrustdata/braintrust-java/commit/80067f7e4f450cdc259a457a97f9691fcab7f11d))
+
+
+### Chores
+
+* **ci:** add build job ([3e6fe1e](https://github.com/braintrustdata/braintrust-java/commit/3e6fe1ebc28c61201c870f4e7068d3e1972ba80c))
+* **ci:** bump `actions/setup-java` to v4 ([3769b89](https://github.com/braintrustdata/braintrust-java/commit/3769b89c224b437df98fdbb28f0cbd8d097022d6))
+* **ci:** enable for pull requests ([3e67a54](https://github.com/braintrustdata/braintrust-java/commit/3e67a5462e640fea77f782aff31047f3ccea099f))
+* **ci:** ensure docs generation always succeeds ([266ed1d](https://github.com/braintrustdata/braintrust-java/commit/266ed1d813a06fc1b73201923da77d6bc2850eda))
+* **ci:** only run for pushes and fork pull requests ([7437b68](https://github.com/braintrustdata/braintrust-java/commit/7437b686f0b58d761465567d88e242ffad67012b))
+* **ci:** only use depot for staging repos ([fc25618](https://github.com/braintrustdata/braintrust-java/commit/fc25618f693dd09f1de17b6c53a479fac1863b03))
+* **ci:** reduce log noise ([0b93a6a](https://github.com/braintrustdata/braintrust-java/commit/0b93a6ac62936f46da8a9ce743ef8971a3a45a79))
+* **client:** refactor closing / shutdown ([09819df](https://github.com/braintrustdata/braintrust-java/commit/09819df78d9560f2dd6024c90df88e7754be8191))
+* **docs:** grammar improvements ([f8dd702](https://github.com/braintrustdata/braintrust-java/commit/f8dd702521b2400d82bacafeba88cc85cb7e94f2))
+* **example:** fix run example comment ([ab5aa6c](https://github.com/braintrustdata/braintrust-java/commit/ab5aa6c6afdfea06c9d14ebc27cd7ec79a69081a))
+* increase max gradle JVM heap to 8GB ([00333f4](https://github.com/braintrustdata/braintrust-java/commit/00333f406e3d855e519d9c52110589bc8d02e1ae))
+* **internal:** add async lock helper ([5904a7a](https://github.com/braintrustdata/braintrust-java/commit/5904a7af5e0942a5025ee76b9c9f4264c594da56))
+* **internal:** allow running specific example from cli ([1f6ea51](https://github.com/braintrustdata/braintrust-java/commit/1f6ea51f9017c4790c3bc88633f994840a917023))
+* **internal:** bump ci test timeout ([3d46fc5](https://github.com/braintrustdata/braintrust-java/commit/3d46fc54a39e825f75e16fd4698b14feb9259b35))
+* **internal:** codegen related update ([23e9c7b](https://github.com/braintrustdata/braintrust-java/commit/23e9c7be91a92814d984eb8ee14f8b640c65fbe3))
+* **internal:** codegen related update ([ee51402](https://github.com/braintrustdata/braintrust-java/commit/ee51402c535e190bc5800086e02f7c184f1d7d9b))
+* **internal:** dynamically determine included projects ([8129561](https://github.com/braintrustdata/braintrust-java/commit/8129561686f8808b8871009844056fafd99502e9))
+* **internal:** java 17 -> 21 on ci ([fc13394](https://github.com/braintrustdata/braintrust-java/commit/fc13394a9cf52b353036f153e2cf5a0f53343750))
+* **internal:** reduce proguard ci logging ([2c716b1](https://github.com/braintrustdata/braintrust-java/commit/2c716b1510cbebf94570204533b8e1aaef11db80))
+* **internal:** refactor delegating from client to options ([70faee4](https://github.com/braintrustdata/braintrust-java/commit/70faee4a69f3a382336d30ef7d52737a2d6ab560))
+* **internal:** remove flaky `-Xbackend-threads=0` option ([96fdae2](https://github.com/braintrustdata/braintrust-java/commit/96fdae2700801166f887a8195e36029ee15d4aa3))
+* **internal:** remove unnecessary `[...]` in `[@see](https://github.com/see)` ([a0ef543](https://github.com/braintrustdata/braintrust-java/commit/a0ef543b879834083db1b17103a03962b1734edf))
+* **internal:** support passing arguments to test script ([87f7a5f](https://github.com/braintrustdata/braintrust-java/commit/87f7a5f68f1b67008daccc7dc93238af71f1ab3e))
+* **internal:** support running formatters directly ([6a03471](https://github.com/braintrustdata/braintrust-java/commit/6a034710297d455d4601a5a2845c149592bc7da3))
+* **internal:** update comment in script ([b73560b](https://github.com/braintrustdata/braintrust-java/commit/b73560bfafbf96a366e0a9d44e25ccc0a4704bfc))
+* **internal:** update java toolchain ([7c8e53c](https://github.com/braintrustdata/braintrust-java/commit/7c8e53c86b4733074c59ca9de3d8a7fb63070350))
+* remove memory upper bound from publishing step ([1efb5d6](https://github.com/braintrustdata/braintrust-java/commit/1efb5d642e574d1e589734c7460fd8e87dd4389f))
+* update @stainless-api/prism-cli to v5.15.0 ([dba7eae](https://github.com/braintrustdata/braintrust-java/commit/dba7eaed2f677029109085d15355e048efaa006d))
+
+
+### Documentation
+
+* fix missing readme comment ([4cf9617](https://github.com/braintrustdata/braintrust-java/commit/4cf9617412a0abf92433d8435728f2b8e77058a7))
+* more code comments ([7a93a83](https://github.com/braintrustdata/braintrust-java/commit/7a93a8349115484ba0f073cf60e8084ed9c8be51))
+
+
+### Refactors
+
+* **internal:** minor `ClientOptionsTest` change ([2e7263e](https://github.com/braintrustdata/braintrust-java/commit/2e7263e6927f66064c7609f99c1073381bbc2300))
+
## 0.9.0 (2025-04-23)
Full Changelog: [v0.8.0...v0.9.0](https://github.com/braintrustdata/braintrust-java/compare/v0.8.0...v0.9.0)
diff --git a/README.md b/README.md
index 18106805..c42de50c 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-[](https://central.sonatype.com/artifact/com.braintrustdata.api/braintrust-java/0.9.0)
-[](https://javadoc.io/doc/com.braintrustdata.api/braintrust-java/0.9.0)
+[](https://central.sonatype.com/artifact/com.braintrustdata.api/braintrust-java/0.10.0)
+[](https://javadoc.io/doc/com.braintrustdata.api/braintrust-java/0.10.0)
@@ -15,7 +15,7 @@ It is generated with [Stainless](https://www.stainless.com/).
-The REST API documentation can be found on [www.braintrustdata.com](https://www.braintrustdata.com/docs/api/spec). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.braintrustdata.api/braintrust-java/0.9.0).
+The REST API documentation can be found on [www.braintrustdata.com](https://www.braintrustdata.com/docs/api/spec). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.braintrustdata.api/braintrust-java/0.10.0).
@@ -26,7 +26,7 @@ The REST API documentation can be found on [www.braintrustdata.com](https://www.
### Gradle
```kotlin
-implementation("com.braintrustdata.api:braintrust-java:0.9.0")
+implementation("com.braintrustdata.api:braintrust-java:0.10.0")
```
### Maven
@@ -35,7 +35,7 @@ implementation("com.braintrustdata.api:braintrust-java:0.9.0")
com.braintrustdata.api
braintrust-java
- 0.9.0
+ 0.10.0
```
@@ -53,7 +53,8 @@ import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
import com.braintrustdata.api.models.Project;
import com.braintrustdata.api.models.ProjectCreateParams;
-// Configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
+// Configures using the `braintrust.apiKey` and `braintrust.baseUrl` system properties
+// Or configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
BraintrustClient client = BraintrustOkHttpClient.fromEnv();
ProjectCreateParams params = ProjectCreateParams.builder()
@@ -64,13 +65,14 @@ Project project = client.projects().create(params);
## Client configuration
-Configure the client using environment variables:
+Configure the client using system properties or environment variables:
```java
import com.braintrustdata.api.client.BraintrustClient;
import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
-// Configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
+// Configures using the `braintrust.apiKey` and `braintrust.baseUrl` system properties
+// Or configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
BraintrustClient client = BraintrustOkHttpClient.fromEnv();
```
@@ -92,7 +94,8 @@ import com.braintrustdata.api.client.BraintrustClient;
import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
BraintrustClient client = BraintrustOkHttpClient.builder()
- // Configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
+ // Configures using the `braintrust.apiKey` and `braintrust.baseUrl` system properties
+ // Or configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
.fromEnv()
.apiKey("My API Key")
.build();
@@ -100,15 +103,32 @@ BraintrustClient client = BraintrustOkHttpClient.builder()
See this table for the available options:
-| Setter | Environment variable | Required | Default value |
-| --------- | --------------------- | -------- | ------------------------------ |
-| `apiKey` | `BRAINTRUST_API_KEY` | false | - |
-| `baseUrl` | `BRAINTRUST_BASE_URL` | true | `"https://api.braintrust.dev"` |
+| Setter | System property | Environment variable | Required | Default value |
+| --------- | -------------------- | --------------------- | -------- | ------------------------------ |
+| `apiKey` | `braintrust.apiKey` | `BRAINTRUST_API_KEY` | false | - |
+| `baseUrl` | `braintrust.baseUrl` | `BRAINTRUST_BASE_URL` | true | `"https://api.braintrust.dev"` |
+
+System properties take precedence over environment variables.
> [!TIP]
> Don't create more than one client in the same application. Each client has a connection pool and
> thread pools, which are more efficient to share between requests.
+### Modifying configuration
+
+To temporarily use a modified client configuration, while reusing the same connection and thread pools, call `withOptions()` on any client or service:
+
+```java
+import com.braintrustdata.api.client.BraintrustClient;
+
+BraintrustClient clientWithOptions = client.withOptions(optionsBuilder -> {
+ optionsBuilder.baseUrl("https://example.com");
+ optionsBuilder.maxRetries(42);
+});
+```
+
+The `withOptions()` method does not affect the original client or service.
+
## Requests and responses
To send a request to the Braintrust API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
@@ -134,7 +154,8 @@ import com.braintrustdata.api.models.Project;
import com.braintrustdata.api.models.ProjectCreateParams;
import java.util.concurrent.CompletableFuture;
-// Configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
+// Configures using the `braintrust.apiKey` and `braintrust.baseUrl` system properties
+// Or configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
BraintrustClient client = BraintrustOkHttpClient.fromEnv();
ProjectCreateParams params = ProjectCreateParams.builder()
@@ -152,7 +173,8 @@ import com.braintrustdata.api.models.Project;
import com.braintrustdata.api.models.ProjectCreateParams;
import java.util.concurrent.CompletableFuture;
-// Configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
+// Configures using the `braintrust.apiKey` and `braintrust.baseUrl` system properties
+// Or configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
BraintrustClientAsync client = BraintrustOkHttpClientAsync.fromEnv();
ProjectCreateParams params = ProjectCreateParams.builder()
@@ -211,59 +233,109 @@ The SDK throws custom unchecked exception types:
- [`BraintrustIoException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustIoException.kt): I/O networking errors.
+- [`BraintrustRetryableException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustRetryableException.kt): Generic error indicating a failure that could be retried by the client.
+
- [`BraintrustInvalidDataException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
- [`BraintrustException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
## Pagination
-For methods that return a paginated list of results, this library provides convenient ways access the results either one page at a time, or item-by-item across all pages.
+The SDK defines methods that return a paginated lists of results. It provides convenient ways to access the results either one page at a time or item-by-item across all pages.
### Auto-pagination
-To iterate through all results across all pages, you can use `autoPager`, which automatically handles fetching more pages for you:
+To iterate through all results across all pages, use the `autoPager()` method, which automatically fetches more pages as needed.
-### Synchronous
+When using the synchronous client, the method returns an [`Iterable`](https://docs.oracle.com/javase/8/docs/api/java/lang/Iterable.html)
```java
import com.braintrustdata.api.models.Project;
import com.braintrustdata.api.models.ProjectListPage;
-// As an Iterable:
-ProjectListPage page = client.projects().list(params);
+ProjectListPage page = client.projects().list();
+
+// Process as an Iterable
for (Project project : page.autoPager()) {
System.out.println(project);
-};
+}
-// As a Stream:
-client.projects().list(params).autoPager().stream()
+// Process as a Stream
+page.autoPager()
+ .stream()
.limit(50)
.forEach(project -> System.out.println(project));
```
-### Asynchronous
+When using the asynchronous client, the method returns an [`AsyncStreamResponse`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/AsyncStreamResponse.kt):
```java
-// Using forEach, which returns CompletableFuture:
-asyncClient.projects().list(params).autoPager()
- .forEach(project -> System.out.println(project), executor);
+import com.braintrustdata.api.core.http.AsyncStreamResponse;
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectListPageAsync;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+CompletableFuture pageFuture = client.async().projects().list();
+
+pageFuture.thenRun(page -> page.autoPager().subscribe(project -> {
+ System.out.println(project);
+}));
+
+// If you need to handle errors or completion of the stream
+pageFuture.thenRun(page -> page.autoPager().subscribe(new AsyncStreamResponse.Handler<>() {
+ @Override
+ public void onNext(Project project) {
+ System.out.println(project);
+ }
+
+ @Override
+ public void onComplete(Optional error) {
+ if (error.isPresent()) {
+ System.out.println("Something went wrong!");
+ throw new RuntimeException(error.get());
+ } else {
+ System.out.println("No more!");
+ }
+ }
+}));
+
+// Or use futures
+pageFuture.thenRun(page -> page.autoPager()
+ .subscribe(project -> {
+ System.out.println(project);
+ })
+ .onCompleteFuture()
+ .whenComplete((unused, error) -> {
+ if (error != null) {
+ System.out.println("Something went wrong!");
+ throw new RuntimeException(error);
+ } else {
+ System.out.println("No more!");
+ }
+ }));
```
### Manual pagination
-If none of the above helpers meet your needs, you can also manually request pages one-by-one. A page of results has a `data()` method to fetch the list of objects, as well as top-level `response` and other methods to fetch top-level data about the page. It also has methods `hasNextPage`, `getNextPage`, and `getNextPageParams` methods to help with pagination.
+To access individual page items and manually request the next page, use the `items()`,
+`hasNextPage()`, and `nextPage()` methods:
```java
import com.braintrustdata.api.models.Project;
import com.braintrustdata.api.models.ProjectListPage;
-ProjectListPage page = client.projects().list(params);
-while (page != null) {
- for (Project project : page.objects()) {
+ProjectListPage page = client.projects().list();
+while (true) {
+ for (Project project : page.items()) {
System.out.println(project);
}
- page = page.getNextPage().orElse(null);
+ if (!page.hasNextPage()) {
+ break;
+ }
+
+ page = page.nextPage();
}
```
@@ -283,6 +355,12 @@ Or to `debug` for more verbose logging:
$ export BRAINTRUST_LOG=debug
```
+## ProGuard and R8
+
+Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `braintrust-java-core` is published with a [configuration file](braintrust-java-core/src/main/resources/META-INF/proguard/braintrust-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
+
+ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
+
## Jackson
The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
@@ -298,7 +376,7 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t
### Retries
-The SDK automatically retries 2 times by default, with a short exponential backoff.
+The SDK automatically retries 2 times by default, with a short exponential backoff between requests.
Only the following error types are retried:
@@ -308,7 +386,7 @@ Only the following error types are retried:
- 429 Rate Limit
- 5xx Internal
-The API may also explicitly instruct the SDK to retry or not retry a response.
+The API may also explicitly instruct the SDK to retry or not retry a request.
To set a custom number of retries, configure the client using the `maxRetries` method:
@@ -330,7 +408,6 @@ To set a custom timeout, configure the method call using the `timeout` method:
```java
import com.braintrustdata.api.models.Project;
-import com.braintrustdata.api.models.ProjectCreateParams;
Project project = client.projects().create(
params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
@@ -370,6 +447,27 @@ BraintrustClient client = BraintrustOkHttpClient.builder()
.build();
```
+### HTTPS
+
+> [!NOTE]
+> Most applications should not call these methods, and instead use the system defaults. The defaults include
+> special optimizations that can be lost if the implementations are modified.
+
+To configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods:
+
+```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+
+BraintrustClient client = BraintrustOkHttpClient.builder()
+ .fromEnv()
+ // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa.
+ .sslSocketFactory(yourSSLSocketFactory)
+ .trustManager(yourTrustManager)
+ .hostnameVerifier(yourHostnameVerifier)
+ .build();
+```
+
### Custom HTTP client
The SDK consists of three artifacts:
@@ -578,7 +676,6 @@ Or configure the method call to validate the response using the `responseValidat
```java
import com.braintrustdata.api.models.Project;
-import com.braintrustdata.api.models.ProjectCreateParams;
Project project = client.projects().create(
params, RequestOptions.builder().responseValidation(true).build()
diff --git a/SECURITY.md b/SECURITY.md
index 0923aae0..a7f4f3b4 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -16,11 +16,11 @@ before making any information public.
## Reporting Non-SDK Related Security Issues
If you encounter security issues that are not directly related to SDKs but pertain to the services
-or products provided by Braintrust please follow the respective company's security reporting guidelines.
+or products provided by Braintrust, please follow the respective company's security reporting guidelines.
### Braintrust Terms and Policies
-Please contact info@braintrustdata.com for any questions or concerns regarding security of our services.
+Please contact info@braintrustdata.com for any questions or concerns regarding the security of our services.
---
diff --git a/bin/check-release-environment b/bin/check-release-environment
index cc9c2cd7..3a6a7b4a 100755
--- a/bin/check-release-environment
+++ b/bin/check-release-environment
@@ -3,19 +3,19 @@
errors=()
if [ -z "${SONATYPE_USERNAME}" ]; then
- errors+=("The BRAINTRUST_SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
if [ -z "${SONATYPE_PASSWORD}" ]; then
- errors+=("The BRAINTRUST_SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
if [ -z "${GPG_SIGNING_KEY}" ]; then
- errors+=("The BRAINTRUST_SONATYPE_GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
if [ -z "${GPG_SIGNING_PASSWORD}" ]; then
- errors+=("The BRAINTRUST_SONATYPE_GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+ errors+=("The GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
fi
lenErrors=${#errors[@]}
diff --git a/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt b/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt
index aa4f40a4..4410ab09 100755
--- a/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt
+++ b/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt
@@ -6,22 +6,38 @@ import com.braintrustdata.api.client.BraintrustClient
import com.braintrustdata.api.client.BraintrustClientImpl
import com.braintrustdata.api.core.ClientOptions
import com.braintrustdata.api.core.Timeout
+import com.braintrustdata.api.core.http.AsyncStreamResponse
import com.braintrustdata.api.core.http.Headers
+import com.braintrustdata.api.core.http.HttpClient
import com.braintrustdata.api.core.http.QueryParams
+import com.braintrustdata.api.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import java.util.concurrent.Executor
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
import kotlin.jvm.optionals.getOrNull
+/**
+ * A class that allows building an instance of [BraintrustClient] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
class BraintrustOkHttpClient private constructor() {
companion object {
- /** Returns a mutable builder for constructing an instance of [BraintrustOkHttpClient]. */
+ /** Returns a mutable builder for constructing an instance of [BraintrustClient]. */
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): BraintrustClient = builder().fromEnv().build()
}
@@ -29,10 +45,63 @@ class BraintrustOkHttpClient private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
+
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
- fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
@@ -45,10 +114,93 @@ class BraintrustOkHttpClient private constructor() {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.braintrustdata.api.core.jsonMapper]. The default is usually sufficient
+ * and rarely needs to be overridden.
+ */
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+ /**
+ * The executor to use for running [AsyncStreamResponse.Handler] callbacks.
+ *
+ * Defaults to a dedicated cached thread pool.
+ *
+ * This class takes ownership of the executor and shuts it down, if possible, when closed.
+ */
+ fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
+ clientOptions.streamHandlerExecutor(streamHandlerExecutor)
+ }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.braintrust.dev`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
+
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
fun headers(headers: Map>) = apply {
@@ -57,33 +209,33 @@ class BraintrustOkHttpClient private constructor() {
fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) }
- fun putHeaders(name: String, values: Iterable) = apply {
- clientOptions.putHeaders(name, values)
+ fun putheaders(name: String, values: Iterable) = apply {
+ clientOptions.putheaders(name, values)
}
- fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+ fun putAllheaders(headers: Headers) = apply { clientOptions.putAllheaders(headers) }
- fun putAllHeaders(headers: Map>) = apply {
- clientOptions.putAllHeaders(headers)
+ fun putAllheaders(headers: Map>) = apply {
+ clientOptions.putAllheaders(headers)
}
- fun replaceHeaders(name: String, value: String) = apply {
- clientOptions.replaceHeaders(name, value)
+ fun replaceheaders(name: String, value: String) = apply {
+ clientOptions.replaceheaders(name, value)
}
- fun replaceHeaders(name: String, values: Iterable) = apply {
- clientOptions.replaceHeaders(name, values)
+ fun replaceheaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceheaders(name, values)
}
- fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+ fun replaceAllheaders(headers: Headers) = apply { clientOptions.replaceAllheaders(headers) }
- fun replaceAllHeaders(headers: Map>) = apply {
- clientOptions.replaceAllHeaders(headers)
+ fun replaceAllheaders(headers: Map>) = apply {
+ clientOptions.replaceAllheaders(headers)
}
- fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+ fun removeheaders(name: String) = apply { clientOptions.removeheaders(name) }
- fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+ fun removeAllheaders(names: Set) = apply { clientOptions.removeAllheaders(names) }
fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
@@ -95,67 +247,45 @@ class BraintrustOkHttpClient private constructor() {
clientOptions.putQueryParam(key, value)
}
- fun putQueryParams(key: String, values: Iterable) = apply {
- clientOptions.putQueryParams(key, values)
+ fun putquery_params(key: String, values: Iterable) = apply {
+ clientOptions.putquery_params(key, values)
}
- fun putAllQueryParams(queryParams: QueryParams) = apply {
- clientOptions.putAllQueryParams(queryParams)
+ fun putAllquery_params(queryParams: QueryParams) = apply {
+ clientOptions.putAllquery_params(queryParams)
}
- fun putAllQueryParams(queryParams: Map>) = apply {
- clientOptions.putAllQueryParams(queryParams)
+ fun putAllquery_params(queryParams: Map>) = apply {
+ clientOptions.putAllquery_params(queryParams)
}
- fun replaceQueryParams(key: String, value: String) = apply {
- clientOptions.replaceQueryParams(key, value)
+ fun replacequery_params(key: String, value: String) = apply {
+ clientOptions.replacequery_params(key, value)
}
- fun replaceQueryParams(key: String, values: Iterable) = apply {
- clientOptions.replaceQueryParams(key, values)
+ fun replacequery_params(key: String, values: Iterable) = apply {
+ clientOptions.replacequery_params(key, values)
}
- fun replaceAllQueryParams(queryParams: QueryParams) = apply {
- clientOptions.replaceAllQueryParams(queryParams)
+ fun replaceAllquery_params(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllquery_params(queryParams)
}
- fun replaceAllQueryParams(queryParams: Map>) = apply {
- clientOptions.replaceAllQueryParams(queryParams)
+ fun replaceAllquery_params(queryParams: Map>) = apply {
+ clientOptions.replaceAllquery_params(queryParams)
}
- fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
-
- fun removeAllQueryParams(keys: Set) = apply {
- clientOptions.removeAllQueryParams(keys)
- }
+ fun removequery_params(key: String) = apply { clientOptions.removequery_params(key) }
- fun timeout(timeout: Timeout) = apply {
- clientOptions.timeout(timeout)
- this.timeout = timeout
+ fun removeAllquery_params(keys: Set) = apply {
+ clientOptions.removeAllquery_params(keys)
}
/**
- * Sets the maximum time allowed for a complete HTTP call, not including retries.
- *
- * See [Timeout.request] for more details.
+ * Updates configuration using system properties and environment variables.
*
- * For fine-grained control, pass a [Timeout] object.
+ * @see ClientOptions.Builder.fromEnv
*/
- fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
-
- fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
-
- fun proxy(proxy: Proxy) = apply { this.proxy = proxy }
-
- fun responseValidation(responseValidation: Boolean) = apply {
- clientOptions.responseValidation(responseValidation)
- }
-
- fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
-
- /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
- fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
-
fun fromEnv() = apply { clientOptions.fromEnv() }
/**
@@ -168,9 +298,11 @@ class BraintrustOkHttpClient private constructor() {
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(clientOptions.baseUrl())
- .timeout(timeout)
+ .timeout(clientOptions.timeout())
.proxy(proxy)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
.build()
)
.build()
diff --git a/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt b/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt
index c0d37976..3dfae040 100755
--- a/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt
+++ b/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt
@@ -6,24 +6,38 @@ import com.braintrustdata.api.client.BraintrustClientAsync
import com.braintrustdata.api.client.BraintrustClientAsyncImpl
import com.braintrustdata.api.core.ClientOptions
import com.braintrustdata.api.core.Timeout
+import com.braintrustdata.api.core.http.AsyncStreamResponse
import com.braintrustdata.api.core.http.Headers
+import com.braintrustdata.api.core.http.HttpClient
import com.braintrustdata.api.core.http.QueryParams
+import com.braintrustdata.api.core.jsonMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import java.net.Proxy
import java.time.Clock
import java.time.Duration
import java.util.Optional
+import java.util.concurrent.Executor
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
import kotlin.jvm.optionals.getOrNull
+/**
+ * A class that allows building an instance of [BraintrustClientAsync] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
class BraintrustOkHttpClientAsync private constructor() {
companion object {
- /**
- * Returns a mutable builder for constructing an instance of [BraintrustOkHttpClientAsync].
- */
+ /** Returns a mutable builder for constructing an instance of [BraintrustClientAsync]. */
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): BraintrustClientAsync = builder().fromEnv().build()
}
@@ -31,10 +45,63 @@ class BraintrustOkHttpClientAsync private constructor() {
class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
- fun baseUrl(baseUrl: String) = apply { clientOptions.baseUrl(baseUrl) }
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
+
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
/**
* Whether to throw an exception if any of the Jackson versions detected at runtime are
@@ -47,10 +114,93 @@ class BraintrustOkHttpClientAsync private constructor() {
clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
}
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.braintrustdata.api.core.jsonMapper]. The default is usually sufficient
+ * and rarely needs to be overridden.
+ */
fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+ /**
+ * The executor to use for running [AsyncStreamResponse.Handler] callbacks.
+ *
+ * Defaults to a dedicated cached thread pool.
+ *
+ * This class takes ownership of the executor and shuts it down, if possible, when closed.
+ */
+ fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
+ clientOptions.streamHandlerExecutor(streamHandlerExecutor)
+ }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.braintrust.dev`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
+
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
fun headers(headers: Map>) = apply {
@@ -59,33 +209,33 @@ class BraintrustOkHttpClientAsync private constructor() {
fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) }
- fun putHeaders(name: String, values: Iterable) = apply {
- clientOptions.putHeaders(name, values)
+ fun putheaders(name: String, values: Iterable) = apply {
+ clientOptions.putheaders(name, values)
}
- fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+ fun putAllheaders(headers: Headers) = apply { clientOptions.putAllheaders(headers) }
- fun putAllHeaders(headers: Map>) = apply {
- clientOptions.putAllHeaders(headers)
+ fun putAllheaders(headers: Map>) = apply {
+ clientOptions.putAllheaders(headers)
}
- fun replaceHeaders(name: String, value: String) = apply {
- clientOptions.replaceHeaders(name, value)
+ fun replaceheaders(name: String, value: String) = apply {
+ clientOptions.replaceheaders(name, value)
}
- fun replaceHeaders(name: String, values: Iterable) = apply {
- clientOptions.replaceHeaders(name, values)
+ fun replaceheaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceheaders(name, values)
}
- fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+ fun replaceAllheaders(headers: Headers) = apply { clientOptions.replaceAllheaders(headers) }
- fun replaceAllHeaders(headers: Map>) = apply {
- clientOptions.replaceAllHeaders(headers)
+ fun replaceAllheaders(headers: Map>) = apply {
+ clientOptions.replaceAllheaders(headers)
}
- fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+ fun removeheaders(name: String) = apply { clientOptions.removeheaders(name) }
- fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+ fun removeAllheaders(names: Set) = apply { clientOptions.removeAllheaders(names) }
fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
@@ -97,67 +247,45 @@ class BraintrustOkHttpClientAsync private constructor() {
clientOptions.putQueryParam(key, value)
}
- fun putQueryParams(key: String, values: Iterable) = apply {
- clientOptions.putQueryParams(key, values)
+ fun putquery_params(key: String, values: Iterable) = apply {
+ clientOptions.putquery_params(key, values)
}
- fun putAllQueryParams(queryParams: QueryParams) = apply {
- clientOptions.putAllQueryParams(queryParams)
+ fun putAllquery_params(queryParams: QueryParams) = apply {
+ clientOptions.putAllquery_params(queryParams)
}
- fun putAllQueryParams(queryParams: Map>) = apply {
- clientOptions.putAllQueryParams(queryParams)
+ fun putAllquery_params(queryParams: Map>) = apply {
+ clientOptions.putAllquery_params(queryParams)
}
- fun replaceQueryParams(key: String, value: String) = apply {
- clientOptions.replaceQueryParams(key, value)
+ fun replacequery_params(key: String, value: String) = apply {
+ clientOptions.replacequery_params(key, value)
}
- fun replaceQueryParams(key: String, values: Iterable) = apply {
- clientOptions.replaceQueryParams(key, values)
+ fun replacequery_params(key: String, values: Iterable) = apply {
+ clientOptions.replacequery_params(key, values)
}
- fun replaceAllQueryParams(queryParams: QueryParams) = apply {
- clientOptions.replaceAllQueryParams(queryParams)
+ fun replaceAllquery_params(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllquery_params(queryParams)
}
- fun replaceAllQueryParams(queryParams: Map>) = apply {
- clientOptions.replaceAllQueryParams(queryParams)
+ fun replaceAllquery_params(queryParams: Map>) = apply {
+ clientOptions.replaceAllquery_params(queryParams)
}
- fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
-
- fun removeAllQueryParams(keys: Set) = apply {
- clientOptions.removeAllQueryParams(keys)
- }
+ fun removequery_params(key: String) = apply { clientOptions.removequery_params(key) }
- fun timeout(timeout: Timeout) = apply {
- clientOptions.timeout(timeout)
- this.timeout = timeout
+ fun removeAllquery_params(keys: Set) = apply {
+ clientOptions.removeAllquery_params(keys)
}
/**
- * Sets the maximum time allowed for a complete HTTP call, not including retries.
- *
- * See [Timeout.request] for more details.
+ * Updates configuration using system properties and environment variables.
*
- * For fine-grained control, pass a [Timeout] object.
+ * @see ClientOptions.Builder.fromEnv
*/
- fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
-
- fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
-
- fun proxy(proxy: Proxy) = apply { this.proxy = proxy }
-
- fun responseValidation(responseValidation: Boolean) = apply {
- clientOptions.responseValidation(responseValidation)
- }
-
- fun apiKey(apiKey: String?) = apply { clientOptions.apiKey(apiKey) }
-
- /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
- fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
-
fun fromEnv() = apply { clientOptions.fromEnv() }
/**
@@ -170,9 +298,11 @@ class BraintrustOkHttpClientAsync private constructor() {
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(clientOptions.baseUrl())
- .timeout(timeout)
+ .timeout(clientOptions.timeout())
.proxy(proxy)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
.build()
)
.build()
diff --git a/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClient.kt b/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClient.kt
index 49a5df68..7a162b29 100755
--- a/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClient.kt
+++ b/braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClient.kt
@@ -2,7 +2,6 @@ package com.braintrustdata.api.client.okhttp
import com.braintrustdata.api.core.RequestOptions
import com.braintrustdata.api.core.Timeout
-import com.braintrustdata.api.core.checkRequired
import com.braintrustdata.api.core.http.Headers
import com.braintrustdata.api.core.http.HttpClient
import com.braintrustdata.api.core.http.HttpMethod
@@ -15,9 +14,11 @@ import java.io.InputStream
import java.net.Proxy
import java.time.Duration
import java.util.concurrent.CompletableFuture
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
import okhttp3.Call
import okhttp3.Callback
-import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -28,8 +29,7 @@ import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
-class OkHttpClient
-private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
+class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpClient) :
HttpClient {
override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
@@ -140,11 +140,7 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
}
private fun HttpRequest.toUrl(): String {
- url?.let {
- return it
- }
-
- val builder = baseUrl.newBuilder()
+ val builder = baseUrl.toHttpUrl().newBuilder()
pathSegments.forEach(builder::addPathSegment)
queryParams.keys().forEach { key ->
queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
@@ -194,11 +190,11 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
class Builder internal constructor() {
- private var baseUrl: HttpUrl? = null
private var timeout: Timeout = Timeout.default()
private var proxy: Proxy? = null
-
- fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
@@ -206,6 +202,18 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
fun build(): OkHttpClient =
OkHttpClient(
okhttp3.OkHttpClient.Builder()
@@ -214,8 +222,25 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
.writeTimeout(timeout.write())
.callTimeout(timeout.request())
.proxy(proxy)
- .build(),
- checkRequired("baseUrl", baseUrl),
+ .apply {
+ val sslSocketFactory = sslSocketFactory
+ val trustManager = trustManager
+ if (sslSocketFactory != null && trustManager != null) {
+ sslSocketFactory(sslSocketFactory, trustManager)
+ } else {
+ check((sslSocketFactory != null) == (trustManager != null)) {
+ "Both or none of `sslSocketFactory` and `trustManager` must be set, but only one was set"
+ }
+ }
+
+ hostnameVerifier?.let(::hostnameVerifier)
+ }
+ .build()
+ .apply {
+ // We usually make all our requests to the same host so it makes sense to
+ // raise the per-host limit to the overall limit.
+ dispatcher.maxRequestsPerHost = dispatcher.maxRequests
+ }
)
}
}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClient.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClient.kt
index bc327480..7881923e 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClient.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClient.kt
@@ -2,6 +2,7 @@
package com.braintrustdata.api.client
+import com.braintrustdata.api.core.ClientOptions
import com.braintrustdata.api.services.blocking.AclService
import com.braintrustdata.api.services.blocking.AiSecretService
import com.braintrustdata.api.services.blocking.ApiKeyService
@@ -21,6 +22,7 @@ import com.braintrustdata.api.services.blocking.SpanIframeService
import com.braintrustdata.api.services.blocking.TopLevelService
import com.braintrustdata.api.services.blocking.UserService
import com.braintrustdata.api.services.blocking.ViewService
+import java.util.function.Consumer
/**
* A client for interacting with the Braintrust REST API synchronously. You can also switch to
@@ -51,6 +53,13 @@ interface BraintrustClient {
*/
fun withRawResponse(): WithRawResponse
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): BraintrustClient
+
fun topLevel(): TopLevelService
fun projects(): ProjectService
@@ -105,6 +114,13 @@ interface BraintrustClient {
/** A view of [BraintrustClient] that provides access to raw HTTP responses for each method. */
interface WithRawResponse {
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): BraintrustClient.WithRawResponse
+
fun topLevel(): TopLevelService.WithRawResponse
fun projects(): ProjectService.WithRawResponse
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsync.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsync.kt
index 24ed3368..c6edb032 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsync.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsync.kt
@@ -2,6 +2,7 @@
package com.braintrustdata.api.client
+import com.braintrustdata.api.core.ClientOptions
import com.braintrustdata.api.services.async.AclServiceAsync
import com.braintrustdata.api.services.async.AiSecretServiceAsync
import com.braintrustdata.api.services.async.ApiKeyServiceAsync
@@ -21,6 +22,7 @@ import com.braintrustdata.api.services.async.SpanIframeServiceAsync
import com.braintrustdata.api.services.async.TopLevelServiceAsync
import com.braintrustdata.api.services.async.UserServiceAsync
import com.braintrustdata.api.services.async.ViewServiceAsync
+import java.util.function.Consumer
/**
* A client for interacting with the Braintrust REST API asynchronously. You can also switch to
@@ -51,6 +53,13 @@ interface BraintrustClientAsync {
*/
fun withRawResponse(): WithRawResponse
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): BraintrustClientAsync
+
fun topLevel(): TopLevelServiceAsync
fun projects(): ProjectServiceAsync
@@ -107,6 +116,15 @@ interface BraintrustClientAsync {
*/
interface WithRawResponse {
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(
+ modifier: Consumer
+ ): BraintrustClientAsync.WithRawResponse
+
fun topLevel(): TopLevelServiceAsync.WithRawResponse
fun projects(): ProjectServiceAsync.WithRawResponse
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt
index e945466b..2d94732b 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt
@@ -42,6 +42,7 @@ import com.braintrustdata.api.services.async.UserServiceAsync
import com.braintrustdata.api.services.async.UserServiceAsyncImpl
import com.braintrustdata.api.services.async.ViewServiceAsync
import com.braintrustdata.api.services.async.ViewServiceAsyncImpl
+import java.util.function.Consumer
class BraintrustClientAsyncImpl(private val clientOptions: ClientOptions) : BraintrustClientAsync {
@@ -130,6 +131,9 @@ class BraintrustClientAsyncImpl(private val clientOptions: ClientOptions) : Brai
override fun withRawResponse(): BraintrustClientAsync.WithRawResponse = withRawResponse
+ override fun withOptions(modifier: Consumer): BraintrustClientAsync =
+ BraintrustClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
override fun topLevel(): TopLevelServiceAsync = topLevel
override fun projects(): ProjectServiceAsync = projects
@@ -168,7 +172,7 @@ class BraintrustClientAsyncImpl(private val clientOptions: ClientOptions) : Brai
override fun evals(): EvalServiceAsync = evals
- override fun close() = clientOptions.httpClient.close()
+ override fun close() = clientOptions.close()
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
BraintrustClientAsync.WithRawResponse {
@@ -249,6 +253,13 @@ class BraintrustClientAsyncImpl(private val clientOptions: ClientOptions) : Brai
EvalServiceAsyncImpl.WithRawResponseImpl(clientOptions)
}
+ override fun withOptions(
+ modifier: Consumer
+ ): BraintrustClientAsync.WithRawResponse =
+ BraintrustClientAsyncImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
override fun topLevel(): TopLevelServiceAsync.WithRawResponse = topLevel
override fun projects(): ProjectServiceAsync.WithRawResponse = projects
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt
index 3eecd3cb..c205ae48 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt
@@ -42,6 +42,7 @@ import com.braintrustdata.api.services.blocking.UserService
import com.braintrustdata.api.services.blocking.UserServiceImpl
import com.braintrustdata.api.services.blocking.ViewService
import com.braintrustdata.api.services.blocking.ViewServiceImpl
+import java.util.function.Consumer
class BraintrustClientImpl(private val clientOptions: ClientOptions) : BraintrustClient {
@@ -118,6 +119,9 @@ class BraintrustClientImpl(private val clientOptions: ClientOptions) : Braintrus
override fun withRawResponse(): BraintrustClient.WithRawResponse = withRawResponse
+ override fun withOptions(modifier: Consumer): BraintrustClient =
+ BraintrustClientImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
override fun topLevel(): TopLevelService = topLevel
override fun projects(): ProjectService = projects
@@ -156,7 +160,7 @@ class BraintrustClientImpl(private val clientOptions: ClientOptions) : Braintrus
override fun evals(): EvalService = evals
- override fun close() = clientOptions.httpClient.close()
+ override fun close() = clientOptions.close()
class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
BraintrustClient.WithRawResponse {
@@ -237,6 +241,13 @@ class BraintrustClientImpl(private val clientOptions: ClientOptions) : Braintrus
EvalServiceImpl.WithRawResponseImpl(clientOptions)
}
+ override fun withOptions(
+ modifier: Consumer
+ ): BraintrustClient.WithRawResponse =
+ BraintrustClientImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
override fun topLevel(): TopLevelService.WithRawResponse = topLevel
override fun projects(): ProjectService.WithRawResponse = projects
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/AutoPager.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/AutoPager.kt
new file mode 100644
index 00000000..d208a9f8
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/AutoPager.kt
@@ -0,0 +1,21 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.braintrustdata.api.core
+
+import java.util.stream.Stream
+import java.util.stream.StreamSupport
+
+class AutoPager private constructor(private val firstPage: Page) : Iterable {
+
+ companion object {
+
+ fun from(firstPage: Page): AutoPager = AutoPager(firstPage)
+ }
+
+ override fun iterator(): Iterator =
+ generateSequence(firstPage) { if (it.hasNextPage()) it.nextPage() else null }
+ .flatMap { it.items() }
+ .iterator()
+
+ fun stream(): Stream = StreamSupport.stream(spliterator(), false)
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/AutoPagerAsync.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/AutoPagerAsync.kt
new file mode 100644
index 00000000..7ff270b1
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/AutoPagerAsync.kt
@@ -0,0 +1,88 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.braintrustdata.api.core
+
+import com.braintrustdata.api.core.http.AsyncStreamResponse
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionException
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+class AutoPagerAsync
+private constructor(private val firstPage: PageAsync, private val defaultExecutor: Executor) :
+ AsyncStreamResponse {
+
+ companion object {
+
+ fun from(firstPage: PageAsync, defaultExecutor: Executor): AutoPagerAsync =
+ AutoPagerAsync(firstPage, defaultExecutor)
+ }
+
+ private val onCompleteFuture = CompletableFuture()
+ private val state = AtomicReference(State.NEW)
+
+ override fun subscribe(handler: AsyncStreamResponse.Handler): AsyncStreamResponse =
+ subscribe(handler, defaultExecutor)
+
+ override fun subscribe(
+ handler: AsyncStreamResponse.Handler,
+ executor: Executor,
+ ): AsyncStreamResponse = apply {
+ // TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
+ check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
+ if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
+ else "Cannot subscribe after the response is closed"
+ }
+
+ fun PageAsync.handle(): CompletableFuture {
+ if (state.get() == State.CLOSED) {
+ return CompletableFuture.completedFuture(null)
+ }
+
+ items().forEach { handler.onNext(it) }
+ return if (hasNextPage()) nextPage().thenCompose { it.handle() }
+ else CompletableFuture.completedFuture(null)
+ }
+
+ executor.execute {
+ firstPage.handle().whenComplete { _, error ->
+ val actualError =
+ if (error is CompletionException && error.cause != null) error.cause else error
+ try {
+ handler.onComplete(Optional.ofNullable(actualError))
+ } finally {
+ try {
+ if (actualError == null) {
+ onCompleteFuture.complete(null)
+ } else {
+ onCompleteFuture.completeExceptionally(actualError)
+ }
+ } finally {
+ close()
+ }
+ }
+ }
+ }
+ }
+
+ override fun onCompleteFuture(): CompletableFuture = onCompleteFuture
+
+ override fun close() {
+ val previousState = state.getAndSet(State.CLOSED)
+ if (previousState == State.CLOSED) {
+ return
+ }
+
+ // When the stream is closed, we should always consider it closed. If it closed due
+ // to an error, then we will have already completed the future earlier, and this
+ // will be a no-op.
+ onCompleteFuture.complete(null)
+ }
+}
+
+private enum class State {
+ NEW,
+ SUBSCRIBED,
+ CLOSED,
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Check.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Check.kt
index 7b05d4da..254bc88e 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Check.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Check.kt
@@ -5,6 +5,9 @@ package com.braintrustdata.api.core
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.core.util.VersionUtil
+fun checkRequired(name: String, condition: Boolean) =
+ check(condition) { "`$name` is required, but was not set" }
+
fun checkRequired(name: String, value: T?): T =
checkNotNull(value) { "`$name` is required, but was not set" }
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ClientOptions.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ClientOptions.kt
index 9841ea6e..ab5fd8bd 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ClientOptions.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ClientOptions.kt
@@ -2,6 +2,7 @@
package com.braintrustdata.api.core
+import com.braintrustdata.api.core.http.AsyncStreamResponse
import com.braintrustdata.api.core.http.Headers
import com.braintrustdata.api.core.http.HttpClient
import com.braintrustdata.api.core.http.PhantomReachableClosingHttpClient
@@ -9,21 +10,92 @@ import com.braintrustdata.api.core.http.QueryParams
import com.braintrustdata.api.core.http.RetryingHttpClient
import com.fasterxml.jackson.databind.json.JsonMapper
import java.time.Clock
+import java.time.Duration
import java.util.Optional
+import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+import java.util.concurrent.atomic.AtomicLong
import kotlin.jvm.optionals.getOrNull
+/** A class representing the SDK client configuration. */
class ClientOptions
private constructor(
private val originalHttpClient: HttpClient,
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `braintrust-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
@get:JvmName("httpClient") val httpClient: HttpClient,
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee that
+ * the SDK will work correctly when using an incompatible Jackson version.
+ */
@get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.braintrustdata.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
@get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
+ /**
+ * The executor to use for running [AsyncStreamResponse.Handler] callbacks.
+ *
+ * Defaults to a dedicated cached thread pool.
+ *
+ * This class takes ownership of the executor and shuts it down, if possible, when closed.
+ */
+ @get:JvmName("streamHandlerExecutor") val streamHandlerExecutor: Executor,
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
@get:JvmName("clock") val clock: Clock,
- @get:JvmName("baseUrl") val baseUrl: String,
+ private val baseUrl: String?,
+ /** Headers to send with the request. */
@get:JvmName("headers") val headers: Headers,
+ /** Query params to send with the request. */
@get:JvmName("queryParams") val queryParams: QueryParams,
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
@get:JvmName("responseValidation") val responseValidation: Boolean,
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
@get:JvmName("timeout") val timeout: Timeout,
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
@get:JvmName("maxRetries") val maxRetries: Int,
private val apiKey: String?,
) {
@@ -34,6 +106,13 @@ private constructor(
}
}
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.braintrust.dev`.
+ */
+ fun baseUrl(): String = baseUrl ?: PRODUCTION_URL
+
fun apiKey(): Optional = Optional.ofNullable(apiKey)
fun toBuilder() = Builder().from(this)
@@ -52,6 +131,11 @@ private constructor(
*/
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns options configured using system properties and environment variables.
+ *
+ * @see Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
}
@@ -61,8 +145,9 @@ private constructor(
private var httpClient: HttpClient? = null
private var checkJacksonVersionCompatibility: Boolean = true
private var jsonMapper: JsonMapper = jsonMapper()
+ private var streamHandlerExecutor: Executor? = null
private var clock: Clock = Clock.systemUTC()
- private var baseUrl: String = PRODUCTION_URL
+ private var baseUrl: String? = null
private var headers: Headers.Builder = Headers.builder()
private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
@@ -75,6 +160,7 @@ private constructor(
httpClient = clientOptions.originalHttpClient
checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
jsonMapper = clientOptions.jsonMapper
+ streamHandlerExecutor = clientOptions.streamHandlerExecutor
clock = clientOptions.clock
baseUrl = clientOptions.baseUrl
headers = clientOptions.headers.toBuilder()
@@ -85,24 +171,111 @@ private constructor(
apiKey = clientOptions.apiKey
}
- fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `braintrust-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
+ fun httpClient(httpClient: HttpClient) = apply {
+ this.httpClient = PhantomReachableClosingHttpClient(httpClient)
+ }
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
}
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.braintrustdata.api.core.jsonMapper]. The default is usually sufficient
+ * and rarely needs to be overridden.
+ */
fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
+ /**
+ * The executor to use for running [AsyncStreamResponse.Handler] callbacks.
+ *
+ * Defaults to a dedicated cached thread pool.
+ *
+ * This class takes ownership of the executor and shuts it down, if possible, when closed.
+ */
+ fun streamHandlerExecutor(streamHandlerExecutor: Executor) = apply {
+ this.streamHandlerExecutor =
+ if (streamHandlerExecutor is ExecutorService)
+ PhantomReachableExecutorService(streamHandlerExecutor)
+ else streamHandlerExecutor
+ }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
fun clock(clock: Clock) = apply { this.clock = clock }
- fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.braintrust.dev`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { this.baseUrl = baseUrl }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
fun apiKey(apiKey: String?) = apply { this.apiKey = apiKey }
@@ -112,89 +285,104 @@ private constructor(
fun headers(headers: Headers) = apply {
this.headers.clear()
- putAllHeaders(headers)
+ putAllheaders(headers)
}
fun headers(headers: Map>) = apply {
this.headers.clear()
- putAllHeaders(headers)
+ putAllheaders(headers)
}
fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
- fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+ fun putheaders(name: String, values: Iterable) = apply { headers.put(name, values) }
- fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
+ fun putAllheaders(headers: Headers) = apply { this.headers.putAll(headers) }
- fun putAllHeaders(headers: Map>) = apply {
+ fun putAllheaders(headers: Map>) = apply {
this.headers.putAll(headers)
}
- fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
+ fun replaceheaders(name: String, value: String) = apply { headers.replace(name, value) }
- fun replaceHeaders(name: String, values: Iterable) = apply {
+ fun replaceheaders(name: String, values: Iterable) = apply {
headers.replace(name, values)
}
- fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+ fun replaceAllheaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
- fun replaceAllHeaders(headers: Map>) = apply {
+ fun replaceAllheaders(headers: Map>) = apply {
this.headers.replaceAll(headers)
}
- fun removeHeaders(name: String) = apply { headers.remove(name) }
+ fun removeheaders(name: String) = apply { headers.remove(name) }
- fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
+ fun removeAllheaders(names: Set) = apply { headers.removeAll(names) }
fun queryParams(queryParams: QueryParams) = apply {
this.queryParams.clear()
- putAllQueryParams(queryParams)
+ putAllquery_params(queryParams)
}
fun queryParams(queryParams: Map>) = apply {
this.queryParams.clear()
- putAllQueryParams(queryParams)
+ putAllquery_params(queryParams)
}
fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
- fun putQueryParams(key: String, values: Iterable) = apply {
+ fun putquery_params(key: String, values: Iterable) = apply {
queryParams.put(key, values)
}
- fun putAllQueryParams(queryParams: QueryParams) = apply {
+ fun putAllquery_params(queryParams: QueryParams) = apply {
this.queryParams.putAll(queryParams)
}
- fun putAllQueryParams(queryParams: Map>) = apply {
+ fun putAllquery_params(queryParams: Map>) = apply {
this.queryParams.putAll(queryParams)
}
- fun replaceQueryParams(key: String, value: String) = apply {
+ fun replacequery_params(key: String, value: String) = apply {
queryParams.replace(key, value)
}
- fun replaceQueryParams(key: String, values: Iterable) = apply {
+ fun replacequery_params(key: String, values: Iterable) = apply {
queryParams.replace(key, values)
}
- fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ fun replaceAllquery_params(queryParams: QueryParams) = apply {
this.queryParams.replaceAll(queryParams)
}
- fun replaceAllQueryParams(queryParams: Map>) = apply {
+ fun replaceAllquery_params(queryParams: Map>) = apply {
this.queryParams.replaceAll(queryParams)
}
- fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
+ fun removequery_params(key: String) = apply { queryParams.remove(key) }
- fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
+ fun removeAllquery_params(keys: Set) = apply { queryParams.removeAll(keys) }
- fun baseUrl(): String = baseUrl
+ fun timeout(): Timeout = timeout
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * See this table for the available options:
+ *
+ * |Setter |System property |Environment variable |Required|Default value |
+ * |---------|--------------------|---------------------|--------|------------------------------|
+ * |`apiKey` |`braintrust.apiKey` |`BRAINTRUST_API_KEY` |false |- |
+ * |`baseUrl`|`braintrust.baseUrl`|`BRAINTRUST_BASE_URL`|true |`"https://api.braintrust.dev"`|
+ *
+ * System properties take precedence over environment variables.
+ */
fun fromEnv() = apply {
- System.getenv("BRAINTRUST_BASE_URL")?.let { baseUrl(it) }
- System.getenv("BRAINTRUST_API_KEY")?.let { apiKey(it) }
+ (System.getProperty("braintrust.baseUrl") ?: System.getenv("BRAINTRUST_BASE_URL"))
+ ?.let { baseUrl(it) }
+ (System.getProperty("braintrust.apiKey") ?: System.getenv("BRAINTRUST_API_KEY"))?.let {
+ apiKey(it)
+ }
}
/**
@@ -231,15 +419,28 @@ private constructor(
return ClientOptions(
httpClient,
- PhantomReachableClosingHttpClient(
- RetryingHttpClient.builder()
- .httpClient(httpClient)
- .clock(clock)
- .maxRetries(maxRetries)
- .build()
- ),
+ RetryingHttpClient.builder()
+ .httpClient(httpClient)
+ .clock(clock)
+ .maxRetries(maxRetries)
+ .build(),
checkJacksonVersionCompatibility,
jsonMapper,
+ streamHandlerExecutor
+ ?: Executors.newCachedThreadPool(
+ object : ThreadFactory {
+
+ private val threadFactory: ThreadFactory =
+ Executors.defaultThreadFactory()
+ private val count = AtomicLong(0)
+
+ override fun newThread(runnable: Runnable): Thread =
+ threadFactory.newThread(runnable).also {
+ it.name =
+ "braintrust-stream-handler-thread-${count.getAndIncrement()}"
+ }
+ }
+ ),
clock,
baseUrl,
headers.build(),
@@ -251,4 +452,19 @@ private constructor(
)
}
}
+
+ /**
+ * Closes these client options, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client options are
+ * long-lived and usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default client automatically
+ * releases threads and connections if they remain idle, but if you are writing an application
+ * that needs to aggressively release unused resources, then you may call this method.
+ */
+ fun close() {
+ httpClient.close()
+ (streamHandlerExecutor as? ExecutorService)?.shutdown()
+ }
}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Page.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Page.kt
new file mode 100644
index 00000000..5d5afd20
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Page.kt
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.braintrustdata.api.core
+
+/**
+ * An interface representing a single page, with items of type [T], from a paginated endpoint
+ * response.
+ *
+ * Implementations of this interface are expected to request additional pages synchronously. For
+ * asynchronous pagination, see the [PageAsync] interface.
+ */
+interface Page {
+
+ /**
+ * Returns whether there's another page after this one.
+ *
+ * The method generally doesn't make requests so the result depends entirely on the data in this
+ * page. If a significant amount of time has passed between requesting this page and calling
+ * this method, then the result could be stale.
+ */
+ fun hasNextPage(): Boolean
+
+ /**
+ * Returns the page after this one by making another request.
+ *
+ * @throws IllegalStateException if it's impossible to get the next page. This exception is
+ * avoidable by calling [hasNextPage] first.
+ */
+ fun nextPage(): Page
+
+ /** Returns the items in this page. */
+ fun items(): List
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PageAsync.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PageAsync.kt
new file mode 100644
index 00000000..10b3b47a
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PageAsync.kt
@@ -0,0 +1,35 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.braintrustdata.api.core
+
+import java.util.concurrent.CompletableFuture
+
+/**
+ * An interface representing a single page, with items of type [T], from a paginated endpoint
+ * response.
+ *
+ * Implementations of this interface are expected to request additional pages asynchronously. For
+ * synchronous pagination, see the [Page] interface.
+ */
+interface PageAsync {
+
+ /**
+ * Returns whether there's another page after this one.
+ *
+ * The method generally doesn't make requests so the result depends entirely on the data in this
+ * page. If a significant amount of time has passed between requesting this page and calling
+ * this method, then the result could be stale.
+ */
+ fun hasNextPage(): Boolean
+
+ /**
+ * Returns the page after this one by making another request.
+ *
+ * @throws IllegalStateException if it's impossible to get the next page. This exception is
+ * avoidable by calling [hasNextPage] first.
+ */
+ fun nextPage(): CompletableFuture>
+
+ /** Returns the items in this page. */
+ fun items(): List
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachableExecutorService.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachableExecutorService.kt
new file mode 100644
index 00000000..0538d523
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachableExecutorService.kt
@@ -0,0 +1,58 @@
+package com.braintrustdata.api.core
+
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+
+/**
+ * A delegating wrapper around an [ExecutorService] that shuts it down once it's only phantom
+ * reachable.
+ *
+ * This class ensures the [ExecutorService] is shut down even if the user forgets to do it.
+ */
+internal class PhantomReachableExecutorService(private val executorService: ExecutorService) :
+ ExecutorService {
+ init {
+ closeWhenPhantomReachable(this) { executorService.shutdown() }
+ }
+
+ override fun execute(command: Runnable) = executorService.execute(command)
+
+ override fun shutdown() = executorService.shutdown()
+
+ override fun shutdownNow(): MutableList = executorService.shutdownNow()
+
+ override fun isShutdown(): Boolean = executorService.isShutdown
+
+ override fun isTerminated(): Boolean = executorService.isTerminated
+
+ override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean =
+ executorService.awaitTermination(timeout, unit)
+
+ override fun submit(task: Callable): Future = executorService.submit(task)
+
+ override fun submit(task: Runnable, result: T): Future =
+ executorService.submit(task, result)
+
+ override fun submit(task: Runnable): Future<*> = executorService.submit(task)
+
+ override fun invokeAll(
+ tasks: MutableCollection>
+ ): MutableList> = executorService.invokeAll(tasks)
+
+ override fun invokeAll(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): MutableList> = executorService.invokeAll(tasks, timeout, unit)
+
+ override fun invokeAny(tasks: MutableCollection>): T =
+ executorService.invokeAny(tasks)
+
+ override fun invokeAny(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): T = executorService.invokeAny(tasks, timeout, unit)
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Timeout.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Timeout.kt
index 06ee42fa..b2320ac8 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Timeout.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Timeout.kt
@@ -157,10 +157,14 @@ private constructor(
return true
}
- return /* spotless:off */ other is Timeout && connect == other.connect && read == other.read && write == other.write && request == other.request /* spotless:on */
+ return other is Timeout &&
+ connect == other.connect &&
+ read == other.read &&
+ write == other.write &&
+ request == other.request
}
- override fun hashCode(): Int = /* spotless:off */ Objects.hash(connect, read, write, request) /* spotless:on */
+ override fun hashCode(): Int = Objects.hash(connect, read, write, request)
override fun toString() =
"Timeout{connect=$connect, read=$read, write=$write, request=$request}"
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Utils.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Utils.kt
index 6f9fb155..a1657ff3 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Utils.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Utils.kt
@@ -5,6 +5,8 @@ package com.braintrustdata.api.core
import com.braintrustdata.api.errors.BraintrustInvalidDataException
import java.util.Collections
import java.util.SortedMap
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.locks.Lock
@JvmSynthetic
internal fun T?.getOrThrow(name: String): T =
@@ -90,3 +92,24 @@ internal fun Any?.contentToString(): String {
}
internal interface Enum
+
+/**
+ * Executes the given [action] while holding the lock, returning a [CompletableFuture] with the
+ * result.
+ *
+ * @param action The asynchronous action to execute while holding the lock
+ * @return A [CompletableFuture] that completes with the result of the action
+ */
+@JvmSynthetic
+internal fun Lock.withLockAsync(action: () -> CompletableFuture): CompletableFuture {
+ lock()
+ val future =
+ try {
+ action()
+ } catch (e: Throwable) {
+ unlock()
+ throw e
+ }
+ future.whenComplete { _, _ -> unlock() }
+ return future
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/handlers/ErrorHandler.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/handlers/ErrorHandler.kt
index f3b14814..67559b98 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/handlers/ErrorHandler.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/handlers/ErrorHandler.kt
@@ -19,7 +19,7 @@ import com.braintrustdata.api.errors.UnprocessableEntityException
import com.fasterxml.jackson.databind.json.JsonMapper
@JvmSynthetic
-internal fun errorHandler(jsonMapper: JsonMapper): Handler {
+internal fun errorBodyHandler(jsonMapper: JsonMapper): Handler {
val handler = jsonHandler(jsonMapper)
return object : Handler {
@@ -33,52 +33,52 @@ internal fun errorHandler(jsonMapper: JsonMapper): Handler {
}
@JvmSynthetic
-internal fun Handler.withErrorHandler(errorHandler: Handler): Handler =
- object : Handler {
- override fun handle(response: HttpResponse): T =
+internal fun errorHandler(errorBodyHandler: Handler): Handler =
+ object : Handler {
+ override fun handle(response: HttpResponse): HttpResponse =
when (val statusCode = response.statusCode()) {
- in 200..299 -> this@withErrorHandler.handle(response)
+ in 200..299 -> response
400 ->
throw BadRequestException.builder()
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
401 ->
throw UnauthorizedException.builder()
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
403 ->
throw PermissionDeniedException.builder()
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
404 ->
throw NotFoundException.builder()
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
422 ->
throw UnprocessableEntityException.builder()
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
429 ->
throw RateLimitException.builder()
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
in 500..599 ->
throw InternalServerException.builder()
.statusCode(statusCode)
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
else ->
throw UnexpectedStatusCodeException.builder()
.statusCode(statusCode)
.headers(response.headers())
- .body(errorHandler.handle(response))
+ .body(errorBodyHandler.handle(response))
.build()
}
}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/AsyncStreamResponse.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/AsyncStreamResponse.kt
new file mode 100644
index 00000000..93dff9ec
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/AsyncStreamResponse.kt
@@ -0,0 +1,157 @@
+package com.braintrustdata.api.core.http
+
+import com.braintrustdata.api.core.http.AsyncStreamResponse.Handler
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicReference
+
+/**
+ * A class providing access to an API response as an asynchronous stream of chunks of type [T],
+ * where each chunk can be individually processed as soon as it arrives instead of waiting on the
+ * full response.
+ */
+interface AsyncStreamResponse {
+
+ /**
+ * Registers [handler] to be called for events of this stream.
+ *
+ * [handler]'s methods will be called in the client's configured or default thread pool.
+ *
+ * @throws IllegalStateException if [subscribe] has already been called.
+ */
+ fun subscribe(handler: Handler): AsyncStreamResponse
+
+ /**
+ * Registers [handler] to be called for events of this stream.
+ *
+ * [handler]'s methods will be called in the given [executor].
+ *
+ * @throws IllegalStateException if [subscribe] has already been called.
+ */
+ fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse
+
+ /**
+ * Returns a future that completes when a stream is fully consumed, errors, or gets closed
+ * early.
+ */
+ fun onCompleteFuture(): CompletableFuture
+
+ /**
+ * Closes this resource, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because this response should not be
+ * synchronously closed via try-with-resources.
+ */
+ fun close()
+
+ /** A class for handling streaming events. */
+ fun interface Handler {
+
+ /** Called whenever a chunk is received. */
+ fun onNext(value: T)
+
+ /**
+ * Called when a stream is fully consumed, errors, or gets closed early.
+ *
+ * [onNext] will not be called once this method is called.
+ *
+ * @param error Non-empty if the stream completed due to an error.
+ */
+ fun onComplete(error: Optional) {}
+ }
+}
+
+@JvmSynthetic
+internal fun CompletableFuture>.toAsync(streamHandlerExecutor: Executor) =
+ PhantomReachableClosingAsyncStreamResponse(
+ object : AsyncStreamResponse {
+
+ private val onCompleteFuture = CompletableFuture()
+ private val state = AtomicReference(State.NEW)
+
+ init {
+ this@toAsync.whenComplete { _, error ->
+ // If an error occurs from the original future, then we should resolve the
+ // `onCompleteFuture` even if `subscribe` has not been called.
+ error?.let(onCompleteFuture::completeExceptionally)
+ }
+ }
+
+ override fun subscribe(handler: Handler): AsyncStreamResponse =
+ subscribe(handler, streamHandlerExecutor)
+
+ override fun subscribe(
+ handler: Handler,
+ executor: Executor,
+ ): AsyncStreamResponse = apply {
+ // TODO(JDK): Use `compareAndExchange` once targeting JDK 9.
+ check(state.compareAndSet(State.NEW, State.SUBSCRIBED)) {
+ if (state.get() == State.SUBSCRIBED) "Cannot subscribe more than once"
+ else "Cannot subscribe after the response is closed"
+ }
+
+ this@toAsync.whenCompleteAsync(
+ { streamResponse, futureError ->
+ if (state.get() == State.CLOSED) {
+ // Avoid doing any work if `close` was called before the future
+ // completed.
+ return@whenCompleteAsync
+ }
+
+ if (futureError != null) {
+ // An error occurred before we started passing chunks to the handler.
+ handler.onComplete(Optional.of(futureError))
+ return@whenCompleteAsync
+ }
+
+ var streamError: Throwable? = null
+ try {
+ streamResponse.stream().forEach(handler::onNext)
+ } catch (e: Throwable) {
+ streamError = e
+ }
+
+ try {
+ handler.onComplete(Optional.ofNullable(streamError))
+ } finally {
+ try {
+ // Notify completion via the `onCompleteFuture` as well. This is in
+ // a separate `try-finally` block so that we still complete the
+ // future if `handler.onComplete` throws.
+ if (streamError == null) {
+ onCompleteFuture.complete(null)
+ } else {
+ onCompleteFuture.completeExceptionally(streamError)
+ }
+ } finally {
+ close()
+ }
+ }
+ },
+ executor,
+ )
+ }
+
+ override fun onCompleteFuture(): CompletableFuture = onCompleteFuture
+
+ override fun close() {
+ val previousState = state.getAndSet(State.CLOSED)
+ if (previousState == State.CLOSED) {
+ return
+ }
+
+ this@toAsync.whenComplete { streamResponse, error -> streamResponse?.close() }
+ // When the stream is closed, we should always consider it closed. If it closed due
+ // to an error, then we will have already completed the future earlier, and this
+ // will be a no-op.
+ onCompleteFuture.complete(null)
+ }
+ }
+ )
+
+private enum class State {
+ NEW,
+ SUBSCRIBED,
+ CLOSED,
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/Headers.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/Headers.kt
index 6d37e804..774088ec 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/Headers.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/Headers.kt
@@ -1,5 +1,15 @@
+// File generated from our OpenAPI spec by Stainless.
+
package com.braintrustdata.api.core.http
+import com.braintrustdata.api.core.JsonArray
+import com.braintrustdata.api.core.JsonBoolean
+import com.braintrustdata.api.core.JsonMissing
+import com.braintrustdata.api.core.JsonNull
+import com.braintrustdata.api.core.JsonNumber
+import com.braintrustdata.api.core.JsonObject
+import com.braintrustdata.api.core.JsonString
+import com.braintrustdata.api.core.JsonValue
import com.braintrustdata.api.core.toImmutable
import java.util.TreeMap
@@ -28,6 +38,19 @@ private constructor(
TreeMap(String.CASE_INSENSITIVE_ORDER)
private var size: Int = 0
+ fun put(name: String, value: JsonValue): Builder = apply {
+ when (value) {
+ is JsonMissing,
+ is JsonNull -> {}
+ is JsonBoolean -> put(name, value.value.toString())
+ is JsonNumber -> put(name, value.value.toString())
+ is JsonString -> put(name, value.value)
+ is JsonArray -> value.values.forEach { put(name, it) }
+ is JsonObject ->
+ value.values.forEach { (nestedName, value) -> put("$name.$nestedName", value) }
+ }
+ }
+
fun put(name: String, value: String) = apply {
map.getOrPut(name) { mutableListOf() }.add(value)
size++
@@ -41,15 +64,6 @@ private constructor(
headers.names().forEach { put(it, headers.values(it)) }
}
- fun remove(name: String) = apply { size -= map.remove(name).orEmpty().size }
-
- fun removeAll(names: Set) = apply { names.forEach(::remove) }
-
- fun clear() = apply {
- map.clear()
- size = 0
- }
-
fun replace(name: String, value: String) = apply {
remove(name)
put(name, value)
@@ -68,6 +82,15 @@ private constructor(
headers.names().forEach { replace(it, headers.values(it)) }
}
+ fun remove(name: String) = apply { size -= map.remove(name).orEmpty().size }
+
+ fun removeAll(names: Set) = apply { names.forEach(::remove) }
+
+ fun clear() = apply {
+ map.clear()
+ size = 0
+ }
+
fun build() =
Headers(
map.mapValuesTo(TreeMap(String.CASE_INSENSITIVE_ORDER)) { (_, values) ->
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/HttpRequest.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/HttpRequest.kt
index 1105f7d4..64b9b171 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/HttpRequest.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/HttpRequest.kt
@@ -6,7 +6,7 @@ import com.braintrustdata.api.core.toImmutable
class HttpRequest
private constructor(
@get:JvmName("method") val method: HttpMethod,
- @get:JvmName("url") val url: String?,
+ @get:JvmName("baseUrl") val baseUrl: String,
@get:JvmName("pathSegments") val pathSegments: List,
@get:JvmName("headers") val headers: Headers,
@get:JvmName("queryParams") val queryParams: QueryParams,
@@ -16,7 +16,7 @@ private constructor(
fun toBuilder(): Builder = Builder().from(this)
override fun toString(): String =
- "HttpRequest{method=$method, url=$url, pathSegments=$pathSegments, headers=$headers, queryParams=$queryParams, body=$body}"
+ "HttpRequest{method=$method, baseUrl=$baseUrl, pathSegments=$pathSegments, headers=$headers, queryParams=$queryParams, body=$body}"
companion object {
@JvmStatic fun builder() = Builder()
@@ -25,7 +25,7 @@ private constructor(
class Builder internal constructor() {
private var method: HttpMethod? = null
- private var url: String? = null
+ private var baseUrl: String? = null
private var pathSegments: MutableList = mutableListOf()
private var headers: Headers.Builder = Headers.builder()
private var queryParams: QueryParams.Builder = QueryParams.builder()
@@ -34,7 +34,7 @@ private constructor(
@JvmSynthetic
internal fun from(request: HttpRequest) = apply {
method = request.method
- url = request.url
+ baseUrl = request.baseUrl
pathSegments = request.pathSegments.toMutableList()
headers = request.headers.toBuilder()
queryParams = request.queryParams.toBuilder()
@@ -43,7 +43,7 @@ private constructor(
fun method(method: HttpMethod) = apply { this.method = method }
- fun url(url: String) = apply { this.url = url }
+ fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
fun addPathSegment(pathSegment: String) = apply { pathSegments.add(pathSegment) }
@@ -53,90 +53,90 @@ private constructor(
fun headers(headers: Headers) = apply {
this.headers.clear()
- putAllHeaders(headers)
+ putAllheaders(headers)
}
fun headers(headers: Map>) = apply {
this.headers.clear()
- putAllHeaders(headers)
+ putAllheaders(headers)
}
fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
- fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+ fun putheaders(name: String, values: Iterable) = apply { headers.put(name, values) }
- fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
+ fun putAllheaders(headers: Headers) = apply { this.headers.putAll(headers) }
- fun putAllHeaders(headers: Map>) = apply {
+ fun putAllheaders(headers: Map>) = apply {
this.headers.putAll(headers)
}
- fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
+ fun replaceheaders(name: String, value: String) = apply { headers.replace(name, value) }
- fun replaceHeaders(name: String, values: Iterable) = apply {
+ fun replaceheaders(name: String, values: Iterable) = apply {
headers.replace(name, values)
}
- fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+ fun replaceAllheaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
- fun replaceAllHeaders(headers: Map>) = apply {
+ fun replaceAllheaders(headers: Map>) = apply {
this.headers.replaceAll(headers)
}
- fun removeHeaders(name: String) = apply { headers.remove(name) }
+ fun removeheaders(name: String) = apply { headers.remove(name) }
- fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
+ fun removeAllheaders(names: Set) = apply { headers.removeAll(names) }
fun queryParams(queryParams: QueryParams) = apply {
this.queryParams.clear()
- putAllQueryParams(queryParams)
+ putAllquery_params(queryParams)
}
fun queryParams(queryParams: Map>) = apply {
this.queryParams.clear()
- putAllQueryParams(queryParams)
+ putAllquery_params(queryParams)
}
fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
- fun putQueryParams(key: String, values: Iterable) = apply {
+ fun putquery_params(key: String, values: Iterable) = apply {
queryParams.put(key, values)
}
- fun putAllQueryParams(queryParams: QueryParams) = apply {
+ fun putAllquery_params(queryParams: QueryParams) = apply {
this.queryParams.putAll(queryParams)
}
- fun putAllQueryParams(queryParams: Map>) = apply {
+ fun putAllquery_params(queryParams: Map>) = apply {
this.queryParams.putAll(queryParams)
}
- fun replaceQueryParams(key: String, value: String) = apply {
+ fun replacequery_params(key: String, value: String) = apply {
queryParams.replace(key, value)
}
- fun replaceQueryParams(key: String, values: Iterable) = apply {
+ fun replacequery_params(key: String, values: Iterable) = apply {
queryParams.replace(key, values)
}
- fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ fun replaceAllquery_params(queryParams: QueryParams) = apply {
this.queryParams.replaceAll(queryParams)
}
- fun replaceAllQueryParams(queryParams: Map>) = apply {
+ fun replaceAllquery_params(queryParams: Map>) = apply {
this.queryParams.replaceAll(queryParams)
}
- fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
+ fun removequery_params(key: String) = apply { queryParams.remove(key) }
- fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
+ fun removeAllquery_params(keys: Set) = apply { queryParams.removeAll(keys) }
fun body(body: HttpRequestBody) = apply { this.body = body }
fun build(): HttpRequest =
HttpRequest(
checkRequired("method", method),
- url,
+ checkRequired("baseUrl", baseUrl),
pathSegments.toImmutable(),
headers.build(),
queryParams.build(),
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/PhantomReachableClosingAsyncStreamResponse.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/PhantomReachableClosingAsyncStreamResponse.kt
new file mode 100644
index 00000000..6d826dd4
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/PhantomReachableClosingAsyncStreamResponse.kt
@@ -0,0 +1,56 @@
+package com.braintrustdata.api.core.http
+
+import com.braintrustdata.api.core.closeWhenPhantomReachable
+import com.braintrustdata.api.core.http.AsyncStreamResponse.Handler
+import java.util.Optional
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+
+/**
+ * A delegating wrapper around an `AsyncStreamResponse` that closes it once it's only phantom
+ * reachable.
+ *
+ * This class ensures the `AsyncStreamResponse` is closed even if the user forgets to close it.
+ */
+internal class PhantomReachableClosingAsyncStreamResponse(
+ private val asyncStreamResponse: AsyncStreamResponse
+) : AsyncStreamResponse {
+
+ /**
+ * An object used for keeping `asyncStreamResponse` open while the object is still reachable.
+ */
+ private val reachabilityTracker = Object()
+
+ init {
+ closeWhenPhantomReachable(reachabilityTracker, asyncStreamResponse::close)
+ }
+
+ override fun subscribe(handler: Handler): AsyncStreamResponse = apply {
+ asyncStreamResponse.subscribe(TrackedHandler(handler, reachabilityTracker))
+ }
+
+ override fun subscribe(handler: Handler, executor: Executor): AsyncStreamResponse =
+ apply {
+ asyncStreamResponse.subscribe(TrackedHandler(handler, reachabilityTracker), executor)
+ }
+
+ override fun onCompleteFuture(): CompletableFuture =
+ asyncStreamResponse.onCompleteFuture()
+
+ override fun close() = asyncStreamResponse.close()
+}
+
+/**
+ * A wrapper around a `Handler` that also references a `reachabilityTracker` object.
+ *
+ * Referencing the `reachabilityTracker` object prevents it from getting reclaimed while the handler
+ * is still reachable.
+ */
+private class TrackedHandler(
+ private val handler: Handler,
+ private val reachabilityTracker: Any,
+) : Handler {
+ override fun onNext(value: T) = handler.onNext(value)
+
+ override fun onComplete(error: Optional) = handler.onComplete(error)
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/PhantomReachableClosingStreamResponse.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/PhantomReachableClosingStreamResponse.kt
new file mode 100644
index 00000000..9c79ceb6
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/PhantomReachableClosingStreamResponse.kt
@@ -0,0 +1,21 @@
+package com.braintrustdata.api.core.http
+
+import com.braintrustdata.api.core.closeWhenPhantomReachable
+import java.util.stream.Stream
+
+/**
+ * A delegating wrapper around a `StreamResponse` that closes it once it's only phantom reachable.
+ *
+ * This class ensures the `StreamResponse` is closed even if the user forgets to close it.
+ */
+internal class PhantomReachableClosingStreamResponse(
+ private val streamResponse: StreamResponse
+) : StreamResponse {
+ init {
+ closeWhenPhantomReachable(this, streamResponse)
+ }
+
+ override fun stream(): Stream = streamResponse.stream()
+
+ override fun close() = streamResponse.close()
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/QueryParams.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/QueryParams.kt
index 03c719df..60103d82 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/QueryParams.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/QueryParams.kt
@@ -2,6 +2,14 @@
package com.braintrustdata.api.core.http
+import com.braintrustdata.api.core.JsonArray
+import com.braintrustdata.api.core.JsonBoolean
+import com.braintrustdata.api.core.JsonMissing
+import com.braintrustdata.api.core.JsonNull
+import com.braintrustdata.api.core.JsonNumber
+import com.braintrustdata.api.core.JsonObject
+import com.braintrustdata.api.core.JsonString
+import com.braintrustdata.api.core.JsonValue
import com.braintrustdata.api.core.toImmutable
class QueryParams
@@ -28,6 +36,39 @@ private constructor(
private val map: MutableMap> = mutableMapOf()
private var size: Int = 0
+ fun put(key: String, value: JsonValue): Builder = apply {
+ when (value) {
+ is JsonMissing,
+ is JsonNull -> {}
+ is JsonBoolean -> put(key, value.value.toString())
+ is JsonNumber -> put(key, value.value.toString())
+ is JsonString -> put(key, value.value)
+ is JsonArray ->
+ put(
+ key,
+ value.values
+ .asSequence()
+ .mapNotNull {
+ when (it) {
+ is JsonMissing,
+ is JsonNull -> null
+ is JsonBoolean -> it.value.toString()
+ is JsonNumber -> it.value.toString()
+ is JsonString -> it.value
+ is JsonArray,
+ is JsonObject ->
+ throw IllegalArgumentException(
+ "Cannot comma separate non-primitives in query params"
+ )
+ }
+ }
+ .joinToString(","),
+ )
+ is JsonObject ->
+ value.values.forEach { (nestedKey, value) -> put("$key[$nestedKey]", value) }
+ }
+ }
+
fun put(key: String, value: String) = apply {
map.getOrPut(key) { mutableListOf() }.add(value)
size++
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/RetryingHttpClient.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/RetryingHttpClient.kt
index 830c55cc..da690b8b 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/RetryingHttpClient.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/RetryingHttpClient.kt
@@ -3,6 +3,7 @@ package com.braintrustdata.api.core.http
import com.braintrustdata.api.core.RequestOptions
import com.braintrustdata.api.core.checkRequired
import com.braintrustdata.api.errors.BraintrustIoException
+import com.braintrustdata.api.errors.BraintrustRetryableException
import java.io.IOException
import java.time.Clock
import java.time.Duration
@@ -176,9 +177,10 @@ private constructor(
}
private fun shouldRetry(throwable: Throwable): Boolean =
- // Only retry IOException and BraintrustIoException, other exceptions are not intended to be
- // retried.
- throwable is IOException || throwable is BraintrustIoException
+ // Only retry known retryable exceptions, other exceptions are not intended to be retried.
+ throwable is IOException ||
+ throwable is BraintrustIoException ||
+ throwable is BraintrustRetryableException
private fun getRetryBackoffDuration(retries: Int, response: HttpResponse?): Duration {
// About the Retry-After header:
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/StreamResponse.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/StreamResponse.kt
new file mode 100644
index 00000000..3903400b
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/StreamResponse.kt
@@ -0,0 +1,19 @@
+package com.braintrustdata.api.core.http
+
+import java.util.stream.Stream
+
+interface StreamResponse : AutoCloseable {
+
+ fun stream(): Stream
+
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
+}
+
+@JvmSynthetic
+internal fun StreamResponse.map(transform: (T) -> R): StreamResponse =
+ object : StreamResponse {
+ override fun stream(): Stream = this@map.stream().map(transform)
+
+ override fun close() = this@map.close()
+ }
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustRetryableException.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustRetryableException.kt
new file mode 100644
index 00000000..e70380e9
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustRetryableException.kt
@@ -0,0 +1,15 @@
+package com.braintrustdata.api.errors
+
+/**
+ * Exception that indicates a transient error that can be retried.
+ *
+ * When this exception is thrown during an HTTP request, the SDK will automatically retry the
+ * request up to the maximum number of retries.
+ *
+ * @param message A descriptive error message
+ * @param cause The underlying cause of this exception, if any
+ */
+class BraintrustRetryableException
+@JvmOverloads
+constructor(message: String? = null, cause: Throwable? = null) :
+ BraintrustException(message, cause)
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AISecret.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AISecret.kt
index f5ad5a1b..474fcbe7 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AISecret.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AISecret.kt
@@ -331,20 +331,20 @@ private constructor(
fun additionalProperties(additionalProperties: Map) = apply {
this.additionalProperties.clear()
- putAllAdditionalProperties(additionalProperties)
+ putAlladditional_properties(additionalProperties)
}
fun putAdditionalProperty(key: String, value: JsonValue) = apply {
additionalProperties.put(key, value)
}
- fun putAllAdditionalProperties(additionalProperties: Map) = apply {
+ fun putAlladditional_properties(additionalProperties: Map) = apply {
this.additionalProperties.putAll(additionalProperties)
}
fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) }
- fun removeAllAdditionalProperties(keys: Set) = apply {
+ fun removeAlladditional_properties(keys: Set) = apply {
keys.forEach(::removeAdditionalProperty)
}
@@ -449,20 +449,20 @@ private constructor(
fun additionalProperties(additionalProperties: Map) = apply {
this.additionalProperties.clear()
- putAllAdditionalProperties(additionalProperties)
+ putAlladditional_properties(additionalProperties)
}
fun putAdditionalProperty(key: String, value: JsonValue) = apply {
additionalProperties.put(key, value)
}
- fun putAllAdditionalProperties(additionalProperties: Map) = apply {
+ fun putAlladditional_properties(additionalProperties: Map) = apply {
this.additionalProperties.putAll(additionalProperties)
}
fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) }
- fun removeAllAdditionalProperties(keys: Set) = apply {
+ fun removeAlladditional_properties(keys: Set) = apply {
keys.forEach(::removeAdditionalProperty)
}
@@ -507,12 +507,10 @@ private constructor(
return true
}
- return /* spotless:off */ other is Metadata && additionalProperties == other.additionalProperties /* spotless:on */
+ return other is Metadata && additionalProperties == other.additionalProperties
}
- /* spotless:off */
private val hashCode: Int by lazy { Objects.hash(additionalProperties) }
- /* spotless:on */
override fun hashCode(): Int = hashCode
@@ -524,12 +522,31 @@ private constructor(
return true
}
- return /* spotless:off */ other is AISecret && id == other.id && name == other.name && orgId == other.orgId && created == other.created && metadata == other.metadata && previewSecret == other.previewSecret && type == other.type && updatedAt == other.updatedAt && additionalProperties == other.additionalProperties /* spotless:on */
+ return other is AISecret &&
+ id == other.id &&
+ name == other.name &&
+ orgId == other.orgId &&
+ created == other.created &&
+ metadata == other.metadata &&
+ previewSecret == other.previewSecret &&
+ type == other.type &&
+ updatedAt == other.updatedAt &&
+ additionalProperties == other.additionalProperties
}
- /* spotless:off */
- private val hashCode: Int by lazy { Objects.hash(id, name, orgId, created, metadata, previewSecret, type, updatedAt, additionalProperties) }
- /* spotless:on */
+ private val hashCode: Int by lazy {
+ Objects.hash(
+ id,
+ name,
+ orgId,
+ created,
+ metadata,
+ previewSecret,
+ type,
+ updatedAt,
+ additionalProperties,
+ )
+ }
override fun hashCode(): Int = hashCode
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/Acl.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/Acl.kt
index 34941085..617a3137 100755
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/Acl.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/Acl.kt
@@ -454,20 +454,20 @@ private constructor(
fun additionalProperties(additionalProperties: Map) = apply {
this.additionalProperties.clear()
- putAllAdditionalProperties(additionalProperties)
+ putAlladditional_properties(additionalProperties)
}
fun putAdditionalProperty(key: String, value: JsonValue) = apply {
additionalProperties.put(key, value)
}
- fun putAllAdditionalProperties(additionalProperties: Map) = apply {
+ fun putAlladditional_properties(additionalProperties: Map) = apply {
this.additionalProperties.putAll(additionalProperties)
}
fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) }
- fun removeAllAdditionalProperties(keys: Set) = apply {
+ fun removeAlladditional_properties(keys: Set) = apply {
keys.forEach(::removeAdditionalProperty)
}
@@ -553,12 +553,35 @@ private constructor(
return true
}
- return /* spotless:off */ other is Acl && id == other.id && _objectOrgId == other._objectOrgId && objectId == other.objectId && objectType == other.objectType && created == other.created && groupId == other.groupId && permission == other.permission && restrictObjectType == other.restrictObjectType && roleId == other.roleId && userId == other.userId && additionalProperties == other.additionalProperties /* spotless:on */
+ return other is Acl &&
+ id == other.id &&
+ _objectOrgId == other._objectOrgId &&
+ objectId == other.objectId &&
+ objectType == other.objectType &&
+ created == other.created &&
+ groupId == other.groupId &&
+ permission == other.permission &&
+ restrictObjectType == other.restrictObjectType &&
+ roleId == other.roleId &&
+ userId == other.userId &&
+ additionalProperties == other.additionalProperties
}
- /* spotless:off */
- private val hashCode: Int by lazy { Objects.hash(id, _objectOrgId, objectId, objectType, created, groupId, permission, restrictObjectType, roleId, userId, additionalProperties) }
- /* spotless:on */
+ private val hashCode: Int by lazy {
+ Objects.hash(
+ id,
+ _objectOrgId,
+ objectId,
+ objectType,
+ created,
+ groupId,
+ permission,
+ restrictObjectType,
+ roleId,
+ userId,
+ additionalProperties,
+ )
+ }
override fun hashCode(): Int = hashCode
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AclBatchUpdateParams.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AclBatchUpdateParams.kt
index 3276e25a..0a07191a 100644
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AclBatchUpdateParams.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/models/AclBatchUpdateParams.kt
@@ -79,8 +79,10 @@ private constructor(
fun _additionalBodyProperties(): Map = body._additionalProperties()
+ /** Additional headers to send with the request. */
fun _additionalHeaders(): Headers = additionalHeaders
+ /** Additional query param to send with the request. */
fun _additionalQueryParams(): QueryParams = additionalQueryParams
fun toBuilder() = Builder().from(this)
@@ -189,112 +191,111 @@ private constructor(
body.putAdditionalProperty(key, value)
}
- fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) =
+ fun putAlladditional_body_properties(additionalBodyProperties: Map) =
apply {
- body.putAllAdditionalProperties(additionalBodyProperties)
+ body.putAlladditional_body_properties(additionalBodyProperties)
}
fun removeAdditionalBodyProperty(key: String) = apply { body.removeAdditionalProperty(key) }
- fun removeAllAdditionalBodyProperties(keys: Set) = apply {
- body.removeAllAdditionalProperties(keys)
+ fun removeAlladditional_body_properties(keys: Set) = apply {
+ body.removeAlladditional_body_properties(keys)
}
fun additionalHeaders(additionalHeaders: Headers) = apply {
this.additionalHeaders.clear()
- putAllAdditionalHeaders(additionalHeaders)
+ putAlladditional_headers(additionalHeaders)
}
fun additionalHeaders(additionalHeaders: Map>) = apply {
this.additionalHeaders.clear()
- putAllAdditionalHeaders(additionalHeaders)
+ putAlladditional_headers(additionalHeaders)
}
fun putAdditionalHeader(name: String, value: String) = apply {
additionalHeaders.put(name, value)
}
- fun putAdditionalHeaders(name: String, values: Iterable) = apply {
+ fun putadditional_headers(name: String, values: Iterable) = apply {
additionalHeaders.put(name, values)
}
- fun putAllAdditionalHeaders(additionalHeaders: Headers) = apply {
+ fun putAlladditional_headers(additionalHeaders: Headers) = apply {
this.additionalHeaders.putAll(additionalHeaders)
}
- fun putAllAdditionalHeaders(additionalHeaders: Map>) = apply {
+ fun putAlladditional_headers(additionalHeaders: Map>) = apply {
this.additionalHeaders.putAll(additionalHeaders)
}
- fun replaceAdditionalHeaders(name: String, value: String) = apply {
+ fun replaceadditional_headers(name: String, value: String) = apply {
additionalHeaders.replace(name, value)
}
- fun replaceAdditionalHeaders(name: String, values: Iterable) = apply {
+ fun replaceadditional_headers(name: String, values: Iterable) = apply {
additionalHeaders.replace(name, values)
}
- fun replaceAllAdditionalHeaders(additionalHeaders: Headers) = apply {
+ fun replaceAlladditional_headers(additionalHeaders: Headers) = apply {
this.additionalHeaders.replaceAll(additionalHeaders)
}
- fun replaceAllAdditionalHeaders(additionalHeaders: Map>) = apply {
+ fun replaceAlladditional_headers(additionalHeaders: Map>) = apply {
this.additionalHeaders.replaceAll(additionalHeaders)
}
- fun removeAdditionalHeaders(name: String) = apply { additionalHeaders.remove(name) }
+ fun removeadditional_headers(name: String) = apply { additionalHeaders.remove(name) }
- fun removeAllAdditionalHeaders(names: Set) = apply {
+ fun removeAlladditional_headers(names: Set) = apply {
additionalHeaders.removeAll(names)
}
fun additionalQueryParams(additionalQueryParams: QueryParams) = apply {
this.additionalQueryParams.clear()
- putAllAdditionalQueryParams(additionalQueryParams)
+ putAlladditional_query_params(additionalQueryParams)
}
fun additionalQueryParams(additionalQueryParams: Map>) = apply {
this.additionalQueryParams.clear()
- putAllAdditionalQueryParams(additionalQueryParams)
+ putAlladditional_query_params(additionalQueryParams)
}
fun putAdditionalQueryParam(key: String, value: String) = apply {
additionalQueryParams.put(key, value)
}
- fun putAdditionalQueryParams(key: String, values: Iterable) = apply {
+ fun putadditional_query_params(key: String, values: Iterable) = apply {
additionalQueryParams.put(key, values)
}
- fun putAllAdditionalQueryParams(additionalQueryParams: QueryParams) = apply {
+ fun putAlladditional_query_params(additionalQueryParams: QueryParams) = apply {
this.additionalQueryParams.putAll(additionalQueryParams)
}
- fun putAllAdditionalQueryParams(additionalQueryParams: Map