Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
jobs:
check:
strategy:
# One OS or Python version failing should not hide the others' results.
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.14"]
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ All tasks are run via `uv run poe <task>`:

## End-to-End Tests

E2e tests in `tests/test_e2e.py` run against a live Baseten environment. They are skipped automatically when `BASETEN_E2E_TEST_API_KEY` is not set.
E2e tests in `tests/client/test_e2e.py` and `tests/sandbox/test_e2e.py` run against a live Baseten environment. They are skipped automatically when `BASETEN_E2E_TEST_API_KEY` is not set. The sandbox tests need no model, and print every request and response with `BASETEN_E2E_TEST_DEBUG=1`.

### Bootstrap

Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,31 @@ async with AsyncManagementClient(api_key="my-api-key") as client:
print(model.name)
```

## Sandboxes

`baseten.sandbox` creates and works with sandboxes. HTTP/2 is used when the `h2` package is installed, which is strongly recommended:

```bash
pip install "baseten" "httpx[http2]"
```

```python
from baseten.sandbox import SandboxClient

with SandboxClient(api_key="my-api-key") as client:
sandbox = client.create()
try:
sandbox.fs.write(path="/tmp/hello.txt", content="hello")
result = sandbox.process.exec(
command="cat /tmp/hello.txt", wait_for_completion=True
)
print(result.stdout)
finally:
client.delete(name=sandbox.name)
```

`AsyncSandboxClient` is the same for async code. Operations the SDK does not wrap are available on the generated clients, through `client.raw_api` for the control plane and `sandbox.raw_api` for a sandbox. See the [API documentation](https://basetenlabs.github.io/baseten-python/) for the rest.

## Upgrading from 0.8.2 and earlier

Version 0.9.0 is a rewrite and shares no API with the earlier `baseten` releases.
Expand Down
91 changes: 79 additions & 12 deletions baseten/client/inferenceapi/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import contextlib
import urllib.parse
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Literal, TypeVar

Expand Down Expand Up @@ -72,9 +73,33 @@ class ApiClient:
They may change without notice between versions.
"""

def __init__(self, http_client: httpx.Client) -> None:
"""Create a new client. The caller is responsible for closing *http_client*."""
def __init__(
self,
http_client: httpx.Client,
*,
base_url: str | None = None,
auth: httpx.Auth | None = None,
headers: Mapping[str, str] | None = None,
timeout: httpx.Timeout | None = None,
) -> None:
"""Create a new client. The caller is responsible for closing *http_client*.

The keyword arguments apply to this client's requests only, so one
HTTP client can serve several APIs. Each one left ``None`` falls back
to the HTTP client's own setting.

Args:
http_client: HTTP client to send requests with.
base_url: Base URL that request paths are appended to.
auth: Authentication for each request.
headers: Headers for each request, added to the HTTP client's own.
timeout: Timeouts for each request.
"""
self._http_client = http_client
self._base_url = base_url
self._auth = auth
self._headers = headers
self._timeout = timeout

def async_predict(
self, *, env_name: str, request: AsyncPredictRequest
Expand Down Expand Up @@ -848,7 +873,7 @@ def _build_request(self, request: _ApiRequest) -> httpx.Request:
json_body = None
content_body = None
files_body = None
headers: dict[str, str] = {}
headers: dict[str, str] = {**(self._headers or {})}
if request.accept is not None:
headers["Accept"] = request.accept
if request.body is not None:
Expand Down Expand Up @@ -882,16 +907,22 @@ def _build_request(self, request: _ApiRequest) -> httpx.Request:
params = request.query
return self._http_client.build_request(
request.method,
path,
path if self._base_url is None else self._base_url.rstrip("/") + path,
json=json_body,
content=content_body,
files=files_body,
params=params,
headers=headers,
timeout=httpx.USE_CLIENT_DEFAULT
if self._timeout is None
else self._timeout,
)

def _do(self, request: _ApiRequest) -> httpx.Response:
response = self._http_client.send(self._build_request(request))
response = self._http_client.send(
self._build_request(request),
auth=httpx.USE_CLIENT_DEFAULT if self._auth is None else self._auth,
)
if response.status_code not in request.success_codes:
if request.error_codes and response.status_code in request.error_codes:
error_name = request.error_codes[response.status_code]
Expand All @@ -911,7 +942,11 @@ def _do(self, request: _ApiRequest) -> httpx.Response:
return response

def _do_raw(self, request: _ApiRequest) -> httpx.Response:
response = self._http_client.send(self._build_request(request), stream=True)
response = self._http_client.send(
self._build_request(request),
auth=httpx.USE_CLIENT_DEFAULT if self._auth is None else self._auth,
stream=True,
)
if response.status_code not in request.success_codes:
response.read()
if request.error_codes and response.status_code in request.error_codes:
Expand Down Expand Up @@ -952,9 +987,33 @@ class AsyncApiClient:
They may change without notice between versions.
"""

def __init__(self, http_client: httpx.AsyncClient) -> None:
"""Create a new client. The caller is responsible for closing *http_client*."""
def __init__(
self,
http_client: httpx.AsyncClient,
*,
base_url: str | None = None,
auth: httpx.Auth | None = None,
headers: Mapping[str, str] | None = None,
timeout: httpx.Timeout | None = None,
) -> None:
"""Create a new client. The caller is responsible for closing *http_client*.

The keyword arguments apply to this client's requests only, so one
HTTP client can serve several APIs. Each one left ``None`` falls back
to the HTTP client's own setting.

Args:
http_client: HTTP client to send requests with.
base_url: Base URL that request paths are appended to.
auth: Authentication for each request.
headers: Headers for each request, added to the HTTP client's own.
timeout: Timeouts for each request.
"""
self._http_client = http_client
self._base_url = base_url
self._auth = auth
self._headers = headers
self._timeout = timeout

async def async_predict(
self, *, env_name: str, request: AsyncPredictRequest
Expand Down Expand Up @@ -1738,7 +1797,7 @@ def _build_request(self, request: _ApiRequest) -> httpx.Request:
json_body = None
content_body = None
files_body = None
headers: dict[str, str] = {}
headers: dict[str, str] = {**(self._headers or {})}
if request.accept is not None:
headers["Accept"] = request.accept
if request.body is not None:
Expand Down Expand Up @@ -1772,16 +1831,22 @@ def _build_request(self, request: _ApiRequest) -> httpx.Request:
params = request.query
return self._http_client.build_request(
request.method,
path,
path if self._base_url is None else self._base_url.rstrip("/") + path,
json=json_body,
content=content_body,
files=files_body,
params=params,
headers=headers,
timeout=httpx.USE_CLIENT_DEFAULT
if self._timeout is None
else self._timeout,
)

async def _do(self, request: _ApiRequest) -> httpx.Response:
response = await self._http_client.send(self._build_request(request))
response = await self._http_client.send(
self._build_request(request),
auth=httpx.USE_CLIENT_DEFAULT if self._auth is None else self._auth,
)
if response.status_code not in request.success_codes:
if request.error_codes and response.status_code in request.error_codes:
error_name = request.error_codes[response.status_code]
Expand All @@ -1802,7 +1867,9 @@ async def _do(self, request: _ApiRequest) -> httpx.Response:

async def _do_raw(self, request: _ApiRequest) -> httpx.Response:
response = await self._http_client.send(
self._build_request(request), stream=True
self._build_request(request),
auth=httpx.USE_CLIENT_DEFAULT if self._auth is None else self._auth,
stream=True,
)
if response.status_code not in request.success_codes:
await response.aread()
Expand Down
Loading
Loading