Skip to content

Commit 644e5ad

Browse files
authored
Release v4.32.0 (#1859)
1 parent b68c838 commit 644e5ad

5 files changed

Lines changed: 136 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## [v4.32.0] (2026-04-10)
4+
5+
* Add `push_dataset` helpers to the experimental API client by @dmontagu in [#1848](https://github.com/pydantic/logfire/pull/1848)
6+
37
## [v4.31.2] (2026-04-10)
48

59
* fix: Show token badges for OpenAI streaming logs by @alexmojaki in [#1857](https://github.com/pydantic/logfire/pull/1857)
@@ -1105,3 +1109,4 @@ First release from new repo!
11051109
[v4.31.0]: https://github.com/pydantic/logfire/compare/v4.30.0...v4.31.0
11061110
[v4.31.1]: https://github.com/pydantic/logfire/compare/v4.31.0...v4.31.1
11071111
[v4.31.2]: https://github.com/pydantic/logfire/compare/v4.31.1...v4.31.2
1112+
[v4.32.0]: https://github.com/pydantic/logfire/compare/v4.31.2...v4.32.0

logfire-api/logfire_api/experimental/api_client.pyi

Lines changed: 126 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ from logfire._internal.config import get_base_url_from_token as get_base_url_fro
66
from pydantic_evals import Case, Dataset
77
from pydantic_evals.evaluators import Evaluator
88
from types import TracebackType
9-
from typing import Any, Generic, TypeVar, overload
9+
from typing import Any, Generic, Literal, TypeVar, overload
1010
from typing_extensions import Self
1111

1212
Case = Any
@@ -42,8 +42,9 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
4242
Example usage:
4343
```python skip-run="true" skip-reason="external-connection"
4444
from dataclasses import dataclass
45+
from pydantic_evals import Case, Dataset
46+
4547
from logfire.experimental.api_client import LogfireAPIClient
46-
from pydantic_evals import Case
4748
4849
4950
@dataclass
@@ -56,21 +57,17 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
5657
answer: str
5758
5859
59-
with LogfireAPIClient(api_key=\'your-api-key\') as client:
60-
# Create typed dataset
61-
dataset = client.create_dataset(
62-
name=\'qa-dataset\',
63-
input_type=MyInput,
64-
output_type=MyOutput,
65-
)
60+
local_dataset = Dataset[MyInput, MyOutput, None](
61+
name=\'qa-dataset\',
62+
cases=[
63+
Case(name=\'q1\', inputs=MyInput(\'Hello?\'), expected_output=MyOutput(\'Hi!\')),
64+
],
65+
)
6666
67-
# Add cases using pydantic-evals Case objects
68-
client.add_cases(
69-
dataset[\'id\'],
70-
cases=[
71-
Case(name=\'q1\', inputs=MyInput(\'Hello?\'), expected_output=MyOutput(\'Hi!\')),
72-
],
73-
)
67+
68+
with LogfireAPIClient(api_key=\'your-api-key\') as client:
69+
# Publish the local dataset to hosted
70+
client.push_dataset(local_dataset)
7471
7572
# Get as pydantic-evals Dataset
7673
dataset = client.get_dataset(\'qa-dataset\', MyInput, MyOutput)
@@ -93,7 +90,7 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
9390
Returns:
9491
List of dataset summaries with id, name, description, case_count, etc.
9592
"""
96-
def create_dataset(self, name: str, *, input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = None, guidance: str | None = None, ai_managed_guidance: bool = False) -> dict[str, Any]:
93+
def create_dataset(self, name: str, *, input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = None) -> dict[str, Any]:
9794
'''Create a new dataset with optional type schemas.
9895
9996
Args:
@@ -102,8 +99,6 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
10299
output_type: Type for expected outputs. JSON schema will be generated from this type.
103100
metadata_type: Type for case metadata. JSON schema will be generated from this type.
104101
description: Optional description of the dataset.
105-
guidance: Instructions for AI-assisted population.
106-
ai_managed_guidance: Whether guidance is managed by AI.
107102
108103
Returns:
109104
The created dataset.
@@ -130,7 +125,7 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
130125
)
131126
```
132127
'''
133-
def update_dataset(self, id_or_name: str, *, name: str = ..., input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = ..., guidance: str | None = ..., ai_managed_guidance: bool | None = None) -> dict[str, Any]:
128+
def update_dataset(self, id_or_name: str, *, name: str = ..., input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = ...) -> dict[str, Any]:
134129
"""Update an existing dataset.
135130
136131
Args:
@@ -140,8 +135,6 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
140135
output_type: New output type (generates schema).
141136
metadata_type: New metadata type (generates schema).
142137
description: New description. Pass None to clear.
143-
guidance: New guidance instructions. Pass None to clear.
144-
ai_managed_guidance: Whether guidance is managed by AI.
145138
146139
Returns:
147140
The updated dataset.
@@ -158,12 +151,11 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
158151
Raises:
159152
DatasetNotFoundError: If the dataset does not exist.
160153
"""
161-
def list_cases(self, dataset_id_or_name: str, *, tags: list[str] | None = None) -> list[dict[str, Any]]:
154+
def list_cases(self, dataset_id_or_name: str) -> list[dict[str, Any]]:
162155
"""List all cases in a dataset.
163156
164157
Args:
165158
dataset_id_or_name: The dataset ID (UUID) or name.
166-
tags: Optional list of tags to filter cases by.
167159
168160
Returns:
169161
List of cases with full details.
@@ -185,7 +177,79 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
185177
DatasetNotFoundError: If the dataset does not exist.
186178
CaseNotFoundError: If the case does not exist.
187179
"""
188-
def add_cases(self, dataset_id_or_name: str, cases: Sequence[Case[InputsT, OutputT, MetadataT]] | Sequence[dict[str, Any]], *, tags: list[str] | None = None, on_conflict: str = 'update') -> list[dict[str, Any]]:
180+
def push_dataset(self, dataset: Dataset[InputsT, OutputT, MetadataT], *, name: str | None = None, description: str | None = ..., on_case_conflict: Literal['update', 'error'] = 'update') -> dict[str, Any]:
181+
'''Publish a local `pydantic_evals.Dataset` to the hosted datasets API.
182+
183+
This is the high-level "push my dataset to hosted" helper. It creates a
184+
hosted dataset when one does not exist yet, updates the hosted dataset
185+
when one already exists with the same name, uploads all local cases
186+
through the existing import/upsert API, and finally returns hosted
187+
dataset metadata.
188+
189+
The JSON schemas for inputs, expected outputs, and metadata are
190+
inferred from the `Dataset[InputsT, OutputT, MetadataT]` generic
191+
parameters of the dataset you pass in — instantiate your local dataset
192+
with the types you want hosted.
193+
194+
Args:
195+
dataset: The local `pydantic_evals.Dataset` to publish. Case-level
196+
evaluators are uploaded with their cases. Dataset-level
197+
`evaluators` and `report_evaluators` are not supported yet and
198+
will raise `ValueError`.
199+
name: Optional hosted dataset name override. Defaults to
200+
`dataset.name`.
201+
description: Hosted dataset description. Omit this argument to leave
202+
the existing description unchanged when updating an existing
203+
dataset. Pass `None` to clear the description on update. On
204+
initial create, `None` means no description is set.
205+
on_case_conflict: Conflict behavior for uploaded cases. The default
206+
`\'update\'` makes repeated pushes idempotent for named cases.
207+
Pass `\'error\'` to fail instead of updating an existing case with
208+
the same name.
209+
210+
Returns:
211+
Hosted dataset metadata as returned by
212+
`get_dataset(..., include_cases=False)`.
213+
214+
Raises:
215+
ValueError: If neither `dataset.name` nor `name` is provided, or if
216+
the dataset contains unsupported dataset-level evaluators.
217+
DatasetApiError: If the API returns an error other than the expected
218+
`409` conflict used to trigger an update flow.
219+
DatasetNotFoundError: If the hosted dataset cannot be fetched after
220+
the push completes.
221+
222+
Example:
223+
```python skip-run="true" skip-reason="external-connection"
224+
from dataclasses import dataclass
225+
226+
from pydantic_evals import Case, Dataset
227+
228+
229+
@dataclass
230+
class MyInput:
231+
question: str
232+
233+
234+
@dataclass
235+
class MyOutput:
236+
answer: str
237+
238+
239+
local_dataset = Dataset[MyInput, MyOutput, None](
240+
name=\'qa-dataset\',
241+
cases=[
242+
Case(name=\'q1\', inputs=MyInput(\'Hello?\'), expected_output=MyOutput(\'Hi!\')),
243+
],
244+
)
245+
246+
dataset_info = client.push_dataset(
247+
local_dataset,
248+
description=\'Golden test cases for the Q&A task\',
249+
)
250+
```
251+
'''
252+
def add_cases(self, dataset_id_or_name: str, cases: Sequence[Case[InputsT, OutputT, MetadataT]] | Sequence[dict[str, Any]], *, on_conflict: str = 'update') -> list[dict[str, Any]]:
189253
'''Add cases to a dataset.
190254
191255
Accepts either pydantic-evals Case objects or plain dicts.
@@ -197,7 +261,6 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
197261
Args:
198262
dataset_id_or_name: The dataset ID (UUID) or name.
199263
cases: A sequence of pydantic-evals Case objects or dicts.
200-
tags: Optional list of tags to associate with all cases.
201264
on_conflict: Conflict resolution strategy: `\'update\'` (default) to
202265
upsert cases with matching names, or `\'error\'` to fail on conflicts.
203266
@@ -220,7 +283,7 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
220283
)
221284
```
222285
'''
223-
def update_case(self, dataset_id_or_name: str, case_id: str, *, name: str | None = ..., inputs: Any | None = None, expected_output: Any | None = ..., metadata: Any | None = ..., evaluators: Sequence[Evaluator[Any, Any, Any]] | None = ..., tags: list[str] | None = ...) -> dict[str, Any]:
286+
def update_case(self, dataset_id_or_name: str, case_id: str, *, name: str | None = ..., inputs: Any | None = None, expected_output: Any | None = ..., metadata: Any | None = ..., evaluators: Sequence[Evaluator[Any, Any, Any]] | None = ...) -> dict[str, Any]:
224287
"""Update an existing case.
225288
226289
Args:
@@ -231,7 +294,6 @@ class LogfireAPIClient(_BaseLogfireAPIClient[Client]):
231294
expected_output: New expected output (dict or typed object). Pass None to clear.
232295
metadata: New metadata (dict or typed object). Pass None to clear.
233296
evaluators: New evaluators. Pass None to clear.
234-
tags: New tags for the case. Pass None to clear.
235297
236298
Returns:
237299
The updated case.
@@ -266,17 +328,48 @@ class AsyncLogfireAPIClient(_BaseLogfireAPIClient[AsyncClient]):
266328
async def __aexit__(self, exc_type: type[BaseException] | None = None, exc_value: BaseException | None = None, traceback: TracebackType | None = None) -> None: ...
267329
async def list_datasets(self) -> list[dict[str, Any]]:
268330
"""List all datasets."""
269-
async def create_dataset(self, name: str, *, input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = None, guidance: str | None = None, ai_managed_guidance: bool = False) -> dict[str, Any]:
331+
async def create_dataset(self, name: str, *, input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = None) -> dict[str, Any]:
270332
"""Create a new dataset."""
271-
async def update_dataset(self, id_or_name: str, *, name: str = ..., input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = ..., guidance: str | None = ..., ai_managed_guidance: bool | None = None) -> dict[str, Any]:
333+
async def update_dataset(self, id_or_name: str, *, name: str = ..., input_type: type[Any] | None = None, output_type: type[Any] | None = None, metadata_type: type[Any] | None = None, description: str | None = ...) -> dict[str, Any]:
272334
"""Update an existing dataset."""
273335
async def delete_dataset(self, id_or_name: str) -> None:
274336
"""Delete a dataset."""
275-
async def list_cases(self, dataset_id_or_name: str, *, tags: list[str] | None = None) -> list[dict[str, Any]]:
337+
async def list_cases(self, dataset_id_or_name: str) -> list[dict[str, Any]]:
276338
"""List all cases in a dataset."""
277339
async def get_case(self, dataset_id_or_name: str, case_id: str) -> dict[str, Any]:
278340
"""Get a specific case from a dataset."""
279-
async def add_cases(self, dataset_id_or_name: str, cases: Sequence[Case[InputsT, OutputT, MetadataT]] | Sequence[dict[str, Any]], *, tags: list[str] | None = None, on_conflict: str = 'update') -> list[dict[str, Any]]:
341+
async def push_dataset(self, dataset: Dataset[InputsT, OutputT, MetadataT], *, name: str | None = None, description: str | None = ..., on_case_conflict: Literal['update', 'error'] = 'update') -> dict[str, Any]:
342+
"""Async version of `LogfireAPIClient.push_dataset`.
343+
344+
Args:
345+
dataset: The local `pydantic_evals.Dataset` to publish. Case-level
346+
evaluators are uploaded with their cases. Dataset-level
347+
`evaluators` and `report_evaluators` are not supported yet and
348+
will raise `ValueError`.
349+
name: Optional hosted dataset name override. Defaults to
350+
`dataset.name`.
351+
description: Hosted dataset description. Omit this argument to leave
352+
the existing description unchanged when updating an existing
353+
dataset. Pass `None` to clear the description on update. On
354+
initial create, `None` means no description is set.
355+
on_case_conflict: Conflict behavior for uploaded cases. The default
356+
`'update'` makes repeated pushes idempotent for named cases.
357+
Pass `'error'` to fail instead of updating an existing case with
358+
the same name.
359+
360+
Returns:
361+
Hosted dataset metadata as returned by
362+
`get_dataset(..., include_cases=False)`.
363+
364+
Raises:
365+
ValueError: If neither `dataset.name` nor `name` is provided, or if
366+
the dataset contains unsupported dataset-level evaluators.
367+
DatasetApiError: If the API returns an error other than the expected
368+
`409` conflict used to trigger an update flow.
369+
DatasetNotFoundError: If the hosted dataset cannot be fetched after
370+
the push completes.
371+
"""
372+
async def add_cases(self, dataset_id_or_name: str, cases: Sequence[Case[InputsT, OutputT, MetadataT]] | Sequence[dict[str, Any]], *, on_conflict: str = 'update') -> list[dict[str, Any]]:
280373
"""Add cases to a dataset.
281374
282375
Accepts either pydantic-evals Case objects or plain dicts.
@@ -285,7 +378,7 @@ class AsyncLogfireAPIClient(_BaseLogfireAPIClient[AsyncClient]):
285378
case in the dataset are updated; cases without a name or with a new name
286379
are created. Set `on_conflict='error'` to fail on name conflicts instead.
287380
"""
288-
async def update_case(self, dataset_id_or_name: str, case_id: str, *, name: str | None = ..., inputs: Any | None = None, expected_output: Any | None = ..., metadata: Any | None = ..., evaluators: Sequence[Evaluator[Any, Any, Any]] | None = ..., tags: list[str] | None = ...) -> dict[str, Any]:
381+
async def update_case(self, dataset_id_or_name: str, case_id: str, *, name: str | None = ..., inputs: Any | None = None, expected_output: Any | None = ..., metadata: Any | None = ..., evaluators: Sequence[Evaluator[Any, Any, Any]] | None = ...) -> dict[str, Any]:
289382
"""Update an existing case."""
290383
async def delete_case(self, dataset_id_or_name: str, case_id: str) -> None:
291384
"""Delete a case from a dataset."""

logfire-api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "logfire-api"
7-
version = "4.31.2"
7+
version = "4.32.0"
88
description = "Shim for the Logfire SDK which does nothing unless Logfire is installed"
99
authors = [
1010
{ name = "Pydantic Team", email = "engineering@pydantic.dev" },

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "logfire"
7-
version = "4.31.2"
7+
version = "4.32.0"
88
description = "The best Python observability tool! 🪵🔥"
99
requires-python = ">=3.9"
1010
authors = [

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)