Skip to content

Commit 9062bf9

Browse files
Simon Stieberclaude
andcommitted
fix: add wrapper methods for dataset runs with proper URL encoding
Fixes langfuse/langfuse#10184 Added wrapper methods in the Langfuse client that properly URL-encode dataset and run names before passing to the Fern API client: - get_dataset_run(): Fetch a specific dataset run by name - get_dataset_runs(): Fetch all runs for a dataset - delete_dataset_run(): Delete a dataset run This enables support for folder-format dataset names containing slashes (e.g., "folder/subfolder/dataset") which previously returned 404 errors due to double URL encoding. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 25c5ef9 commit 9062bf9

File tree

2 files changed

+245
-0
lines changed

2 files changed

+245
-0
lines changed

langfuse/_client/client.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@
8080
from langfuse._utils.prompt_cache import PromptCache
8181
from langfuse.api.resources.commons.errors.error import Error
8282
from langfuse.api.resources.commons.errors.not_found_error import NotFoundError
83+
from langfuse.api.resources.commons.types.dataset_run_with_items import (
84+
DatasetRunWithItems,
85+
)
86+
from langfuse.api.resources.datasets.types.delete_dataset_run_response import (
87+
DeleteDatasetRunResponse,
88+
)
89+
from langfuse.api.resources.datasets.types.paginated_dataset_runs import (
90+
PaginatedDatasetRuns,
91+
)
8392
from langfuse.api.resources.ingestion.types.score_body import ScoreBody
8493
from langfuse.api.resources.prompts.types import (
8594
CreatePromptRequest_Chat,
@@ -2461,6 +2470,128 @@ def get_dataset(
24612470
handle_fern_exception(e)
24622471
raise e
24632472

2473+
def get_dataset_run(
2474+
self,
2475+
dataset_name: str,
2476+
run_name: str,
2477+
) -> DatasetRunWithItems:
2478+
"""Fetch a dataset run by dataset name and run name.
2479+
2480+
This method properly URL-encodes the dataset and run names, supporting
2481+
folder-format names that contain slashes (e.g., "folder/subfolder/dataset").
2482+
2483+
Args:
2484+
dataset_name: The name of the dataset (can include slashes for folder structure).
2485+
run_name: The name of the run to fetch (can include slashes).
2486+
2487+
Returns:
2488+
DatasetRunWithItems: The dataset run with its items.
2489+
2490+
Raises:
2491+
NotFoundError: If the dataset or run is not found.
2492+
2493+
Example:
2494+
```python
2495+
# Fetch a run from a dataset in a folder
2496+
run = langfuse.get_dataset_run(
2497+
dataset_name="evaluation/qa-dataset",
2498+
run_name="experiment-1"
2499+
)
2500+
print(f"Run has {len(run.dataset_run_items)} items")
2501+
```
2502+
"""
2503+
try:
2504+
langfuse_logger.debug(f"Getting dataset run {dataset_name}/{run_name}")
2505+
return self.api.datasets.get_run(
2506+
dataset_name=self._url_encode(dataset_name),
2507+
run_name=self._url_encode(run_name),
2508+
)
2509+
except Error as e:
2510+
handle_fern_exception(e)
2511+
raise e
2512+
2513+
def get_dataset_runs(
2514+
self,
2515+
dataset_name: str,
2516+
*,
2517+
page: Optional[int] = None,
2518+
limit: Optional[int] = None,
2519+
) -> PaginatedDatasetRuns:
2520+
"""Fetch all runs for a dataset.
2521+
2522+
This method properly URL-encodes the dataset name, supporting
2523+
folder-format names that contain slashes (e.g., "folder/subfolder/dataset").
2524+
2525+
Args:
2526+
dataset_name: The name of the dataset (can include slashes for folder structure).
2527+
page: Optional page number for pagination (starts at 1).
2528+
limit: Optional limit of items per page.
2529+
2530+
Returns:
2531+
PaginatedDatasetRuns: Paginated list of dataset runs.
2532+
2533+
Raises:
2534+
NotFoundError: If the dataset is not found.
2535+
2536+
Example:
2537+
```python
2538+
# Get all runs for a dataset in a folder
2539+
runs = langfuse.get_dataset_runs("evaluation/qa-dataset")
2540+
for run in runs.data:
2541+
print(f"Run: {run.name}")
2542+
```
2543+
"""
2544+
try:
2545+
langfuse_logger.debug(f"Getting dataset runs for {dataset_name}")
2546+
return self.api.datasets.get_runs(
2547+
dataset_name=self._url_encode(dataset_name),
2548+
page=page,
2549+
limit=limit,
2550+
)
2551+
except Error as e:
2552+
handle_fern_exception(e)
2553+
raise e
2554+
2555+
def delete_dataset_run(
2556+
self,
2557+
dataset_name: str,
2558+
run_name: str,
2559+
) -> DeleteDatasetRunResponse:
2560+
"""Delete a dataset run and all its run items.
2561+
2562+
This action is irreversible. This method properly URL-encodes the dataset
2563+
and run names, supporting folder-format names that contain slashes.
2564+
2565+
Args:
2566+
dataset_name: The name of the dataset (can include slashes for folder structure).
2567+
run_name: The name of the run to delete (can include slashes).
2568+
2569+
Returns:
2570+
DeleteDatasetRunResponse: Confirmation of the deletion.
2571+
2572+
Raises:
2573+
NotFoundError: If the dataset or run is not found.
2574+
2575+
Example:
2576+
```python
2577+
# Delete a run from a dataset in a folder
2578+
result = langfuse.delete_dataset_run(
2579+
dataset_name="evaluation/qa-dataset",
2580+
run_name="old-experiment"
2581+
)
2582+
print(result.message) # "Dataset run deleted successfully"
2583+
```
2584+
"""
2585+
try:
2586+
langfuse_logger.debug(f"Deleting dataset run {dataset_name}/{run_name}")
2587+
return self.api.datasets.delete_run(
2588+
dataset_name=self._url_encode(dataset_name),
2589+
run_name=self._url_encode(run_name),
2590+
)
2591+
except Error as e:
2592+
handle_fern_exception(e)
2593+
raise e
2594+
24642595
def run_experiment(
24652596
self,
24662597
*,

tests/test_datasets.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,3 +419,117 @@ def execute_dataset_item(item, run_name):
419419
assert "args" in trace.input
420420
assert trace.input["args"][0] == expected_input
421421
assert trace.output == expected_input
422+
423+
424+
def test_get_dataset_with_folder_name():
425+
"""Test that get_dataset works with folder-format names containing slashes."""
426+
langfuse = Langfuse(debug=False)
427+
428+
# Create a dataset with slashes in the name (folder format)
429+
folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}"
430+
langfuse.create_dataset(name=folder_name)
431+
432+
# Fetch the dataset using the wrapper method
433+
dataset = langfuse.get_dataset(folder_name)
434+
435+
assert dataset.name == folder_name
436+
assert "/" in dataset.name # Verify slashes are preserved
437+
438+
439+
def test_get_dataset_runs_with_folder_name():
440+
"""Test that get_dataset_runs works with folder-format dataset names."""
441+
langfuse = Langfuse(debug=False)
442+
443+
# Create a dataset with slashes in the name
444+
folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}"
445+
langfuse.create_dataset(name=folder_name)
446+
447+
# Create a dataset item
448+
langfuse.create_dataset_item(dataset_name=folder_name, input={"test": "data"})
449+
450+
dataset = langfuse.get_dataset(folder_name)
451+
assert len(dataset.items) == 1
452+
453+
# Create a run
454+
run_name = f"run-{create_uuid()[:8]}"
455+
for item in dataset.items:
456+
with item.run(run_name=run_name):
457+
pass
458+
459+
langfuse.flush()
460+
time.sleep(1) # Give API time to process
461+
462+
# Fetch runs using the new wrapper method
463+
runs = langfuse.get_dataset_runs(folder_name)
464+
465+
assert len(runs.data) == 1
466+
assert runs.data[0].name == run_name
467+
468+
469+
def test_get_dataset_run_with_folder_names():
470+
"""Test that get_dataset_run works with folder-format dataset and run names."""
471+
langfuse = Langfuse(debug=False)
472+
473+
# Create a dataset with slashes in the name
474+
folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}"
475+
langfuse.create_dataset(name=folder_name)
476+
477+
# Create a dataset item
478+
langfuse.create_dataset_item(dataset_name=folder_name, input={"test": "data"})
479+
480+
dataset = langfuse.get_dataset(folder_name)
481+
assert len(dataset.items) == 1
482+
483+
# Create a run with slashes in the name
484+
run_name = f"run/nested/{create_uuid()[:8]}"
485+
for item in dataset.items:
486+
with item.run(run_name=run_name, run_metadata={"key": "value"}):
487+
pass
488+
489+
langfuse.flush()
490+
time.sleep(1) # Give API time to process
491+
492+
# Fetch the specific run using the new wrapper method
493+
run = langfuse.get_dataset_run(folder_name, run_name)
494+
495+
assert run.name == run_name
496+
assert run.metadata == {"key": "value"}
497+
assert "/" in folder_name # Verify slashes are preserved
498+
499+
500+
def test_delete_dataset_run_with_folder_names():
501+
"""Test that delete_dataset_run works with folder-format dataset and run names."""
502+
langfuse = Langfuse(debug=False)
503+
504+
# Create a dataset with slashes in the name
505+
folder_name = f"folder/subfolder/dataset-{create_uuid()[:8]}"
506+
langfuse.create_dataset(name=folder_name)
507+
508+
# Create a dataset item
509+
langfuse.create_dataset_item(dataset_name=folder_name, input={"test": "data"})
510+
511+
dataset = langfuse.get_dataset(folder_name)
512+
513+
# Create a run with slashes in the name
514+
run_name = f"run/to/delete/{create_uuid()[:8]}"
515+
for item in dataset.items:
516+
with item.run(run_name=run_name):
517+
pass
518+
519+
langfuse.flush()
520+
time.sleep(1) # Give API time to process
521+
522+
# Verify the run exists
523+
runs_before = langfuse.get_dataset_runs(folder_name)
524+
assert len(runs_before.data) == 1
525+
526+
# Delete the run using the new wrapper method
527+
result = langfuse.delete_dataset_run(folder_name, run_name)
528+
529+
assert result.message is not None
530+
531+
time.sleep(1) # Give API time to process deletion
532+
533+
# Verify the run is deleted
534+
runs_after = langfuse.get_dataset_runs(folder_name)
535+
assert len(runs_after.data) == 0

0 commit comments

Comments
 (0)