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
16 changes: 9 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,20 @@ jobs:
- name: Install locked dependencies
run: uv sync --locked --extra dev
- name: Check formatting
run: uv run ruff format --check .
run: uv run --locked --no-sync --no-build ruff format --check .
- name: Build distributions
run: uv build
- name: Smoke-check built distributions
run: uv run --locked --no-sync --no-build python scripts/check_distribution.py
- name: Lint
run: uv run ruff check .
run: uv run --locked --no-sync --no-build ruff check .
- name: Type check
run: uv run mypy --strict
run: uv run --locked --no-sync --no-build mypy --strict
- name: Test transport and models
run: uv run python -m pytest tests/test_client.py
run: uv run --locked --no-sync --no-build python -m pytest tests/test_client.py
- name: Run mocked examples
run: uv run python -m pytest tests/test_examples.py
run: uv run --locked --no-sync --no-build python -m pytest tests/test_examples.py
- name: Execute README examples
run: uv run python -m pytest tests/test_docs.py
run: uv run --locked --no-sync --no-build python -m pytest tests/test_docs.py
- name: Check API contract
run: uv run python scripts/check_contracts.py
run: uv run --locked --no-sync --no-build python scripts/check_contracts.py
12 changes: 7 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,19 @@ jobs:
- name: Install locked dependencies
run: uv sync --locked --extra dev
- name: Check formatting
run: uv run ruff format --check .
run: uv run --locked --no-sync --no-build ruff format --check .
- name: Build distributions
run: uv build
- name: Smoke-check built distributions
run: uv run --locked --no-sync --no-build python scripts/check_distribution.py
- name: Lint
run: uv run ruff check .
run: uv run --locked --no-sync --no-build ruff check .
- name: Type check
run: uv run mypy --strict
run: uv run --locked --no-sync --no-build mypy --strict
- name: Test sync, async, examples, and docs
run: uv run python -m pytest
run: uv run --locked --no-sync --no-build python -m pytest
- name: Check API contract
run: uv run python scripts/check_contracts.py
run: uv run --locked --no-sync --no-build python scripts/check_contracts.py
- name: Generate SBOM
uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
with:
Expand Down
19 changes: 16 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@ All notable changes to this SDK are recorded here.

<!-- towncrier release notes start -->

## 1.0.0 (2026-10-08)
## 1.0.0 (2026-10-09)

### Breaking changes

- Use resource-oriented method names and semantic path parameters.
- Distinguish omitted request bodies from JSON `null` and `{}`. Omitting `body` now sends no body.

### Features

- Release the unified Python 1.0 SDK with synchronous and asynchronous clients, all 370 catalogued operations, typed AI text generation and embeddings, fluent agent and workflow builders, Pydantic v2 request models, offline HTTPX tests, and PyPI trusted publishing.
- Add sync and async AI tool registry helpers, plus agent configuration, approval predicate, and run completion polling helpers.
- Provide synchronous and asynchronous clients, all 370 catalogued operations,
typed AI helpers, and fluent agent and workflow builders.
- Add `FrontalError.transient` and `FrontalError.safe_to_retry` to separate temporary failures from safe replay.
- Add jitter to client-generated retry delays and enforce asynchronous polling deadlines.
- Add agent configuration, approval predicates, and run completion polling.

### Bug fixes

- Preserve server-sent event IDs across events that omit a new ID.
- Retain details from non-standard JSON error responses and validate pagination limits.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,24 @@ environment header, and debug logging. The SDK does not load `.env` files.
- HTTPX sync and async transports with bounded GET retries, request IDs,
pagination helpers, polling, raw bytes, multipart uploads, and SSE streams.
- Pydantic v2 request models and validation for JSON-compatible payloads.
- A structured error hierarchy with status, code, request ID, and retryability.
- A structured error hierarchy with status, code, request ID, transient status,
and an explicit `safe_to_retry` signal.
- Inline package typing, including `py.typed`.

Streaming POST requests default to no retries because a connection failure can
occur after the server has accepted the request. Pass an explicit retry count
only when replaying that operation is safe. If you provide an `httpx.Client` or
`httpx.AsyncClient`, you retain ownership of its lifecycle and timeout settings.
For JSON writes, omitting `body` sends no request body; pass `body=None` to send
the JSON value `null`, and `body={}` to send an empty JSON object.

The OpenAPI snapshots define the operation inventory but do not include
service-specific request and response schemas for most endpoints. The SDK
does not guess those payload shapes; define request models with `APIModel` and
use `PageResult[T]` for paginated responses where applicable.

The SDK uses resource-oriented names and semantic path arguments.

## Development

Install the locked development environment with `uv`:
Expand Down
8 changes: 7 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ request IDs, timeouts, bounded retries with backoff, Pydantic JSON validation,
structured errors, multipart uploads, raw bytes, and server-sent events. Safe
GET requests are retryable. Stream retries are limited to the period before
the first event is delivered, so an already-consumed stream is never replayed.
Streaming POST requests default to zero retries because a request may have been
accepted before a connection failure is visible to the client. Retrying one is
opt-in through its `max_retries` argument and is appropriate only when replay is
safe. The server's `Retry-After` value is honored up to five seconds. Debug
request logs include the path but omit query values.
`core/pagination.py` and `core/polling.py` provide sync and async cursor
iteration and polling helpers.

The clients default to `https://api.frontal.dev/v1`, a 30 second timeout, and
three transport retries. Pass `base_url`, `timeout`, `max_retries`,
`environment`, or `debug` to override those defaults. Both clients are context
managers and close their HTTPX connection pools on exit.
managers and close SDK-owned HTTPX connection pools on exit. If an HTTPX client
is injected, the caller owns it and its timeout configuration.

## Request flow

Expand Down
6 changes: 3 additions & 3 deletions docs/ENTERPRISE_READINESS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enterprise readiness

The Python SDK includes sync and async HTTPX transports, validated client configuration, structured API errors, bounded GET retries, Pydantic boundary models, and methods for the complete endpoint inventory. Remaining release operations are:
The Python SDK includes sync and async HTTPX transports, validated client configuration, structured API errors, bounded GET retries, Pydantic boundary models, and methods for the complete endpoint inventory. The repository also contains a GitHub OIDC PyPI publishing workflow with SBOM generation and build provenance attestation. Remaining readiness work is:

- Generate operation-specific request and response models when the API publishes complete schemas.
- Configure the PyPI trusted publisher for this repository, workflow, and `pypi` environment.
- Obtain verified operation-specific request and response schemas from API owners. The committed API snapshot has no request-body schemas and uses generic object schemas for successful responses; the AI snapshot has no operation response schemas.
- Configure the PyPI trusted publisher for this repository, the `publish.yml` workflow, and the `pypi` environment in the PyPI project settings.
- Review security controls, supported authentication flows, and service-specific operational guidance during release review.
68 changes: 60 additions & 8 deletions docs/RESOURCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ an execution, list executions, and poll one to a terminal state. The same
accessors work with `AsyncFrontal`; await their results and use `async for` for
streams.

## Raw endpoint methods
## Endpoint methods

Each service also exposes methods corresponding to the committed inventory.
For example:
Expand All @@ -68,19 +68,71 @@ For example:
from frontal_sdk import Frontal

with Frontal() as client:
agent = client.agents.get_agents_by_param_1("agent_123")
page = client.agents.get_agents(query={"limit": 20})
agent = client.agents.get(id="agent_123")
page = client.agents.list(query={"limit": 20})
```

Raw method names include the HTTP verb and route segments. Since the inventory
does not preserve path parameter names, placeholders appear as `param_1`,
`param_2`, and so on in route order. Use `query=` for query parameters,
`body=` for JSON writes, `parts=` for multipart uploads, and `data=` with
`content_type=` for raw request bodies. Raw streaming methods return
The `agents` collection exposes concise `get(id=...)` and `list(query=...)`
methods, with runs and versions under `client.agents.runs` and
`client.agents.versions`. Other service namespaces continue to expose their
current resource-oriented operation names. Across them, use `query=` for query
parameters, `body=` for JSON writes, `parts=` for multipart uploads, and `data=`
with `content_type=` for raw request bodies. Streaming methods return
`ServerEvent` iterators; raw response-body methods return bytes.

Pydantic models cover shared and high-level boundaries. Raw operation payloads
use `JSONValue` where the OpenAPI snapshots do not define operation-specific
schemas. The endpoint inventory and OpenAPI snapshots remain authoritative;
the contract gate checks that endpoint methods do not drift. Run
`python scripts/check_contracts.py` after changing inventory-backed methods.

The SDK uses resource-oriented names and semantic path arguments.

If you inject an `httpx.Client` or `httpx.AsyncClient`, set its timeout on that
HTTPX instance. The SDK uses the injected instance as-is and leaves closing it
to your application:

```python
import httpx
from frontal_sdk import Frontal

http = httpx.Client(timeout=httpx.Timeout(10.0, connect=2.0))
with Frontal(http_client=http) as client:
agents = client.agents.list()
http.close()
```

## Pagination and errors

The endpoint inventory does not assign a response model to every collection.
Validate the documented page envelope at the call site, then use the shared
cursor helper:

```python
from frontal_sdk import Frontal, PageResult, RateLimitError, paginate
from frontal_sdk.models import JSONValue

with Frontal() as client:

def fetch_agents(query):
response = client.agents.list(query=query)
return PageResult[dict[str, JSONValue]].model_validate(response)

try:
agent_rows = list(paginate(fetch_agents, query={"limit": 50}))
except RateLimitError as error:
print(error.request_id, error.retry_after)
```

Each fetch callback receives the original query plus the cursor returned by the
prior page. Configure `timeout` on the client as well as on polling helpers: a
polling deadline cannot interrupt a blocking synchronous fetch.

## Request bodies and retry safety

Write methods distinguish an omitted body from JSON values. Omitting `body`
sends no request body; `body=None` sends JSON `null`, and `body={}` sends an
empty JSON object. API errors expose `transient` for temporary failures and
`safe_to_retry` for operations that can be replayed safely. A transient error
from a write may still represent an operation the server accepted, so inspect
`safe_to_retry` before replaying it.
6 changes: 4 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
- [x] Add the shared API contract snapshots and Python coverage placeholder.
- [x] Implement the core client, transport, configuration, and error model.
- [x] Generate typed resource methods for all endpoint inventory operations.
- [ ] Add operation-specific request and response models when complete API schemas are available.
- [ ] Acquire verified operation-specific request and response schemas; keep unknown payloads as explicit JSON boundaries until confirmed.
- [x] Add contract conformance checks and local HTTP integration tests.
- [x] Add offline runnable examples for sync and async AI, agent definitions, and workflows.
- [ ] Configure protected PyPI releases.
- [x] Add a tag-triggered PyPI publishing workflow with GitHub OIDC, SBOM, and provenance attestation.
- [ ] Configure the account-side PyPI trusted publisher and environment protections.
- [x] Establish the resource-oriented SDK API as the canonical initial interface.
14 changes: 7 additions & 7 deletions examples/SDKS_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ define a workflow with `client.workflows.define(name).manual().task(...).create(
Then use the returned identifier with `client.agents.use(id)` or
`client.workflows.use(id)` to message, inspect, trigger, or poll runs.

Raw operations use method names that include the HTTP verb and route. Path
parameters are named `param_1`, `param_2`, and so on in route order. Pass query
parameters as `query=`, JSON request values as `body=`, multipart uploads as
`parts=`, and raw request bodies as `data=` plus `content_type=`. Most raw
operations return JSON values because the OpenAPI snapshot does not define a
specific response schema for each operation. See [API resources](../docs/RESOURCES.md)
for details and [examples](./README.md) for runnable scripts.
SDK operations use resource-oriented method names and semantic path
arguments where the contract provides them. Pass query parameters as `query=`,
JSON request values as `body=`, multipart uploads as `parts=`, and raw request
bodies as `data=` plus `content_type=`. Most operations return JSON values
because the OpenAPI snapshot does not define a specific response schema for
each operation. See [API resources](../docs/RESOURCES.md) and
[examples](./README.md).

Sync streams use `for`; async streams use `async for`. AI `stream_text()` yields
typed text and lifecycle parts. Agent `watch(run_id)` yields raw
Expand Down
8 changes: 6 additions & 2 deletions frontal_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from frontal_sdk.core.pagination import async_paginate, paginate
from frontal_sdk.core.polling import async_poll_until, poll_until
from frontal_sdk.models import (
UNSET,
Agent,
AgentDefinition,
AgentExecution,
Expand Down Expand Up @@ -43,8 +44,8 @@
PageResult,
PaginationMeta,
QueryParams,
RegisteredTool,
RequestBody,
RequestBodyInput,
RerankOptions,
RerankResult,
ServerEvent,
Expand All @@ -54,6 +55,7 @@
ToolDefinition,
TranscriptionOptions,
TranscriptionResult,
Unset,
Workflow,
WorkflowDefinition,
WorkflowExecution,
Expand Down Expand Up @@ -110,9 +112,9 @@
"PaginationMeta",
"poll_until",
"QueryParams",
"RegisteredTool",
"RateLimitError",
"RequestBody",
"RequestBodyInput",
"RerankOptions",
"RerankResult",
"ServerError",
Expand All @@ -131,5 +133,7 @@
"WorkflowExecution",
"StepDefinition",
"tool",
"UNSET",
"Unset",
"__version__",
]
9 changes: 7 additions & 2 deletions frontal_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ class Frontal:

``api_key`` defaults to ``FRONTAL_API_KEY``. The base URL, environment,
and debug setting also read ``FRONTAL_API_URL``, ``FRONTAL_ENV``, and
``FRONTAL_DEBUG`` when their matching arguments are omitted.
``FRONTAL_DEBUG`` when their matching arguments are omitted. A supplied
``http_client`` remains caller-owned and keeps its own timeout configuration.
"""

def __init__(
Expand Down Expand Up @@ -153,7 +154,11 @@ def __exit__(self, *_: object) -> None:


class AsyncFrontal:
"""Asynchronous API client with the same typed service namespaces."""
"""Async client with matching typed services.

A supplied ``http_client`` remains caller-owned and keeps its own timeout
configuration.
"""

def __init__(
self,
Expand Down
Loading
Loading