diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 1b9642e0..bd8e2619 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,19 +1,23 @@
# syntax=docker/dockerfile:1
FROM debian:bookworm-slim
-RUN apt-get update && apt-get install -y \
+RUN apt-get update && apt-get install -y --no-install-recommends \
libxkbcommon0 \
ca-certificates \
ca-certificates-java \
make \
curl \
git \
- openjdk-17-jdk \
+ openjdk-17-jdk-headless \
unzip \
libc++1 \
vim \
&& apt-get clean autoclean
+# Ensure UTF-8 encoding
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+
WORKDIR /workspace
COPY . /workspace
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 93873e87..42805473 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,33 +1,85 @@
name: CI
on:
push:
- branches:
- - main
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
pull_request:
- branches:
- - main
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
jobs:
lint:
+ timeout-minutes: 15
name: lint
- runs-on: ubuntu-latest
- if: github.repository == 'braintrustdata/braintrust-java'
+ 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@v3
-
+ - 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: 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: 15
+ name: test
+ 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@v3
+ uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- - name: Gradle build
- run: |
- ./gradlew build testClasses -x test
+ - name: Run tests
+ run: ./scripts/test
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
old mode 100644
new mode 100755
index e21ef37f..d914ae66
--- a/.github/workflows/publish-sonatype.yml
+++ b/.github/workflows/publish-sonatype.yml
@@ -14,26 +14,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Set up Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: temurin
java-version: |
8
- 17
+ 21
cache: gradle
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- name: Publish to Sonatype
- run: |
- ./gradlew publish --stacktrace
+ run: |-
+ export -- GPG_SIGNING_KEY_ID
+ printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
+ GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
+ ./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
env:
SONATYPE_USERNAME: ${{ secrets.BRAINTRUST_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.BRAINTRUST_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
- GPG_SIGNING_KEY_ID: ${{ secrets.BRAINTRUST_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }}
GPG_SIGNING_KEY: ${{ secrets.BRAINTRUST_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
GPG_SIGNING_PASSWORD: ${{ secrets.BRAINTRUST_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
old mode 100644
new mode 100755
index c0508157..d9128fbc
--- a/.github/workflows/release-doctor.yml
+++ b/.github/workflows/release-doctor.yml
@@ -1,6 +1,8 @@
name: Release Doctor
on:
pull_request:
+ branches:
+ - main
workflow_dispatch:
jobs:
@@ -10,7 +12,7 @@ jobs:
if: github.repository == 'braintrustdata/braintrust-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- name: Check release environment
run: |
@@ -18,6 +20,5 @@ jobs:
env:
SONATYPE_USERNAME: ${{ secrets.BRAINTRUST_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.BRAINTRUST_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
- GPG_SIGNING_KEY_ID: ${{ secrets.BRAINTRUST_SONATYPE_GPG_SIGNING_KEY_ID || secrets.GPG_SIGNING_KEY_ID }}
GPG_SIGNING_KEY: ${{ secrets.BRAINTRUST_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
GPG_SIGNING_PASSWORD: ${{ secrets.BRAINTRUST_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index face920d..b1346e6d
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
+.prism.log
.gradle
.idea
-build
+.kotlin
+build/
codegen.log
+kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
old mode 100644
new mode 100755
index 10f30916..6d78745c
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.2.0"
+ ".": "0.9.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index 423e2f9a..2e43a77b 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1 +1,4 @@
-configured_endpoints: 31
+configured_endpoints: 110
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/manugoyal%2Fbraintrust-sdk-f0d64ce0e0efde75f9c171f7f3c3d4a72f00a77abb3bc5a7d65b7be1e715689b.yml
+openapi_spec_hash: a027e48cc6aea2fab3cbdd38f4081119
+config_hash: dca6e2cafd0764aa5fa3e78987e8b07c
diff --git a/CHANGELOG.md b/CHANGELOG.md
old mode 100644
new mode 100755
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
index 5e03e95a..f3b9e7e5
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2024 Braintrust
+ Copyright 2025 Braintrust
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 151dfe2b..8d3b176c 100644
--- a/README.md
+++ b/README.md
@@ -2,50 +2,84 @@
-[](https://central.sonatype.com/artifact/com.braintrustdata.api/braintrust-java/0.2.0)
+[](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)
-The Braintrust Java SDK provides convenient access to the Braintrust REST API from applications written in Java. It includes helper classes with helpful types and documentation for every request and response property.
+The Braintrust Java SDK provides convenient access to the [Braintrust REST API](https://www.braintrustdata.com/docs/api/spec) from applications written in Java.
The Braintrust Java SDK is similar to the Braintrust Kotlin SDK but with minor differences that make it more ergonomic for use in Java, such as `Optional` instead of nullable values, `Stream` instead of `Sequence`, and `CompletableFuture` instead of suspend functions.
-## Documentation
+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). The full API of this library can be found in [api.md](api.md).
-
----
+
-## Getting started
+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).
-### Install dependencies
+
-#### Gradle
+## Installation
+### Gradle
+
```kotlin
-implementation("com.braintrustdata.api:braintrust-java:0.2.0")
+implementation("com.braintrustdata.api:braintrust-java:0.9.0")
```
-#### Maven
+### Maven
```xml
- com.braintrustdata.api
- braintrust-java
- 0.2.0
+ com.braintrustdata.api
+ braintrust-java
+ 0.9.0
```
-### Configure the client
+## Requirements
+
+This library requires Java 8 or later.
+
+## Usage
+
+```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectCreateParams;
+
+// 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()
+ .name("foobar")
+ .build();
+Project project = client.projects().create(params);
+```
+
+## Client configuration
-Use `BraintrustOkHttpClient.builder()` to configure the client. At a minimum you need to set `.apiKey()`:
+Configure the client using system properties or environment variables:
```java
-import com.braintrustdata.api.client.BraintrustOkHttpClient;
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+
+// 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();
+```
+
+Or manually:
+
+```java
+import com.braintrustdata.api.client.BraintrustClient;
import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
BraintrustClient client = BraintrustOkHttpClient.builder()
@@ -53,205 +87,313 @@ BraintrustClient client = BraintrustOkHttpClient.builder()
.build();
```
-Alternately, set the environment with `BRAINTRUST_API_KEY`, and use `BraintrustOkHttpClient.fromEnv()` to read from the environment.
+Or using a combination of the two approaches:
```java
-BraintrustClient client = BraintrustOkHttpClient.fromEnv();
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
-// Note: you can also call fromEnv() from the client builder, for example if you need to set additional properties
BraintrustClient client = BraintrustOkHttpClient.builder()
+ // Configures using the `braintrust.apiKey` and `braintrust.baseUrl` system properties
+ // Or configures using the `BRAINTRUST_API_KEY` and `BRAINTRUST_BASE_URL` environment variables
.fromEnv()
- // ... set properties on the builder
+ .apiKey("My API Key")
.build();
```
-| Property | Environment variable | Required | Default value |
-| -------- | -------------------- | -------- | ------------- |
-| apiKey | `BRAINTRUST_API_KEY` | true | — |
+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"` |
-Read the documentation for more configuration options.
+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.
-### Example: creating a resource
+### Modifying configuration
-To create a new project, first use the `ProjectCreateParams` builder to specify attributes,
-then pass that to the `create` method of the `project` service.
+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.models.Project;
-import com.braintrustdata.api.models.ProjectCreateParams;
+import com.braintrustdata.api.client.BraintrustClient;
-ProjectCreateParams params = ProjectCreateParams.builder()
- .bodyparam(true)
- .build();
-Project project = client.project().create(params);
+BraintrustClient clientWithOptions = client.withOptions(optionsBuilder -> {
+ optionsBuilder.baseUrl("https://example.com");
+ optionsBuilder.maxRetries(42);
+});
```
-### Example: listing resources
+The `withOptions()` method does not affect the original client or service.
-The Braintrust API provides a `list` method to get a paginated list of project.
-You can retrieve the first page by:
+## Requests and responses
-```java
-import com.braintrustdata.api.models.Page;
-import com.braintrustdata.api.models.Project;
+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.
-ProjectListPage page = client.project().list();
-for (Project project : page.objects()) {
- System.out.println(project);
-}
-```
+For example, `client.projects().create(...)` should be called with an instance of `ProjectCreateParams`, and it will return an instance of `Project`.
-See [Pagination](#pagination) below for more information on transparently working with lists of objects without worrying about fetching each page.
+## Immutability
----
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
-## Requests
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
-### Parameters and bodies
+Because each class is immutable, builder modification will _never_ affect already built class instances.
-To make a request to the Braintrust API, you generally build an instance of the appropriate `Params` class.
+## Asynchronous execution
-In [Example: creating a resource](#example-creating-a-resource) above, we used the `ProjectCreateParams.builder()` to pass to
-the `create` method of the `project` service.
-
-Sometimes, the API may support other properties that are not yet supported in the Java SDK types. In that case,
-you can attach them using the `putAdditionalProperty` method.
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectCreateParams;
+import java.util.concurrent.CompletableFuture;
+
+// 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()
- // ... normal properties
- .putAdditionalProperty("secret_param", "4242")
+ .name("foobar")
.build();
+CompletableFuture project = client.async().projects().create(params);
```
-## Responses
+Or create an asynchronous client from the beginning:
-### Response validation
+```java
+import com.braintrustdata.api.client.BraintrustClientAsync;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClientAsync;
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectCreateParams;
+import java.util.concurrent.CompletableFuture;
-When receiving a response, the Braintrust Java SDK will deserialize it into instances of the typed model classes. In rare cases, the API may return a response property that doesn't match the expected Java type. If you directly access the mistaken property, the SDK will throw an unchecked `BraintrustInvalidDataException` at runtime. If you would prefer to check in advance that that response is completely well-typed, call `.validate()` on the returned model.
+// 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();
-```java
-Project project = client.project().create().validate();
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .name("foobar")
+ .build();
+CompletableFuture project = client.projects().create(params);
```
-### Response properties as JSON
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
+
+## Raw responses
-In rare cases, you may want to access the underlying JSON value for a response property rather than using the typed version provided by
-this SDK. Each model property has a corresponding JSON version, with an underscore before the method name, which returns a `JsonField` value.
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
+
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
```java
-JsonField field = responseObj._field();
+import com.braintrustdata.api.core.http.Headers;
+import com.braintrustdata.api.core.http.HttpResponseFor;
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectCreateParams;
-if (field.isMissing()) {
- // Value was not specified in the JSON response
-} else if (field.isNull()) {
- // Value was provided as a literal null
-} else {
- // See if value was provided as a string
- Optional jsonString = field.asString();
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .name("foobar")
+ .build();
+HttpResponseFor project = client.projects().withRawResponse().create(params);
- // If the value given by the API did not match the shape that the SDK expects
- // you can deserialise into a custom type
- MyClass myObj = responseObj._field().asUnknown().orElseThrow().convert(MyClass.class);
-}
+int statusCode = project.statusCode();
+Headers headers = project.headers();
```
-### Additional model properties
-
-Sometimes, the server response may include additional properties that are not yet available in this library's types. You can access them using the model's `_additionalProperties` method:
+You can still deserialize the response into an instance of a Java class if needed:
```java
-JsonValue secret = project._additionalProperties().get("secret_field");
+import com.braintrustdata.api.models.Project;
+
+Project parsedProject = project.parse();
```
----
+## Error handling
+
+The SDK throws custom unchecked exception types:
+
+- [`BraintrustServiceException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
+
+ | Status | Exception |
+ | ------ | -------------------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/UnexpectedStatusCodeException.kt) |
+
+- [`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
-// As an Iterable:
-ProjectListPage page = client.project().list(params);
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectListPage;
+
+ProjectListPage page = client.projects().list();
+
+// Process as an Iterable
for (Project project : page.autoPager()) {
System.out.println(project);
-};
+}
-// As a Stream:
-client.project().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.project().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
-ProjectListPage page = client.project().list(params);
-while (page != null) {
- for (Project project : page.objects()) {
+import com.braintrustdata.api.models.Project;
+import com.braintrustdata.api.models.ProjectListPage;
+
+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();
}
```
----
+## Logging
-## Error handling
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
+
+Enable logging by setting the `BRAINTRUST_LOG` environment variable to `info`:
+
+```sh
+export BRAINTRUST_LOG=info
+```
+
+Or to `debug` for more verbose logging:
+
+```sh
+export BRAINTRUST_LOG=debug
+```
+
+## ProGuard and R8
-This library throws exceptions in a single hierarchy for easy handling:
+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).
-- **`BraintrustException`** - Base exception for all exceptions
+ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
- - **`BraintrustServiceException`** - HTTP errors with a well-formed response body we were able to parse. The exception message and the `.debuggingRequestId()` will be set by the server.
+## Jackson
- | 400 | BadRequestException |
- | ------ | ----------------------------- |
- | 401 | AuthenticationException |
- | 403 | PermissionDeniedException |
- | 404 | NotFoundException |
- | 422 | UnprocessableEntityException |
- | 429 | RateLimitException |
- | 5xx | InternalServerException |
- | others | UnexpectedStatusCodeException |
+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.
- - **`BraintrustIoException`** - I/O networking errors
- - **`BraintrustInvalidDataException`** - any other exceptions on the client side, e.g.:
- - We failed to serialize the request body
- - We failed to parse the response body (has access to response code and body)
+The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
+
+If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`BraintrustOkHttpClient`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt) or [`BraintrustOkHttpClientAsync`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt).
+
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
## Network options
### Retries
-Requests that experience certain errors are automatically retried 2 times by default, with a short exponential backoff. Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors will all be retried by default.
-You can provide a `maxRetries` on the client builder to configure this:
+The SDK automatically retries 2 times by default, 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.
+
+To set a custom number of retries, configure the client using the `maxRetries` method:
```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+
BraintrustClient client = BraintrustOkHttpClient.builder()
.fromEnv()
.maxRetries(4)
@@ -260,9 +402,25 @@ BraintrustClient client = BraintrustOkHttpClient.builder()
### Timeouts
-Requests time out after 1 minute by default. You can configure this on the client builder:
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
+
+```java
+import com.braintrustdata.api.models.Project;
+
+Project project = client.projects().create(
+ params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+import java.time.Duration;
+
BraintrustClient client = BraintrustOkHttpClient.builder()
.fromEnv()
.timeout(Duration.ofSeconds(30))
@@ -271,23 +429,305 @@ BraintrustClient client = BraintrustOkHttpClient.builder()
### Proxies
-Requests can be routed through a proxy. You can configure this on the client builder:
+To route requests through a proxy, configure the client using the `proxy` method:
```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
BraintrustClient client = BraintrustOkHttpClient.builder()
.fromEnv()
.proxy(new Proxy(
- Type.HTTP,
- new InetSocketAddress("proxy.com", 8080)
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
))
.build();
```
-## Semantic Versioning
+### 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:
+
+- `braintrust-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`BraintrustClient`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClient.kt), [`BraintrustClientAsync`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsync.kt), [`BraintrustClientImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt), and [`BraintrustClientAsyncImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt), all of which can work with any HTTP client
+- `braintrust-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`BraintrustOkHttpClient`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt) and [`BraintrustOkHttpClientAsync`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt), which provide a way to construct [`BraintrustClientImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt) and [`BraintrustClientAsyncImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt), respectively, using OkHttp
+- `braintrust-java`
+ - Depends on and exposes the APIs of both `braintrust-java-core` and `braintrust-java-client-okhttp`
+ - Does not have its own logic
+
+This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.
+
+#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html)
+
+> [!TIP]
+> Try the available [network options](#network-options) before replacing the default client.
+
+To use a customized `OkHttpClient`:
+
+1. Replace your [`braintrust-java` dependency](#installation) with `braintrust-java-core`
+2. Copy `braintrust-java-client-okhttp`'s [`OkHttpClient`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`BraintrustClientImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt) or [`BraintrustClientAsyncImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt), similarly to [`BraintrustOkHttpClient`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt) or [`BraintrustOkHttpClientAsync`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt), using your customized client
+
+### Completely custom HTTP client
+
+To use a completely custom HTTP client:
+
+1. Replace your [`braintrust-java` dependency](#installation) with `braintrust-java-core`
+2. Write a class that implements the [`HttpClient`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/http/HttpClient.kt) interface
+3. Construct [`BraintrustClientImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientImpl.kt) or [`BraintrustClientAsyncImpl`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/client/BraintrustClientAsyncImpl.kt), similarly to [`BraintrustOkHttpClient`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClient.kt) or [`BraintrustOkHttpClientAsync`](braintrust-java-client-okhttp/src/main/kotlin/com/braintrustdata/api/client/okhttp/BraintrustOkHttpClientAsync.kt), using your new client class
+
+## Undocumented API functionality
+
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
+
+### Parameters
+
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
+
+```java
+import com.braintrustdata.api.core.JsonValue;
+import com.braintrustdata.api.models.ProjectCreateParams;
+
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .putAdditionalHeader("Secret-Header", "42")
+ .putAdditionalQueryParam("secret_query_param", "42")
+ .putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
+ .build();
+```
+
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
+
+To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:
+
+```java
+import com.braintrustdata.api.core.JsonValue;
+import com.braintrustdata.api.models.ProjectSettings;
+import com.braintrustdata.api.models.ProjectUpdateParams;
+
+ProjectUpdateParams params = ProjectUpdateParams.builder()
+ .settings(ProjectSettings.builder()
+ .putAdditionalProperty("secretProperty", JsonValue.from("42"))
+ .build())
+ .build();
+```
+
+These properties can be accessed on the nested built object later using the `_additionalProperties()` method.
+
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Values.kt) object to its setter:
+
+```java
+import com.braintrustdata.api.core.JsonValue;
+import com.braintrustdata.api.models.ProjectCreateParams;
+
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .name(JsonValue.from(42))
+ .build();
+```
+
+The most straightforward way to create a [`JsonValue`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Values.kt) is using its `from(...)` method:
+
+```java
+import com.braintrustdata.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
+
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
+
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
+
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
+```
+
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
+
+To forcibly omit a required parameter or property, pass [`JsonMissing`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Values.kt):
+
+```java
+import com.braintrustdata.api.core.JsonMissing;
+import com.braintrustdata.api.models.ProjectCreateParams;
+
+ProjectCreateParams params = ProjectCreateParams.builder()
+ .name(JsonMissing.of())
+ .build();
+```
+
+### Response properties
+
+To access undocumented response properties, call the `_additionalProperties()` method:
+
+```java
+import com.braintrustdata.api.core.JsonValue;
+import java.util.Map;
+
+Map additionalProperties = client.projects().create(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
+
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
+
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
+
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
+
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
+```
+
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
+
+```java
+import com.braintrustdata.api.core.JsonField;
+import java.util.Optional;
+
+JsonField name = client.projects().create(params)._name();
+
+if (name.isMissing()) {
+ // The property is absent from the JSON response
+} else if (name.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = name.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = name.asUnknown().orElseThrow().convert(MyClass.class);
+}
+```
+
+### Response validation
+
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+
+By default, the SDK will not throw an exception in this case. It will throw [`BraintrustInvalidDataException`](braintrust-java-core/src/main/kotlin/com/braintrustdata/api/errors/BraintrustInvalidDataException.kt) only if you directly access the property.
+
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
+
+```java
+import com.braintrustdata.api.models.Project;
+
+Project project = client.projects().create(params).validate();
+```
+
+Or configure the method call to validate the response using the `responseValidation` method:
+
+```java
+import com.braintrustdata.api.models.Project;
+
+Project project = client.projects().create(
+ params, RequestOptions.builder().responseValidation(true).build()
+);
+```
+
+Or configure the default for all method calls at the client level:
+
+```java
+import com.braintrustdata.api.client.BraintrustClient;
+import com.braintrustdata.api.client.okhttp.BraintrustOkHttpClient;
+
+BraintrustClient client = BraintrustOkHttpClient.builder()
+ .fromEnv()
+ .responseValidation(true)
+ .build();
+```
+
+## FAQ
+
+### Why don't you use plain `enum` classes?
+
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
+
+### Why do you represent fields using `JsonField` instead of just plain `T`?
+
+Using `JsonField` enables a few features:
+
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
+
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
+
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
+
+### Why don't you use checked exceptions?
+
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
+
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
+
+## Semantic versioning
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
-1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals)_.
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
2. Changes that we do not expect to impact the vast majority of users in practice.
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..a7f4f3b4
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,27 @@
+# Security Policy
+
+## Reporting Security Issues
+
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+
+To report a security issue, please contact the Stainless team at security@stainless.com.
+
+## Responsible Disclosure
+
+We appreciate the efforts of security researchers and individuals who help us maintain the security of
+SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
+disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
+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.
+
+### Braintrust Terms and Policies
+
+Please contact info@braintrustdata.com for any questions or concerns regarding the security of our services.
+
+---
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
diff --git a/bin/check-release-environment b/bin/check-release-environment
old mode 100644
new mode 100755
index 63417e3d..3a6a7b4a
--- a/bin/check-release-environment
+++ b/bin/check-release-environment
@@ -3,24 +3,24 @@
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
-len=${#errors[@]}
+lenErrors=${#errors[@]}
-if [[ len -gt 0 ]]; then
+if [[ lenErrors -gt 0 ]]; then
echo -e "Found the following errors in the release environment:\n"
for error in "${errors[@]}"; do
diff --git a/braintrust-java-client-okhttp/build.gradle.kts b/braintrust-java-client-okhttp/build.gradle.kts
old mode 100644
new mode 100755
index 23aa41cc..16bbfd8e
--- a/braintrust-java-client-okhttp/build.gradle.kts
+++ b/braintrust-java-client-okhttp/build.gradle.kts
@@ -6,10 +6,10 @@ plugins {
dependencies {
api(project(":braintrust-java-core"))
- implementation("com.google.guava:guava:31.1-jre")
- implementation("com.squareup.okhttp3:okhttp:4.10.0")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
testImplementation(kotlin("test"))
- testImplementation("org.assertj:assertj-core:3.23.1")
- testImplementation("org.slf4j:slf4j-simple:1.7.29")
+ testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
}
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
old mode 100644
new mode 100755
index 16291f71..e91ce488
--- 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
@@ -5,37 +5,216 @@ package com.braintrustdata.api.client.okhttp
import com.braintrustdata.api.client.BraintrustClient
import com.braintrustdata.api.client.BraintrustClientImpl
import com.braintrustdata.api.core.ClientOptions
+import com.braintrustdata.api.core.Sleeper
+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 [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()
}
- class Builder {
+ /** A builder for [BraintrustOkHttpClient]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // default timeout for client is 1 minute
- private var timeout: Duration = Duration.ofSeconds(60)
private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ 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
+ }
+
+ /** 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
+ * 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 {
+ 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 interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
+
+ /**
+ * 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 {
clientOptions.headers(headers)
}
@@ -46,38 +225,99 @@ class BraintrustOkHttpClient private constructor() {
clientOptions.putHeaders(name, values)
}
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
fun putAllHeaders(headers: Map>) = apply {
clientOptions.putAllHeaders(headers)
}
- fun removeHeader(name: String) = apply { clientOptions.removeHeader(name) }
+ fun replaceHeaders(name: String, value: String) = apply {
+ clientOptions.replaceHeaders(name, value)
+ }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceHeaders(name, values)
+ }
- fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
- fun proxy(proxy: Proxy) = apply { this.proxy = proxy }
+ fun replaceAllHeaders(headers: Map>) = apply {
+ clientOptions.replaceAllHeaders(headers)
+ }
- fun responseValidation(responseValidation: Boolean) = apply {
- clientOptions.responseValidation(responseValidation)
+ fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+
+ fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
+ fun queryParams(queryParams: Map>) = apply {
+ clientOptions.queryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply {
+ clientOptions.putQueryParam(key, value)
+ }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.putQueryParams(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ clientOptions.replaceQueryParams(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.replaceQueryParams(key, values)
}
- fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
+ fun removeAllQueryParams(keys: Set) = apply {
+ clientOptions.removeAllQueryParams(keys)
+ }
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
fun fromEnv() = apply { clientOptions.fromEnv() }
- fun build(): BraintrustClient {
- return BraintrustClientImpl(
+ /**
+ * Returns an immutable instance of [BraintrustClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): BraintrustClient =
+ BraintrustClientImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(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
old mode 100644
new mode 100755
index 307fb329..ae462cb7
--- 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
@@ -5,37 +5,216 @@ package com.braintrustdata.api.client.okhttp
import com.braintrustdata.api.client.BraintrustClientAsync
import com.braintrustdata.api.client.BraintrustClientAsyncImpl
import com.braintrustdata.api.core.ClientOptions
+import com.braintrustdata.api.core.Sleeper
+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 [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()
}
- class Builder {
+ /** A builder for [BraintrustOkHttpClientAsync]. */
+ class Builder internal constructor() {
private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
- private var baseUrl: String = ClientOptions.PRODUCTION_URL
- // default timeout for client is 1 minute
- private var timeout: Duration = Duration.ofSeconds(60)
private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
- fun baseUrl(baseUrl: String) = apply {
- clientOptions.baseUrl(baseUrl)
- this.baseUrl = baseUrl
+ 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
+ }
+
+ /** 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
+ * 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 {
+ 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 interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { clientOptions.sleeper(sleeper) }
+
+ /**
+ * 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 {
clientOptions.headers(headers)
}
@@ -46,38 +225,99 @@ class BraintrustOkHttpClientAsync private constructor() {
clientOptions.putHeaders(name, values)
}
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
fun putAllHeaders(headers: Map>) = apply {
clientOptions.putAllHeaders(headers)
}
- fun removeHeader(name: String) = apply { clientOptions.removeHeader(name) }
+ fun replaceHeaders(name: String, value: String) = apply {
+ clientOptions.replaceHeaders(name, value)
+ }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceHeaders(name, values)
+ }
- fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
- fun proxy(proxy: Proxy) = apply { this.proxy = proxy }
+ fun replaceAllHeaders(headers: Map>) = apply {
+ clientOptions.replaceAllHeaders(headers)
+ }
- fun responseValidation(responseValidation: Boolean) = apply {
- clientOptions.responseValidation(responseValidation)
+ fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+
+ fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
+ fun queryParams(queryParams: Map>) = apply {
+ clientOptions.queryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply {
+ clientOptions.putQueryParam(key, value)
+ }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.putQueryParams(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ clientOptions.replaceQueryParams(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.replaceQueryParams(key, values)
}
- fun apiKey(apiKey: String) = apply { clientOptions.apiKey(apiKey) }
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
+ fun removeAllQueryParams(keys: Set) = apply {
+ clientOptions.removeAllQueryParams(keys)
+ }
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
fun fromEnv() = apply { clientOptions.fromEnv() }
- fun build(): BraintrustClientAsync {
- return BraintrustClientAsyncImpl(
+ /**
+ * Returns an immutable instance of [BraintrustClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): BraintrustClientAsync =
+ BraintrustClientAsyncImpl(
clientOptions
.httpClient(
OkHttpClient.builder()
- .baseUrl(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
old mode 100644
new mode 100755
index 45605ded..2ffdf3d7
--- 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
@@ -1,23 +1,25 @@
package com.braintrustdata.api.client.okhttp
import com.braintrustdata.api.core.RequestOptions
+import com.braintrustdata.api.core.Timeout
+import com.braintrustdata.api.core.http.Headers
import com.braintrustdata.api.core.http.HttpClient
import com.braintrustdata.api.core.http.HttpMethod
import com.braintrustdata.api.core.http.HttpRequest
import com.braintrustdata.api.core.http.HttpRequestBody
import com.braintrustdata.api.core.http.HttpResponse
import com.braintrustdata.api.errors.BraintrustIoException
-import com.google.common.collect.ListMultimap
-import com.google.common.collect.MultimapBuilder
import java.io.IOException
import java.io.InputStream
import java.net.Proxy
import java.time.Duration
+import java.util.concurrent.CancellationException
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.Headers
-import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@@ -25,28 +27,14 @@ import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
import okio.BufferedSink
class OkHttpClient
-private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val baseUrl: HttpUrl) :
- HttpClient {
-
- private fun getClient(requestOptions: RequestOptions): okhttp3.OkHttpClient {
- val timeout = requestOptions.timeout ?: return okHttpClient
- return okHttpClient
- .newBuilder()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
- .build()
- }
+private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClient) : HttpClient {
- override fun execute(
- request: HttpRequest,
- requestOptions: RequestOptions,
- ): HttpResponse {
- val call = getClient(requestOptions).newCall(request.toRequest())
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
return try {
call.execute().toResponse()
@@ -63,10 +51,7 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
): CompletableFuture {
val future = CompletableFuture()
- request.body?.run { future.whenComplete { _, _ -> close() } }
-
- val call = getClient(requestOptions).newCall(request.toRequest())
-
+ val call = newCall(request, requestOptions)
call.enqueue(
object : Callback {
override fun onResponse(call: Call, response: Response) {
@@ -79,6 +64,13 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
}
)
+ future.whenComplete { _, e ->
+ if (e is CancellationException) {
+ call.cancel()
+ }
+ request.body?.close()
+ }
+
return future
}
@@ -88,46 +80,93 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
okHttpClient.cache?.close()
}
- private fun HttpRequest.toRequest(): Request {
+ private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
+ val clientBuilder = okHttpClient.newBuilder()
+
+ val logLevel =
+ when (System.getenv("BRAINTRUST_LOG")?.lowercase()) {
+ "info" -> HttpLoggingInterceptor.Level.BASIC
+ "debug" -> HttpLoggingInterceptor.Level.BODY
+ else -> null
+ }
+ if (logLevel != null) {
+ clientBuilder.addNetworkInterceptor(
+ HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
+ )
+ }
+
+ requestOptions.timeout?.let {
+ clientBuilder
+ .connectTimeout(it.connect())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
+ }
+
+ val client = clientBuilder.build()
+ return client.newCall(request.toRequest(client))
+ }
+
+ private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
var body: RequestBody? = body?.toRequestBody()
- // OkHttpClient always requires a request body for PUT and POST methods
- if (body == null && (method == HttpMethod.PUT || method == HttpMethod.POST)) {
+ if (body == null && requiresBody(method)) {
body = "".toRequestBody()
}
val builder = Request.Builder().url(toUrl()).method(method.name, body)
- headers.forEach(builder::header)
+ headers.names().forEach { name ->
+ headers.values(name).forEach { builder.addHeader(name, it) }
+ }
+
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.addHeader(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.addHeader(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
return builder.build()
}
- private fun HttpRequest.toUrl(): String {
- url?.let {
- return it
+ /** `OkHttpClient` always requires a request body for some methods. */
+ private fun requiresBody(method: HttpMethod): Boolean =
+ when (method) {
+ HttpMethod.POST,
+ HttpMethod.PUT,
+ HttpMethod.PATCH -> true
+ else -> false
}
- val builder = baseUrl.newBuilder()
+ private fun HttpRequest.toUrl(): String {
+ val builder = baseUrl.toHttpUrl().newBuilder()
pathSegments.forEach(builder::addPathSegment)
- queryParams.forEach(builder::addQueryParameter)
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
+ }
return builder.toString()
}
private fun HttpRequestBody.toRequestBody(): RequestBody {
val mediaType = contentType()?.toMediaType()
+ val length = contentLength()
return object : RequestBody() {
- override fun contentType(): MediaType? {
- return mediaType
- }
+ override fun contentType(): MediaType? = mediaType
- override fun isOneShot(): Boolean {
- return !repeatable()
- }
+ override fun contentLength(): Long = length
- override fun writeTo(sink: BufferedSink) {
- writeTo(sink.outputStream())
- }
+ override fun isOneShot(): Boolean = !repeatable()
+
+ override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
}
}
@@ -135,63 +174,79 @@ private constructor(private val okHttpClient: okhttp3.OkHttpClient, private val
val headers = headers.toHeaders()
return object : HttpResponse {
- override fun statusCode(): Int {
- return code
- }
+ override fun statusCode(): Int = code
- override fun headers(): ListMultimap {
- return headers
- }
+ override fun headers(): Headers = headers
- override fun body(): InputStream {
- return body!!.byteStream()
- }
+ override fun body(): InputStream = body!!.byteStream()
- override fun close() {
- body!!.close()
- }
+ override fun close() = body!!.close()
}
}
- private fun Headers.toHeaders(): ListMultimap {
- val headers =
- MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
- .arrayListValues()
- .build()
-
- forEach { pair -> headers.put(pair.first, pair.second) }
-
- return headers
+ private fun okhttp3.Headers.toHeaders(): Headers {
+ val headersBuilder = Headers.builder()
+ forEach { (name, value) -> headersBuilder.put(name, value) }
+ return headersBuilder.build()
}
companion object {
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ class Builder internal constructor() {
- private var baseUrl: HttpUrl? = null
- // default timeout is 1 minute
- private var timeout: Duration = Duration.ofSeconds(60)
+ 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 baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl.toHttpUrl() }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
- fun build(): OkHttpClient {
- return OkHttpClient(
+ 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()
- .connectTimeout(timeout)
- .readTimeout(timeout)
- .writeTimeout(timeout)
- .callTimeout(if (timeout.seconds == 0L) timeout else timeout.plusSeconds(30))
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
.proxy(proxy)
- .build(),
- checkNotNull(baseUrl) { "`baseUrl` is required but was not set" },
+ .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-client-okhttp/src/test/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClientTest.kt b/braintrust-java-client-okhttp/src/test/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClientTest.kt
new file mode 100644
index 00000000..ed9f0458
--- /dev/null
+++ b/braintrust-java-client-okhttp/src/test/kotlin/com/braintrustdata/api/client/okhttp/OkHttpClientTest.kt
@@ -0,0 +1,44 @@
+package com.braintrustdata.api.client.okhttp
+
+import com.braintrustdata.api.core.http.HttpMethod
+import com.braintrustdata.api.core.http.HttpRequest
+import com.github.tomakehurst.wiremock.client.WireMock.*
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo
+import com.github.tomakehurst.wiremock.junit5.WireMockTest
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.parallel.ResourceLock
+
+@WireMockTest
+@ResourceLock("https://github.com/wiremock/wiremock/issues/169")
+internal class OkHttpClientTest {
+
+ private lateinit var baseUrl: String
+ private lateinit var httpClient: OkHttpClient
+
+ @BeforeEach
+ fun beforeEach(wmRuntimeInfo: WireMockRuntimeInfo) {
+ baseUrl = wmRuntimeInfo.httpBaseUrl
+ httpClient = OkHttpClient.builder().build()
+ }
+
+ @Test
+ fun executeAsync_whenFutureCancelled_cancelsUnderlyingCall() {
+ stubFor(post(urlPathEqualTo("/something")).willReturn(ok()))
+ val responseFuture =
+ httpClient.executeAsync(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl(baseUrl)
+ .addPathSegment("something")
+ .build()
+ )
+ val call = httpClient.okHttpClient.dispatcher.runningCalls().single()
+
+ responseFuture.cancel(false)
+
+ // Should have cancelled the underlying call
+ assertThat(call.isCanceled()).isTrue()
+ }
+}
diff --git a/braintrust-java-core/build.gradle.kts b/braintrust-java-core/build.gradle.kts
old mode 100644
new mode 100755
index 9e92de7e..0e09ef31
--- a/braintrust-java-core/build.gradle.kts
+++ b/braintrust-java-core/build.gradle.kts
@@ -3,24 +3,39 @@ plugins {
id("braintrust.publish")
}
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it.
+ // We publish with a higher version (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ }
+}
+
dependencies {
- api("com.fasterxml.jackson.core:jackson-core:2.14.1")
- api("com.fasterxml.jackson.core:jackson-databind:2.14.1")
- api("com.google.guava:guava:31.1-jre")
+ api("com.fasterxml.jackson.core:jackson-core:2.18.2")
+ api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
- implementation("com.fasterxml.jackson.core:jackson-annotations:2.14.1")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.1")
- implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.1")
- implementation("org.apache.httpcomponents.core5:httpcore5:5.2.1")
- implementation("org.apache.httpcomponents.client5:httpclient5:5.2.1")
+ implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
+ implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
+ implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
testImplementation(kotlin("test"))
testImplementation(project(":braintrust-java-client-okhttp"))
- testImplementation("com.github.tomakehurst:wiremock-jre8:2.33.2")
- testImplementation("org.assertj:assertj-core:3.23.1")
- testImplementation("org.assertj:assertj-guava:3.5.0")
- testImplementation("org.slf4j:slf4j-simple:1.7.29")
- testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.1")
- testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.1")
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
+ testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
+ testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
+ testImplementation("org.mockito:mockito-core:5.14.2")
+ testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
+ testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
}
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
old mode 100644
new mode 100755
index 22359153..7881923e
--- 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
@@ -1,21 +1,162 @@
// File generated from our OpenAPI spec by Stainless.
-@file:Suppress("OVERLOADS_INTERFACE") // See https://youtrack.jetbrains.com/issue/KT-36102
-
package com.braintrustdata.api.client
-import com.braintrustdata.api.models.*
-import com.braintrustdata.api.services.blocking.*
+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
+import com.braintrustdata.api.services.blocking.DatasetService
+import com.braintrustdata.api.services.blocking.EnvVarService
+import com.braintrustdata.api.services.blocking.EvalService
+import com.braintrustdata.api.services.blocking.ExperimentService
+import com.braintrustdata.api.services.blocking.FunctionService
+import com.braintrustdata.api.services.blocking.GroupService
+import com.braintrustdata.api.services.blocking.OrganizationService
+import com.braintrustdata.api.services.blocking.ProjectScoreService
+import com.braintrustdata.api.services.blocking.ProjectService
+import com.braintrustdata.api.services.blocking.ProjectTagService
+import com.braintrustdata.api.services.blocking.PromptService
+import com.braintrustdata.api.services.blocking.RoleService
+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
+ * asynchronous execution via the [async] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface BraintrustClient {
+ /**
+ * Returns a version of this client that uses asynchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun async(): BraintrustClientAsync
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ 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 project(): ProjectService
+ fun projects(): ProjectService
+
+ fun experiments(): ExperimentService
+
+ fun datasets(): DatasetService
+
+ fun prompts(): PromptService
+
+ fun roles(): RoleService
+
+ fun groups(): GroupService
+
+ fun acls(): AclService
+
+ fun users(): UserService
+
+ fun projectScores(): ProjectScoreService
+
+ fun projectTags(): ProjectTagService
+
+ fun spanIframes(): SpanIframeService
+
+ fun functions(): FunctionService
+
+ fun views(): ViewService
+
+ fun organizations(): OrganizationService
+
+ fun apiKeys(): ApiKeyService
+
+ fun aiSecrets(): AiSecretService
+
+ fun envVars(): EnvVarService
+
+ fun evals(): EvalService
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is 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 HTTP 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()
+
+ /** 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
+
+ fun experiments(): ExperimentService.WithRawResponse
+
+ fun datasets(): DatasetService.WithRawResponse
+
+ fun prompts(): PromptService.WithRawResponse
+
+ fun roles(): RoleService.WithRawResponse
+
+ fun groups(): GroupService.WithRawResponse
+
+ fun acls(): AclService.WithRawResponse
+
+ fun users(): UserService.WithRawResponse
+
+ fun projectScores(): ProjectScoreService.WithRawResponse
+
+ fun projectTags(): ProjectTagService.WithRawResponse
+
+ fun spanIframes(): SpanIframeService.WithRawResponse
+
+ fun functions(): FunctionService.WithRawResponse
+
+ fun views(): ViewService.WithRawResponse
+
+ fun organizations(): OrganizationService.WithRawResponse
+
+ fun apiKeys(): ApiKeyService.WithRawResponse
+
+ fun aiSecrets(): AiSecretService.WithRawResponse
- fun experiment(): ExperimentService
+ fun envVars(): EnvVarService.WithRawResponse
- fun dataset(): DatasetService
+ fun evals(): EvalService.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
old mode 100644
new mode 100755
index 726e3a61..c6edb032
--- 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
@@ -1,21 +1,166 @@
// File generated from our OpenAPI spec by Stainless.
-@file:Suppress("OVERLOADS_INTERFACE") // See https://youtrack.jetbrains.com/issue/KT-36102
-
package com.braintrustdata.api.client
-import com.braintrustdata.api.models.*
-import com.braintrustdata.api.services.async.*
+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
+import com.braintrustdata.api.services.async.DatasetServiceAsync
+import com.braintrustdata.api.services.async.EnvVarServiceAsync
+import com.braintrustdata.api.services.async.EvalServiceAsync
+import com.braintrustdata.api.services.async.ExperimentServiceAsync
+import com.braintrustdata.api.services.async.FunctionServiceAsync
+import com.braintrustdata.api.services.async.GroupServiceAsync
+import com.braintrustdata.api.services.async.OrganizationServiceAsync
+import com.braintrustdata.api.services.async.ProjectScoreServiceAsync
+import com.braintrustdata.api.services.async.ProjectServiceAsync
+import com.braintrustdata.api.services.async.ProjectTagServiceAsync
+import com.braintrustdata.api.services.async.PromptServiceAsync
+import com.braintrustdata.api.services.async.RoleServiceAsync
+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
+ * synchronous execution via the [sync] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
interface BraintrustClientAsync {
+ /**
+ * Returns a version of this client that uses synchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
fun sync(): BraintrustClient
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ 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 project(): ProjectServiceAsync
+ fun projects(): ProjectServiceAsync
+
+ fun experiments(): ExperimentServiceAsync
+
+ fun datasets(): DatasetServiceAsync
+
+ fun prompts(): PromptServiceAsync
+
+ fun roles(): RoleServiceAsync
+
+ fun groups(): GroupServiceAsync
+
+ fun acls(): AclServiceAsync
+
+ fun users(): UserServiceAsync
+
+ fun projectScores(): ProjectScoreServiceAsync
+
+ fun projectTags(): ProjectTagServiceAsync
+
+ fun spanIframes(): SpanIframeServiceAsync
+
+ fun functions(): FunctionServiceAsync
+
+ fun views(): ViewServiceAsync
+
+ fun organizations(): OrganizationServiceAsync
+
+ fun apiKeys(): ApiKeyServiceAsync
+
+ fun aiSecrets(): AiSecretServiceAsync
+
+ fun envVars(): EnvVarServiceAsync
+
+ fun evals(): EvalServiceAsync
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is 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 HTTP 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()
+
+ /**
+ * A view of [BraintrustClientAsync] 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
+ ): BraintrustClientAsync.WithRawResponse
+
+ fun topLevel(): TopLevelServiceAsync.WithRawResponse
+
+ fun projects(): ProjectServiceAsync.WithRawResponse
+
+ fun experiments(): ExperimentServiceAsync.WithRawResponse
+
+ fun datasets(): DatasetServiceAsync.WithRawResponse
+
+ fun prompts(): PromptServiceAsync.WithRawResponse
+
+ fun roles(): RoleServiceAsync.WithRawResponse
+
+ fun groups(): GroupServiceAsync.WithRawResponse
+
+ fun acls(): AclServiceAsync.WithRawResponse
+
+ fun users(): UserServiceAsync.WithRawResponse
+
+ fun projectScores(): ProjectScoreServiceAsync.WithRawResponse
+
+ fun projectTags(): ProjectTagServiceAsync.WithRawResponse
+
+ fun spanIframes(): SpanIframeServiceAsync.WithRawResponse
+
+ fun functions(): FunctionServiceAsync.WithRawResponse
+
+ fun views(): ViewServiceAsync.WithRawResponse
+
+ fun organizations(): OrganizationServiceAsync.WithRawResponse
+
+ fun apiKeys(): ApiKeyServiceAsync.WithRawResponse
+
+ fun aiSecrets(): AiSecretServiceAsync.WithRawResponse
- fun experiment(): ExperimentServiceAsync
+ fun envVars(): EnvVarServiceAsync.WithRawResponse
- fun dataset(): DatasetServiceAsync
+ fun evals(): EvalServiceAsync.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
old mode 100644
new mode 100755
index d2d9f655..2d94732b
--- 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
@@ -3,38 +3,299 @@
package com.braintrustdata.api.client
import com.braintrustdata.api.core.ClientOptions
-import com.braintrustdata.api.core.http.HttpResponse.Handler
-import com.braintrustdata.api.errors.BraintrustError
-import com.braintrustdata.api.models.*
-import com.braintrustdata.api.services.async.*
-import com.braintrustdata.api.services.errorHandler
+import com.braintrustdata.api.core.getPackageVersion
+import com.braintrustdata.api.services.async.AclServiceAsync
+import com.braintrustdata.api.services.async.AclServiceAsyncImpl
+import com.braintrustdata.api.services.async.AiSecretServiceAsync
+import com.braintrustdata.api.services.async.AiSecretServiceAsyncImpl
+import com.braintrustdata.api.services.async.ApiKeyServiceAsync
+import com.braintrustdata.api.services.async.ApiKeyServiceAsyncImpl
+import com.braintrustdata.api.services.async.DatasetServiceAsync
+import com.braintrustdata.api.services.async.DatasetServiceAsyncImpl
+import com.braintrustdata.api.services.async.EnvVarServiceAsync
+import com.braintrustdata.api.services.async.EnvVarServiceAsyncImpl
+import com.braintrustdata.api.services.async.EvalServiceAsync
+import com.braintrustdata.api.services.async.EvalServiceAsyncImpl
+import com.braintrustdata.api.services.async.ExperimentServiceAsync
+import com.braintrustdata.api.services.async.ExperimentServiceAsyncImpl
+import com.braintrustdata.api.services.async.FunctionServiceAsync
+import com.braintrustdata.api.services.async.FunctionServiceAsyncImpl
+import com.braintrustdata.api.services.async.GroupServiceAsync
+import com.braintrustdata.api.services.async.GroupServiceAsyncImpl
+import com.braintrustdata.api.services.async.OrganizationServiceAsync
+import com.braintrustdata.api.services.async.OrganizationServiceAsyncImpl
+import com.braintrustdata.api.services.async.ProjectScoreServiceAsync
+import com.braintrustdata.api.services.async.ProjectScoreServiceAsyncImpl
+import com.braintrustdata.api.services.async.ProjectServiceAsync
+import com.braintrustdata.api.services.async.ProjectServiceAsyncImpl
+import com.braintrustdata.api.services.async.ProjectTagServiceAsync
+import com.braintrustdata.api.services.async.ProjectTagServiceAsyncImpl
+import com.braintrustdata.api.services.async.PromptServiceAsync
+import com.braintrustdata.api.services.async.PromptServiceAsyncImpl
+import com.braintrustdata.api.services.async.RoleServiceAsync
+import com.braintrustdata.api.services.async.RoleServiceAsyncImpl
+import com.braintrustdata.api.services.async.SpanIframeServiceAsync
+import com.braintrustdata.api.services.async.SpanIframeServiceAsyncImpl
+import com.braintrustdata.api.services.async.TopLevelServiceAsync
+import com.braintrustdata.api.services.async.TopLevelServiceAsyncImpl
+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
-constructor(
- private val clientOptions: ClientOptions,
-) : BraintrustClientAsync {
+class BraintrustClientAsyncImpl(private val clientOptions: ClientOptions) : BraintrustClientAsync {
- private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
+ private val clientOptionsWithUserAgent =
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
+ else
+ clientOptions
+ .toBuilder()
+ .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
+ .build()
+ // Pass the original clientOptions so that this client sets its own User-Agent.
private val sync: BraintrustClient by lazy { BraintrustClientImpl(clientOptions) }
- private val topLevel: TopLevelServiceAsync by lazy { TopLevelServiceAsyncImpl(clientOptions) }
+ private val withRawResponse: BraintrustClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val topLevel: TopLevelServiceAsync by lazy {
+ TopLevelServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val projects: ProjectServiceAsync by lazy {
+ ProjectServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val experiments: ExperimentServiceAsync by lazy {
+ ExperimentServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val datasets: DatasetServiceAsync by lazy {
+ DatasetServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
- private val project: ProjectServiceAsync by lazy { ProjectServiceAsyncImpl(clientOptions) }
+ private val prompts: PromptServiceAsync by lazy {
+ PromptServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val roles: RoleServiceAsync by lazy { RoleServiceAsyncImpl(clientOptionsWithUserAgent) }
- private val experiment: ExperimentServiceAsync by lazy {
- ExperimentServiceAsyncImpl(clientOptions)
+ private val groups: GroupServiceAsync by lazy {
+ GroupServiceAsyncImpl(clientOptionsWithUserAgent)
}
- private val dataset: DatasetServiceAsync by lazy { DatasetServiceAsyncImpl(clientOptions) }
+ private val acls: AclServiceAsync by lazy { AclServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val users: UserServiceAsync by lazy { UserServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val projectScores: ProjectScoreServiceAsync by lazy {
+ ProjectScoreServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val projectTags: ProjectTagServiceAsync by lazy {
+ ProjectTagServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val spanIframes: SpanIframeServiceAsync by lazy {
+ SpanIframeServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val functions: FunctionServiceAsync by lazy {
+ FunctionServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val views: ViewServiceAsync by lazy { ViewServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val organizations: OrganizationServiceAsync by lazy {
+ OrganizationServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val apiKeys: ApiKeyServiceAsync by lazy {
+ ApiKeyServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val aiSecrets: AiSecretServiceAsync by lazy {
+ AiSecretServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val envVars: EnvVarServiceAsync by lazy {
+ EnvVarServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val evals: EvalServiceAsync by lazy { EvalServiceAsyncImpl(clientOptionsWithUserAgent) }
override fun sync(): BraintrustClient = sync
+ 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 project(): ProjectServiceAsync = project
+ override fun projects(): ProjectServiceAsync = projects
+
+ override fun experiments(): ExperimentServiceAsync = experiments
+
+ override fun datasets(): DatasetServiceAsync = datasets
+
+ override fun prompts(): PromptServiceAsync = prompts
+
+ override fun roles(): RoleServiceAsync = roles
+
+ override fun groups(): GroupServiceAsync = groups
+
+ override fun acls(): AclServiceAsync = acls
+
+ override fun users(): UserServiceAsync = users
+
+ override fun projectScores(): ProjectScoreServiceAsync = projectScores
+
+ override fun projectTags(): ProjectTagServiceAsync = projectTags
+
+ override fun spanIframes(): SpanIframeServiceAsync = spanIframes
+
+ override fun functions(): FunctionServiceAsync = functions
+
+ override fun views(): ViewServiceAsync = views
+
+ override fun organizations(): OrganizationServiceAsync = organizations
- override fun experiment(): ExperimentServiceAsync = experiment
+ override fun apiKeys(): ApiKeyServiceAsync = apiKeys
- override fun dataset(): DatasetServiceAsync = dataset
+ override fun aiSecrets(): AiSecretServiceAsync = aiSecrets
+
+ override fun envVars(): EnvVarServiceAsync = envVars
+
+ override fun evals(): EvalServiceAsync = evals
+
+ override fun close() = clientOptions.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ BraintrustClientAsync.WithRawResponse {
+
+ private val topLevel: TopLevelServiceAsync.WithRawResponse by lazy {
+ TopLevelServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val projects: ProjectServiceAsync.WithRawResponse by lazy {
+ ProjectServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val experiments: ExperimentServiceAsync.WithRawResponse by lazy {
+ ExperimentServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val datasets: DatasetServiceAsync.WithRawResponse by lazy {
+ DatasetServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val prompts: PromptServiceAsync.WithRawResponse by lazy {
+ PromptServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val roles: RoleServiceAsync.WithRawResponse by lazy {
+ RoleServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val groups: GroupServiceAsync.WithRawResponse by lazy {
+ GroupServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val acls: AclServiceAsync.WithRawResponse by lazy {
+ AclServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val users: UserServiceAsync.WithRawResponse by lazy {
+ UserServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val projectScores: ProjectScoreServiceAsync.WithRawResponse by lazy {
+ ProjectScoreServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val projectTags: ProjectTagServiceAsync.WithRawResponse by lazy {
+ ProjectTagServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val spanIframes: SpanIframeServiceAsync.WithRawResponse by lazy {
+ SpanIframeServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val functions: FunctionServiceAsync.WithRawResponse by lazy {
+ FunctionServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val views: ViewServiceAsync.WithRawResponse by lazy {
+ ViewServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val organizations: OrganizationServiceAsync.WithRawResponse by lazy {
+ OrganizationServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val apiKeys: ApiKeyServiceAsync.WithRawResponse by lazy {
+ ApiKeyServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val aiSecrets: AiSecretServiceAsync.WithRawResponse by lazy {
+ AiSecretServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val envVars: EnvVarServiceAsync.WithRawResponse by lazy {
+ EnvVarServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val evals: EvalServiceAsync.WithRawResponse by lazy {
+ 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
+
+ override fun experiments(): ExperimentServiceAsync.WithRawResponse = experiments
+
+ override fun datasets(): DatasetServiceAsync.WithRawResponse = datasets
+
+ override fun prompts(): PromptServiceAsync.WithRawResponse = prompts
+
+ override fun roles(): RoleServiceAsync.WithRawResponse = roles
+
+ override fun groups(): GroupServiceAsync.WithRawResponse = groups
+
+ override fun acls(): AclServiceAsync.WithRawResponse = acls
+
+ override fun users(): UserServiceAsync.WithRawResponse = users
+
+ override fun projectScores(): ProjectScoreServiceAsync.WithRawResponse = projectScores
+
+ override fun projectTags(): ProjectTagServiceAsync.WithRawResponse = projectTags
+
+ override fun spanIframes(): SpanIframeServiceAsync.WithRawResponse = spanIframes
+
+ override fun functions(): FunctionServiceAsync.WithRawResponse = functions
+
+ override fun views(): ViewServiceAsync.WithRawResponse = views
+
+ override fun organizations(): OrganizationServiceAsync.WithRawResponse = organizations
+
+ override fun apiKeys(): ApiKeyServiceAsync.WithRawResponse = apiKeys
+
+ override fun aiSecrets(): AiSecretServiceAsync.WithRawResponse = aiSecrets
+
+ override fun envVars(): EnvVarServiceAsync.WithRawResponse = envVars
+
+ override fun evals(): EvalServiceAsync.WithRawResponse = evals
+ }
}
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
old mode 100644
new mode 100755
index aa93668c..c205ae48
--- 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
@@ -3,36 +3,287 @@
package com.braintrustdata.api.client
import com.braintrustdata.api.core.ClientOptions
-import com.braintrustdata.api.core.http.HttpResponse.Handler
-import com.braintrustdata.api.errors.BraintrustError
-import com.braintrustdata.api.models.*
-import com.braintrustdata.api.services.blocking.*
-import com.braintrustdata.api.services.errorHandler
+import com.braintrustdata.api.core.getPackageVersion
+import com.braintrustdata.api.services.blocking.AclService
+import com.braintrustdata.api.services.blocking.AclServiceImpl
+import com.braintrustdata.api.services.blocking.AiSecretService
+import com.braintrustdata.api.services.blocking.AiSecretServiceImpl
+import com.braintrustdata.api.services.blocking.ApiKeyService
+import com.braintrustdata.api.services.blocking.ApiKeyServiceImpl
+import com.braintrustdata.api.services.blocking.DatasetService
+import com.braintrustdata.api.services.blocking.DatasetServiceImpl
+import com.braintrustdata.api.services.blocking.EnvVarService
+import com.braintrustdata.api.services.blocking.EnvVarServiceImpl
+import com.braintrustdata.api.services.blocking.EvalService
+import com.braintrustdata.api.services.blocking.EvalServiceImpl
+import com.braintrustdata.api.services.blocking.ExperimentService
+import com.braintrustdata.api.services.blocking.ExperimentServiceImpl
+import com.braintrustdata.api.services.blocking.FunctionService
+import com.braintrustdata.api.services.blocking.FunctionServiceImpl
+import com.braintrustdata.api.services.blocking.GroupService
+import com.braintrustdata.api.services.blocking.GroupServiceImpl
+import com.braintrustdata.api.services.blocking.OrganizationService
+import com.braintrustdata.api.services.blocking.OrganizationServiceImpl
+import com.braintrustdata.api.services.blocking.ProjectScoreService
+import com.braintrustdata.api.services.blocking.ProjectScoreServiceImpl
+import com.braintrustdata.api.services.blocking.ProjectService
+import com.braintrustdata.api.services.blocking.ProjectServiceImpl
+import com.braintrustdata.api.services.blocking.ProjectTagService
+import com.braintrustdata.api.services.blocking.ProjectTagServiceImpl
+import com.braintrustdata.api.services.blocking.PromptService
+import com.braintrustdata.api.services.blocking.PromptServiceImpl
+import com.braintrustdata.api.services.blocking.RoleService
+import com.braintrustdata.api.services.blocking.RoleServiceImpl
+import com.braintrustdata.api.services.blocking.SpanIframeService
+import com.braintrustdata.api.services.blocking.SpanIframeServiceImpl
+import com.braintrustdata.api.services.blocking.TopLevelService
+import com.braintrustdata.api.services.blocking.TopLevelServiceImpl
+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
-constructor(
- private val clientOptions: ClientOptions,
-) : BraintrustClient {
+class BraintrustClientImpl(private val clientOptions: ClientOptions) : BraintrustClient {
- private val errorHandler: Handler = errorHandler(clientOptions.jsonMapper)
+ private val clientOptionsWithUserAgent =
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
+ else
+ clientOptions
+ .toBuilder()
+ .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
+ .build()
+ // Pass the original clientOptions so that this client sets its own User-Agent.
private val async: BraintrustClientAsync by lazy { BraintrustClientAsyncImpl(clientOptions) }
- private val topLevel: TopLevelService by lazy { TopLevelServiceImpl(clientOptions) }
+ private val withRawResponse: BraintrustClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
- private val project: ProjectService by lazy { ProjectServiceImpl(clientOptions) }
+ private val topLevel: TopLevelService by lazy {
+ TopLevelServiceImpl(clientOptionsWithUserAgent)
+ }
- private val experiment: ExperimentService by lazy { ExperimentServiceImpl(clientOptions) }
+ private val projects: ProjectService by lazy { ProjectServiceImpl(clientOptionsWithUserAgent) }
- private val dataset: DatasetService by lazy { DatasetServiceImpl(clientOptions) }
+ private val experiments: ExperimentService by lazy {
+ ExperimentServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val datasets: DatasetService by lazy { DatasetServiceImpl(clientOptionsWithUserAgent) }
+
+ private val prompts: PromptService by lazy { PromptServiceImpl(clientOptionsWithUserAgent) }
+
+ private val roles: RoleService by lazy { RoleServiceImpl(clientOptionsWithUserAgent) }
+
+ private val groups: GroupService by lazy { GroupServiceImpl(clientOptionsWithUserAgent) }
+
+ private val acls: AclService by lazy { AclServiceImpl(clientOptionsWithUserAgent) }
+
+ private val users: UserService by lazy { UserServiceImpl(clientOptionsWithUserAgent) }
+
+ private val projectScores: ProjectScoreService by lazy {
+ ProjectScoreServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val projectTags: ProjectTagService by lazy {
+ ProjectTagServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val spanIframes: SpanIframeService by lazy {
+ SpanIframeServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val functions: FunctionService by lazy {
+ FunctionServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val views: ViewService by lazy { ViewServiceImpl(clientOptionsWithUserAgent) }
+
+ private val organizations: OrganizationService by lazy {
+ OrganizationServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val apiKeys: ApiKeyService by lazy { ApiKeyServiceImpl(clientOptionsWithUserAgent) }
+
+ private val aiSecrets: AiSecretService by lazy {
+ AiSecretServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val envVars: EnvVarService by lazy { EnvVarServiceImpl(clientOptionsWithUserAgent) }
+
+ private val evals: EvalService by lazy { EvalServiceImpl(clientOptionsWithUserAgent) }
override fun async(): BraintrustClientAsync = async
+ 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 project(): ProjectService = project
+ override fun projects(): ProjectService = projects
+
+ override fun experiments(): ExperimentService = experiments
+
+ override fun datasets(): DatasetService = datasets
+
+ override fun prompts(): PromptService = prompts
+
+ override fun roles(): RoleService = roles
+
+ override fun groups(): GroupService = groups
+
+ override fun acls(): AclService = acls
+
+ override fun users(): UserService = users
+
+ override fun projectScores(): ProjectScoreService = projectScores
+
+ override fun projectTags(): ProjectTagService = projectTags
+
+ override fun spanIframes(): SpanIframeService = spanIframes
+
+ override fun functions(): FunctionService = functions
+
+ override fun views(): ViewService = views
+
+ override fun organizations(): OrganizationService = organizations
+
+ override fun apiKeys(): ApiKeyService = apiKeys
+
+ override fun aiSecrets(): AiSecretService = aiSecrets
+
+ override fun envVars(): EnvVarService = envVars
+
+ override fun evals(): EvalService = evals
+
+ override fun close() = clientOptions.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ BraintrustClient.WithRawResponse {
+
+ private val topLevel: TopLevelService.WithRawResponse by lazy {
+ TopLevelServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val projects: ProjectService.WithRawResponse by lazy {
+ ProjectServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val experiments: ExperimentService.WithRawResponse by lazy {
+ ExperimentServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val datasets: DatasetService.WithRawResponse by lazy {
+ DatasetServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val prompts: PromptService.WithRawResponse by lazy {
+ PromptServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val roles: RoleService.WithRawResponse by lazy {
+ RoleServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val groups: GroupService.WithRawResponse by lazy {
+ GroupServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val acls: AclService.WithRawResponse by lazy {
+ AclServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val users: UserService.WithRawResponse by lazy {
+ UserServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val projectScores: ProjectScoreService.WithRawResponse by lazy {
+ ProjectScoreServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val projectTags: ProjectTagService.WithRawResponse by lazy {
+ ProjectTagServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val spanIframes: SpanIframeService.WithRawResponse by lazy {
+ SpanIframeServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val functions: FunctionService.WithRawResponse by lazy {
+ FunctionServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val views: ViewService.WithRawResponse by lazy {
+ ViewServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val organizations: OrganizationService.WithRawResponse by lazy {
+ OrganizationServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val apiKeys: ApiKeyService.WithRawResponse by lazy {
+ ApiKeyServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val aiSecrets: AiSecretService.WithRawResponse by lazy {
+ AiSecretServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val envVars: EnvVarService.WithRawResponse by lazy {
+ EnvVarServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val evals: EvalService.WithRawResponse by lazy {
+ 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
+
+ override fun experiments(): ExperimentService.WithRawResponse = experiments
+
+ override fun datasets(): DatasetService.WithRawResponse = datasets
+
+ override fun prompts(): PromptService.WithRawResponse = prompts
+
+ override fun roles(): RoleService.WithRawResponse = roles
+
+ override fun groups(): GroupService.WithRawResponse = groups
+
+ override fun acls(): AclService.WithRawResponse = acls
+
+ override fun users(): UserService.WithRawResponse = users
+
+ override fun projectScores(): ProjectScoreService.WithRawResponse = projectScores
+
+ override fun projectTags(): ProjectTagService.WithRawResponse = projectTags
+
+ override fun spanIframes(): SpanIframeService.WithRawResponse = spanIframes
+
+ override fun functions(): FunctionService.WithRawResponse = functions
+
+ override fun views(): ViewService.WithRawResponse = views
+
+ override fun organizations(): OrganizationService.WithRawResponse = organizations
+
+ override fun apiKeys(): ApiKeyService.WithRawResponse = apiKeys
+
+ override fun aiSecrets(): AiSecretService.WithRawResponse = aiSecrets
- override fun experiment(): ExperimentService = experiment
+ override fun envVars(): EnvVarService.WithRawResponse = envVars
- override fun dataset(): DatasetService = dataset
+ override fun evals(): EvalService.WithRawResponse = evals
+ }
}
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/BaseDeserializer.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/BaseDeserializer.kt
old mode 100644
new mode 100755
index a38e39bd..3ebb507f
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/BaseDeserializer.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/BaseDeserializer.kt
@@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.BeanProperty
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.JsonDeserializer
-import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
@@ -18,7 +17,7 @@ abstract class BaseDeserializer(type: KClass) :
override fun createContextual(
context: DeserializationContext,
- property: BeanProperty?
+ property: BeanProperty?,
): JsonDeserializer {
return this
}
@@ -29,31 +28,17 @@ abstract class BaseDeserializer(type: KClass) :
protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: TypeReference,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
- protected fun ObjectCodec.tryDeserialize(
- node: JsonNode,
- type: JavaType,
- validate: (T) -> Unit = {}
- ): T? {
- return try {
- readValue(treeAsTokens(node), type).apply(validate)
- } catch (e: JsonMappingException) {
- null
- } catch (e: RuntimeException) {
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
null
}
- }
}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/BaseSerializer.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/BaseSerializer.kt
old mode 100644
new mode 100755
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
new file mode 100644
index 00000000..254bc88e
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Check.kt
@@ -0,0 +1,96 @@
+@file:JvmName("Check")
+
+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" }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkLength(name: String, value: String, length: Int): String =
+ value.also {
+ check(it.length == length) { "`$name` must have length $length, but was ${it.length}" }
+ }
+
+@JvmSynthetic
+internal fun checkMinLength(name: String, value: String, minLength: Int): String =
+ value.also {
+ check(it.length >= minLength) {
+ if (minLength == 1) "`$name` must be non-empty, but was empty"
+ else "`$name` must have at least length $minLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkMaxLength(name: String, value: String, maxLength: Int): String =
+ value.also {
+ check(it.length <= maxLength) {
+ "`$name` must have at most length $maxLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()]
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ badVersionReason != null -> it to badVersionReason
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK requires a minimum Jackson version of $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+
+See https://www.github.com/braintrustdata/braintrust-java#jackson for more information.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val BAD_JACKSON_VERSIONS: Map =
+ mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639")
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
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
old mode 100644
new mode 100755
index 3327c070..e77958a6
--- 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,110 +2,500 @@
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
+import com.braintrustdata.api.core.http.QueryParams
import com.braintrustdata.api.core.http.RetryingHttpClient
import com.fasterxml.jackson.databind.json.JsonMapper
-import com.google.common.collect.ArrayListMultimap
-import com.google.common.collect.ListMultimap
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 interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ @get:JvmName("sleeper") val sleeper: Sleeper,
+ /**
+ * 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,
- @get:JvmName("apiKey") val apiKey: String,
- @get:JvmName("headers") val headers: ListMultimap,
+ 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?,
) {
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ /**
+ * 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)
+
companion object {
- const val PRODUCTION_URL = "https://api.braintrustdata.com"
+ const val PRODUCTION_URL = "https://api.braintrust.dev"
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ */
@JvmStatic fun builder() = Builder()
+ /**
+ * Returns options configured using system properties and environment variables.
+ *
+ * @see Builder.fromEnv
+ */
@JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
}
- class Builder {
+ /** A builder for [ClientOptions]. */
+ class Builder internal constructor() {
private var httpClient: HttpClient? = null
- private var jsonMapper: JsonMapper? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
+ private var jsonMapper: JsonMapper = jsonMapper()
+ private var streamHandlerExecutor: Executor? = null
+ private var sleeper: Sleeper? = null
private var clock: Clock = Clock.systemUTC()
- private var baseUrl: String = PRODUCTION_URL
- private var headers: MutableMap> = mutableMapOf()
+ private var baseUrl: String? = null
+ private var headers: Headers.Builder = Headers.builder()
+ private var queryParams: QueryParams.Builder = QueryParams.builder()
private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
private var maxRetries: Int = 2
private var apiKey: String? = null
- fun httpClient(httpClient: HttpClient) = apply { this.httpClient = httpClient }
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions) = apply {
+ httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
+ jsonMapper = clientOptions.jsonMapper
+ streamHandlerExecutor = clientOptions.streamHandlerExecutor
+ sleeper = clientOptions.sleeper
+ clock = clientOptions.clock
+ baseUrl = clientOptions.baseUrl
+ headers = clientOptions.headers.toBuilder()
+ queryParams = clientOptions.queryParams.toBuilder()
+ responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
+ maxRetries = clientOptions.maxRetries
+ apiKey = clientOptions.apiKey
+ }
+
+ /**
+ * 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 }
- fun baseUrl(baseUrl: String) = apply { this.baseUrl = baseUrl }
+ /**
+ * 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 interface to use for delaying execution, like during retries.
+ *
+ * This is primarily useful for using fake delays in tests.
+ *
+ * Defaults to real execution delays.
+ *
+ * This class takes ownership of the sleeper and closes it when closed.
+ */
+ fun sleeper(sleeper: Sleeper) = apply { this.sleeper = PhantomReachableSleeper(sleeper) }
+ /**
+ * 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 }
+ /**
+ * 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 }
+
+ /** Alias for calling [Builder.apiKey] with `apiKey.orElse(null)`. */
+ fun apiKey(apiKey: Optional) = apiKey(apiKey.getOrNull())
+
+ fun headers(headers: Headers) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
fun headers(headers: Map>) = apply {
this.headers.clear()
putAllHeaders(headers)
}
- fun putHeader(name: String, value: String) = apply {
- this.headers.getOrPut(name) { mutableListOf() }.add(value)
+ fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+
+ fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ this.headers.putAll(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ headers.replace(name, values)
}
- fun putHeaders(name: String, values: Iterable) = apply {
- this.headers.getOrPut(name) { mutableListOf() }.addAll(values)
+ fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ this.headers.replaceAll(headers)
}
- fun putAllHeaders(headers: Map>) = apply {
- headers.forEach(this::putHeaders)
+ fun removeHeaders(name: String) = apply { headers.remove(name) }
+
+ fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
}
- fun removeHeader(name: String) = apply { this.headers.put(name, mutableListOf()) }
+ fun queryParams(queryParams: Map>) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
- fun responseValidation(responseValidation: Boolean) = apply {
- this.responseValidation = responseValidation
+ fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ queryParams.put(key, values)
}
- fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.putAll(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.putAll(queryParams)
+ }
- fun apiKey(apiKey: String) = apply { this.apiKey = apiKey }
+ fun replaceQueryParams(key: String, value: String) = apply {
+ queryParams.replace(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ queryParams.replace(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.replaceAll(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.replaceAll(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
- fun fromEnv() = apply { System.getenv("BRAINTRUST_API_KEY")?.let { apiKey(it) } }
+ fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
+ 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.getProperty("braintrust.baseUrl") ?: System.getenv("BRAINTRUST_BASE_URL"))
+ ?.let { baseUrl(it) }
+ (System.getProperty("braintrust.apiKey") ?: System.getenv("BRAINTRUST_API_KEY"))?.let {
+ apiKey(it)
+ }
+ }
+
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
fun build(): ClientOptions {
- checkNotNull(httpClient) { "`httpClient` is required but was not set" }
- checkNotNull(apiKey) { "`apiKey` is required but was not set" }
+ val httpClient = checkRequired("httpClient", httpClient)
+ val streamHandlerExecutor =
+ streamHandlerExecutor
+ ?: PhantomReachableExecutorService(
+ Executors.newCachedThreadPool(
+ object : ThreadFactory {
- val headers = ArrayListMultimap.create()
+ 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()}"
+ }
+ }
+ )
+ )
+ val sleeper = sleeper ?: PhantomReachableSleeper(DefaultSleeper())
+
+ val headers = Headers.builder()
+ val queryParams = QueryParams.builder()
headers.put("X-Stainless-Lang", "java")
headers.put("X-Stainless-Arch", getOsArch())
headers.put("X-Stainless-OS", getOsName())
headers.put("X-Stainless-OS-Version", getOsVersion())
headers.put("X-Stainless-Package-Version", getPackageVersion())
+ headers.put("X-Stainless-Runtime", "JRE")
headers.put("X-Stainless-Runtime-Version", getJavaVersion())
- if (!apiKey.isNullOrEmpty()) {
- headers.put("Authorization", "Bearer ${apiKey}")
+ apiKey?.let {
+ if (!it.isEmpty()) {
+ headers.put("Authorization", "Bearer $it")
+ }
}
- this.headers.forEach(headers::replaceValues)
+ headers.replaceAll(this.headers.build())
+ queryParams.replaceAll(this.queryParams.build())
return ClientOptions(
+ httpClient,
RetryingHttpClient.builder()
- .httpClient(httpClient!!)
+ .httpClient(httpClient)
+ .sleeper(sleeper)
.clock(clock)
.maxRetries(maxRetries)
.build(),
- jsonMapper ?: jsonMapper(),
+ checkJacksonVersionCompatibility,
+ jsonMapper,
+ streamHandlerExecutor,
+ sleeper,
clock,
baseUrl,
- apiKey!!,
- headers.toUnmodifiable(),
+ headers.build(),
+ queryParams.build(),
responseValidation,
+ timeout,
+ maxRetries,
+ apiKey,
)
}
}
+
+ /**
+ * 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()
+ sleeper.close()
+ }
}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/DefaultSleeper.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/DefaultSleeper.kt
new file mode 100644
index 00000000..32ece057
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/DefaultSleeper.kt
@@ -0,0 +1,28 @@
+package com.braintrustdata.api.core
+
+import java.time.Duration
+import java.util.Timer
+import java.util.TimerTask
+import java.util.concurrent.CompletableFuture
+
+class DefaultSleeper : Sleeper {
+
+ private val timer = Timer("DefaultSleeper", true)
+
+ override fun sleep(duration: Duration) = Thread.sleep(duration.toMillis())
+
+ override fun sleepAsync(duration: Duration): CompletableFuture {
+ val future = CompletableFuture()
+ timer.schedule(
+ object : TimerTask() {
+ override fun run() {
+ future.complete(null)
+ }
+ },
+ duration.toMillis(),
+ )
+ return future
+ }
+
+ override fun close() = timer.cancel()
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ObjectMappers.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ObjectMappers.kt
old mode 100644
new mode 100755
index 5b1f8ea1..c3287a6b
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ObjectMappers.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/ObjectMappers.kt
@@ -3,23 +3,165 @@
package com.braintrustdata.api.core
import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
-import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
-import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
fun jsonMapper(): JsonMapper =
- jacksonMapperBuilder()
+ JsonMapper.builder()
+ .addModule(kotlinModule())
.addModule(Jdk8Module())
.addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
- .withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
.build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
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/Params.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Params.kt
new file mode 100644
index 00000000..f98bf142
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Params.kt
@@ -0,0 +1,16 @@
+package com.braintrustdata.api.core
+
+import com.braintrustdata.api.core.http.Headers
+import com.braintrustdata.api.core.http.QueryParams
+
+/** An interface representing parameters passed to a service method. */
+interface Params {
+ /** The full set of headers in the parameters, including both fixed and additional headers. */
+ fun _headers(): Headers
+
+ /**
+ * The full set of query params in the parameters, including both fixed and additional query
+ * params.
+ */
+ fun _queryParams(): QueryParams
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachable.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachable.kt
new file mode 100644
index 00000000..91785b1a
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachable.kt
@@ -0,0 +1,56 @@
+@file:JvmName("PhantomReachable")
+
+package com.braintrustdata.api.core
+
+import com.braintrustdata.api.errors.BraintrustException
+import java.lang.reflect.InvocationTargetException
+
+/**
+ * Closes [closeable] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable) {
+ check(observed !== closeable) {
+ "`observed` cannot be the same object as `closeable` because it would never become phantom reachable"
+ }
+ closeWhenPhantomReachable(observed, closeable::close)
+}
+
+/**
+ * Calls [close] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, close: () -> Unit) {
+ closeWhenPhantomReachable?.let { it(observed, close) }
+}
+
+private val closeWhenPhantomReachable: ((Any, () -> Unit) -> Unit)? by lazy {
+ try {
+ val cleanerClass = Class.forName("java.lang.ref.Cleaner")
+ val cleanerCreate = cleanerClass.getMethod("create")
+ val cleanerRegister =
+ cleanerClass.getMethod("register", Any::class.java, Runnable::class.java)
+ val cleanerObject = cleanerCreate.invoke(null);
+
+ { observed, close ->
+ try {
+ cleanerRegister.invoke(cleanerObject, observed, Runnable { close() })
+ } catch (e: ReflectiveOperationException) {
+ if (e is InvocationTargetException) {
+ when (val cause = e.cause) {
+ is RuntimeException,
+ is Error -> throw cause
+ }
+ }
+ throw BraintrustException("Unexpected reflective invocation failure", e)
+ }
+ }
+ } catch (e: ReflectiveOperationException) {
+ // We're running Java 8, which has no Cleaner.
+ null
+ }
+}
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/PhantomReachableSleeper.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachableSleeper.kt
new file mode 100644
index 00000000..8c293f53
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PhantomReachableSleeper.kt
@@ -0,0 +1,23 @@
+package com.braintrustdata.api.core
+
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+
+/**
+ * A delegating wrapper around a [Sleeper] that closes it once it's only phantom reachable.
+ *
+ * This class ensures the [Sleeper] is closed even if the user forgets to do it.
+ */
+internal class PhantomReachableSleeper(private val sleeper: Sleeper) : Sleeper {
+
+ init {
+ closeWhenPhantomReachable(this, sleeper)
+ }
+
+ override fun sleep(duration: Duration) = sleeper.sleep(duration)
+
+ override fun sleepAsync(duration: Duration): CompletableFuture =
+ sleeper.sleepAsync(duration)
+
+ override fun close() = sleeper.close()
+}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PrepareRequest.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PrepareRequest.kt
new file mode 100644
index 00000000..e9aaadff
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package com.braintrustdata.api.core
+
+import com.braintrustdata.api.core.http.HttpRequest
+import java.util.concurrent.CompletableFuture
+
+@JvmSynthetic
+internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest =
+ toBuilder()
+ .putAllQueryParams(clientOptions.queryParams)
+ .replaceAllQueryParams(params._queryParams())
+ .putAllHeaders(clientOptions.headers)
+ .replaceAllHeaders(params._headers())
+ .build()
+
+@JvmSynthetic
+internal fun HttpRequest.prepareAsync(
+ clientOptions: ClientOptions,
+ params: Params,
+): CompletableFuture =
+ // This async version exists to make it easier to add async specific preparation logic in the
+ // future.
+ CompletableFuture.completedFuture(prepare(clientOptions, params))
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Properties.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Properties.kt
old mode 100644
new mode 100755
index 1c59eee1..5130de1b
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Properties.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Properties.kt
@@ -2,7 +2,7 @@
package com.braintrustdata.api.core
-import java.util.Properties
+import com.braintrustdata.api.client.BraintrustClient
fun getOsArch(): String {
val osArch = System.getProperty("os.arch")
@@ -16,7 +16,7 @@ fun getOsArch(): String {
"x86_64" -> "x64"
"arm" -> "arm"
"aarch64" -> "arm64"
- else -> "other:${osArch}"
+ else -> "other:$osArch"
}
}
@@ -30,18 +30,13 @@ fun getOsName(): String {
osName.startsWith("Linux") -> "Linux"
osName.startsWith("Mac OS") -> "MacOS"
osName.startsWith("Windows") -> "Windows"
- else -> "Other:${osName}"
+ else -> "Other:$osName"
}
}
-fun getOsVersion(): String {
- return System.getProperty("os.version", "unknown")
-}
+fun getOsVersion(): String = System.getProperty("os.version", "unknown")
-fun getPackageVersion(): String {
- return Properties::class.java.`package`.implementationVersion ?: "unknown"
-}
+fun getPackageVersion(): String =
+ BraintrustClient::class.java.`package`.implementationVersion ?: "unknown"
-fun getJavaVersion(): String {
- return System.getProperty("java.version", "unknown")
-}
+fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/RequestOptions.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/RequestOptions.kt
old mode 100644
new mode 100755
index 7a8c8b26..fe7c8372
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/RequestOptions.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/RequestOptions.kt
@@ -2,17 +2,7 @@ package com.braintrustdata.api.core
import java.time.Duration
-class RequestOptions
-private constructor(
- val responseValidation: Boolean?,
- val timeout: Duration?,
-) {
- fun applyDefaults(options: RequestOptions): RequestOptions {
- return RequestOptions(
- responseValidation = this.responseValidation ?: options.responseValidation,
- timeout = this.timeout ?: options.timeout,
- )
- }
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
companion object {
@@ -20,21 +10,37 @@ private constructor(
@JvmStatic fun none() = NONE
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
@JvmStatic fun builder() = Builder()
}
- class Builder {
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
+ class Builder internal constructor() {
+
private var responseValidation: Boolean? = null
- private var timeout: Duration? = null
+ private var timeout: Timeout? = null
fun responseValidation(responseValidation: Boolean) = apply {
this.responseValidation = responseValidation
}
- fun timeout(timeout: Duration) = apply { this.timeout = timeout }
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
- fun build(): RequestOptions {
- return RequestOptions(responseValidation, timeout)
- }
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
}
}
diff --git a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Sleeper.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Sleeper.kt
new file mode 100644
index 00000000..856b4009
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Sleeper.kt
@@ -0,0 +1,21 @@
+package com.braintrustdata.api.core
+
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+
+/**
+ * An interface for delaying execution for a specified amount of time.
+ *
+ * Useful for testing and cleaning up resources.
+ */
+interface Sleeper : AutoCloseable {
+
+ /** Synchronously pauses execution for the given [duration]. */
+ fun sleep(duration: Duration)
+
+ /** Asynchronously pauses execution for the given [duration]. */
+ fun sleepAsync(duration: Duration): CompletableFuture
+
+ /** Overridden from [AutoCloseable] to not have a checked exception in its signature. */
+ override fun close()
+}
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
new file mode 100644
index 00000000..b2320ac8
--- /dev/null
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Timeout.kt
@@ -0,0 +1,171 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.braintrustdata.api.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Timeout &&
+ connect == other.connect &&
+ read == other.read &&
+ write == other.write &&
+ request == other.request
+ }
+
+ 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
old mode 100644
new mode 100755
index 46bee377..a1657ff3
--- 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
@@ -3,57 +3,113 @@
package com.braintrustdata.api.core
import com.braintrustdata.api.errors.BraintrustInvalidDataException
-import com.google.common.collect.ImmutableListMultimap
-import com.google.common.collect.ListMultimap
-import com.google.common.collect.Multimaps
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 {
- if (this == null) {
- throw BraintrustInvalidDataException("'${name}' is not present")
- }
+internal fun T?.getOrThrow(name: String): T =
+ this ?: throw BraintrustInvalidDataException("`${name}` is not present")
- return this
-}
+@JvmSynthetic
+internal fun List.toImmutable(): List =
+ if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
@JvmSynthetic
-internal fun List.toUnmodifiable(): List {
- if (isEmpty()) {
- return Collections.emptyList()
- }
+internal fun Map.toImmutable(): Map =
+ if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
- return Collections.unmodifiableList(this)
-}
+@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap()
+
+@JvmSynthetic
+internal fun , V> SortedMap.toImmutable(): SortedMap =
+ if (isEmpty()) Collections.emptySortedMap()
+ else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
+/**
+ * Returns all elements that yield the largest value for the given function, or an empty list if
+ * there are zero elements.
+ *
+ * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
+ * value; not just the first one.
+ */
@JvmSynthetic
-internal fun Map.toUnmodifiable(): Map {
- if (isEmpty()) {
- return Collections.emptyMap()
+internal fun > Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
}
- return Collections.unmodifiableMap(this)
+ return maxElements
}
+/**
+ * Returns whether [this] is equal to [other].
+ *
+ * This differs from [Object.equals] because it also deeply equates arrays based on their contents,
+ * even when there are arrays directly nested within other arrays.
+ */
@JvmSynthetic
-internal fun ListMultimap.toUnmodifiable(): ListMultimap {
- if (isEmpty()) {
- return ImmutableListMultimap.of()
- }
+internal infix fun Any?.contentEquals(other: Any?): Boolean =
+ arrayOf(this).contentDeepEquals(arrayOf(other))
- return Multimaps.unmodifiableListMultimap(this)
-}
+/**
+ * Returns a hash of the given sequence of [values].
+ *
+ * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode()
+/**
+ * Returns a [String] representation of [this].
+ *
+ * This differs from [Object.toString] because it also deeply stringifies arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
@JvmSynthetic
-internal fun ListMultimap.getRequiredHeader(header: String): String {
- val value =
- entries()
- .stream()
- .filter { entry -> entry.key.equals(header, ignoreCase = true) }
- .map { entry -> entry.value }
- .findFirst()
- if (!value.isPresent) {
- throw BraintrustInvalidDataException("Could not find $header header")
+internal fun Any?.contentToString(): String {
+ var string = arrayOf(this).contentDeepToString()
+ if (string.startsWith('[')) {
+ string = string.substring(1)
}
- return value.get()
+ if (string.endsWith(']')) {
+ string = string.substring(0, string.length - 1)
+ }
+ return 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/Values.kt b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Values.kt
old mode 100644
new mode 100755
index 8628cbb4..7f71390d
--- a/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Values.kt
+++ b/braintrust-java-core/src/main/kotlin/com/braintrustdata/api/core/Values.kt
@@ -2,8 +2,6 @@ package com.braintrustdata.api.core
import com.braintrustdata.api.errors.BraintrustInvalidDataException
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
-import com.fasterxml.jackson.annotation.JsonAutoDetect
-import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.JsonGenerator
@@ -27,65 +25,145 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT
import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
import com.fasterxml.jackson.databind.ser.std.NullSerializer
+import java.io.InputStream
+import java.util.Objects
import java.util.Optional
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
@JsonDeserialize(using = JsonField.Deserializer::class)
sealed class JsonField {
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
fun isMissing(): Boolean = this is JsonMissing
+ /** Whether this field is explicitly set to `null`. */
fun isNull(): Boolean = this is JsonNull
- fun asKnown(): Optional =
- when (this) {
- is KnownValue -> Optional.of(value)
- else -> Optional.empty()
- }
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
/**
- * If the "known" value (i.e. matching the type that the SDK expects) is returned by the API
- * then this method will return an empty `Optional`, otherwise the returned `Optional` is given
- * a `JsonValue`.
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
*/
- fun asUnknown(): Optional =
- when (this) {
- is JsonValue -> Optional.of(this)
- else -> Optional.empty()
- }
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
fun asBoolean(): Optional =
when (this) {
is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
fun asNumber(): Optional =
when (this) {
is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
fun asString(): Optional =
when (this) {
is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
else -> Optional.empty()
}
fun asStringOrThrow(): String =
- when (this) {
- is JsonString -> value
- else -> throw BraintrustInvalidDataException("Value is not a string")
- }
+ asString().orElseThrow { BraintrustInvalidDataException("Value is not a string") }
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
fun asArray(): Optional> =
when (this) {
is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
else -> Optional.empty()
}
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
fun asObject(): Optional