Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-langfuse for openSUSE:Factory
checked in at 2026-04-20 16:12:30
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langfuse (Old)
and /work/SRC/openSUSE:Factory/.python-langfuse.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-langfuse"
Mon Apr 20 16:12:30 2026 rev:7 rq:1348121 version:4.3.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-langfuse/python-langfuse.changes
2026-04-14 17:49:09.025600360 +0200
+++
/work/SRC/openSUSE:Factory/.python-langfuse.new.11940/python-langfuse.changes
2026-04-20 16:12:44.040705721 +0200
@@ -1,0 +2,14 @@
+Mon Apr 20 07:06:19 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.3.1:
+ * feat(api): update API spec from langfuse/langfuse 07cae52
+ * chore(deps): bump actions/github-script from 8.0.0 to 9.0.0
+ in the github-actions group
+ * refactor(tests): split suites by execution level and speed up
+ CI
+ * ci: add 7-day dependabot cooldown
+ * ci: harden GitHub Actions workflows with zizmor
+ * ci: make uv action tag explicit
+ * fix(langchain): propagate trace name metadata
+
+-------------------------------------------------------------------
Old:
----
langfuse-4.2.0.tar.gz
New:
----
langfuse-4.3.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-langfuse.spec ++++++
--- /var/tmp/diff_new_pack.v09ECO/_old 2026-04-20 16:12:44.840739130 +0200
+++ /var/tmp/diff_new_pack.v09ECO/_new 2026-04-20 16:12:44.840739130 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-langfuse
-Version: 4.2.0
+Version: 4.3.1
Release: 0
Summary: A client library for accessing langfuse
License: MIT
++++++ langfuse-4.2.0.tar.gz -> langfuse-4.3.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langfuse-4.2.0/PKG-INFO new/langfuse-4.3.1/PKG-INFO
--- old/langfuse-4.2.0/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/PKG-INFO 1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: langfuse
-Version: 4.2.0
+Version: 4.3.1
Summary: A client library for accessing langfuse
Author: langfuse
Author-email: langfuse <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langfuse-4.2.0/langfuse/_client/client.py
new/langfuse-4.3.1/langfuse/_client/client.py
--- old/langfuse-4.2.0/langfuse/_client/client.py 1970-01-01
01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/_client/client.py 1970-01-01
01:00:00.000000000 +0100
@@ -3486,8 +3486,9 @@
fetch_timeout_seconds=fetch_timeout_seconds,
)
- self._resources.prompt_cache.add_refresh_prompt_task(
+
self._resources.prompt_cache.add_refresh_prompt_task_if_current(
cache_key,
+ cached_prompt,
refresh_task,
)
langfuse_logger.debug(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langfuse-4.2.0/langfuse/_client/resource_manager.py
new/langfuse-4.3.1/langfuse/_client/resource_manager.py
--- old/langfuse-4.2.0/langfuse/_client/resource_manager.py 1970-01-01
01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/_client/resource_manager.py 1970-01-01
01:00:00.000000000 +0100
@@ -405,6 +405,8 @@
for media_upload_consumer in self._media_upload_consumers:
media_upload_consumer.pause()
+
self._media_manager.signal_shutdown(count=len(self._media_upload_consumers))
+
for media_upload_consumer in self._media_upload_consumers:
try:
media_upload_consumer.join()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langfuse-4.2.0/langfuse/_task_manager/media_manager.py
new/langfuse-4.3.1/langfuse/_task_manager/media_manager.py
--- old/langfuse-4.2.0/langfuse/_task_manager/media_manager.py 1970-01-01
01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/_task_manager/media_manager.py 1970-01-01
01:00:00.000000000 +0100
@@ -18,6 +18,7 @@
T = TypeVar("T")
P = ParamSpec("P")
+_SHUTDOWN_SENTINEL = object()
class MediaManager:
@@ -40,6 +41,11 @@
def process_next_media_upload(self) -> None:
try:
upload_job = self._queue.get(block=True, timeout=1)
+
+ if upload_job is _SHUTDOWN_SENTINEL:
+ self._queue.task_done()
+ return
+
logger.debug(
f"Media: Processing upload for
media_id={upload_job['media_id']} in trace_id={upload_job['trace_id']}"
)
@@ -54,6 +60,15 @@
)
self._queue.task_done()
+ def signal_shutdown(self, *, count: int = 1) -> None:
+ for _ in range(count):
+ try:
+ self._queue.put(_SHUTDOWN_SENTINEL, block=False)
+ except Full:
+ # If the queue is full, the consumer will keep draining work
and
+ # observe the paused flag on the next loop iteration.
+ break
+
def _find_and_process_media(
self,
*,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langfuse-4.2.0/langfuse/_task_manager/score_ingestion_consumer.py
new/langfuse-4.3.1/langfuse/_task_manager/score_ingestion_consumer.py
--- old/langfuse-4.2.0/langfuse/_task_manager/score_ingestion_consumer.py
1970-01-01 01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/_task_manager/score_ingestion_consumer.py
1970-01-01 01:00:00.000000000 +0100
@@ -2,7 +2,7 @@
import os
import threading
import time
-from queue import Empty, Queue
+from queue import Empty, Full, Queue
from typing import Any, List, Optional
import backoff
@@ -17,6 +17,7 @@
MAX_EVENT_SIZE_BYTES = int(os.environ.get("LANGFUSE_MAX_EVENT_SIZE_BYTES",
1_000_000))
MAX_BATCH_SIZE_BYTES = int(os.environ.get("LANGFUSE_MAX_BATCH_SIZE_BYTES",
2_500_000))
+_SHUTDOWN_SENTINEL = object()
class ScoreIngestionMetadata(BaseModel):
@@ -71,6 +72,10 @@
block=True, timeout=self._flush_interval - elapsed
)
+ if event is _SHUTDOWN_SENTINEL:
+ self._ingestion_queue.task_done()
+ break
+
# convert pydantic models to dicts
if "body" in event and isinstance(event["body"], BaseModel):
event["body"] = event["body"].model_dump(exclude_none=True)
@@ -139,6 +144,12 @@
def pause(self) -> None:
"""Pause the consumer."""
self.running = False
+ try:
+ self._ingestion_queue.put(_SHUTDOWN_SENTINEL, block=False)
+ except Full:
+ # If the queue is full, the consumer will wake up naturally while
+ # draining items, so a dedicated shutdown signal is not required.
+ pass
def _upload_batch(self, batch: List[Any]) -> None:
logger.debug(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langfuse-4.2.0/langfuse/_utils/prompt_cache.py
new/langfuse-4.3.1/langfuse/_utils/prompt_cache.py
--- old/langfuse-4.2.0/langfuse/_utils/prompt_cache.py 1970-01-01
01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/_utils/prompt_cache.py 1970-01-01
01:00:00.000000000 +0100
@@ -3,8 +3,8 @@
import atexit
import os
from datetime import datetime
-from queue import Empty, Queue
-from threading import Thread
+from queue import Queue
+from threading import RLock, Thread
from typing import Callable, Dict, List, Optional, Set
from langfuse._client.environment_variables import (
@@ -18,6 +18,7 @@
)
DEFAULT_PROMPT_CACHE_REFRESH_WORKERS = 1
+_SHUTDOWN_SENTINEL = object()
class PromptCacheItem:
@@ -46,22 +47,24 @@
def run(self) -> None:
while self.running:
+ task = self._queue.get()
+
+ if task is _SHUTDOWN_SENTINEL:
+ self._queue.task_done()
+ break
+
+ logger.debug(
+ f"PromptCacheRefreshConsumer processing task,
{self._identifier}"
+ )
try:
- task = self._queue.get(timeout=1)
- logger.debug(
- f"PromptCacheRefreshConsumer processing task,
{self._identifier}"
+ task()
+ # Task failed, but we still consider it processed
+ except Exception as e:
+ logger.warning(
+ f"PromptCacheRefreshConsumer encountered an error, cache
was not refreshed: {self._identifier}, {e}"
)
- try:
- task()
- # Task failed, but we still consider it processed
- except Exception as e:
- logger.warning(
- f"PromptCacheRefreshConsumer encountered an error,
cache was not refreshed: {self._identifier}, {e}"
- )
- self._queue.task_done()
- except Empty:
- pass
+ self._queue.task_done()
def pause(self) -> None:
"""Pause the consumer."""
@@ -73,12 +76,14 @@
_threads: int
_queue: Queue
_processing_keys: Set[str]
+ _lock: RLock
def __init__(self, threads: int = 1):
self._queue = Queue()
self._consumers = []
self._threads = threads
self._processing_keys = set()
+ self._lock = RLock()
for i in range(self._threads):
consumer = PromptCacheRefreshConsumer(self._queue, i)
@@ -88,16 +93,23 @@
atexit.register(self.shutdown)
def add_task(self, key: str, task: Callable[[], None]) -> None:
- if key not in self._processing_keys:
- logger.debug(f"Adding prompt cache refresh task for key: {key}")
- self._processing_keys.add(key)
- wrapped_task = self._wrap_task(key, task)
- self._queue.put((wrapped_task))
- else:
- logger.debug(f"Prompt cache refresh task already submitted for
key: {key}")
+ with self._lock:
+ if key not in self._processing_keys:
+ logger.debug(f"Adding prompt cache refresh task for key:
{key}")
+ self._processing_keys.add(key)
+ wrapped_task = self._wrap_task(key, task)
+ self._queue.put((wrapped_task))
+ else:
+ logger.debug(
+ f"Prompt cache refresh task already submitted for key:
{key}"
+ )
def active_tasks(self) -> int:
- return len(self._processing_keys)
+ with self._lock:
+ return len(self._processing_keys)
+
+ def wait_for_idle(self) -> None:
+ self._queue.join()
def _wrap_task(self, key: str, task: Callable[[], None]) -> Callable[[],
None]:
def wrapped() -> None:
@@ -105,7 +117,8 @@
try:
task()
finally:
- self._processing_keys.remove(key)
+ with self._lock:
+ self._processing_keys.remove(key)
logger.debug(f"Refreshed prompt cache for key: {key}")
return wrapped
@@ -120,6 +133,9 @@
for consumer in self._consumers:
consumer.pause()
+ for _ in self._consumers:
+ self._queue.put(_SHUTDOWN_SENTINEL)
+
for consumer in self._consumers:
try:
consumer.join()
@@ -132,6 +148,7 @@
class PromptCache:
_cache: Dict[str, PromptCacheItem]
+ _lock: RLock
_task_manager: PromptCacheTaskManager
"""Task manager for refreshing cache"""
@@ -140,34 +157,60 @@
self, max_prompt_refresh_workers: int =
DEFAULT_PROMPT_CACHE_REFRESH_WORKERS
):
self._cache = {}
+ self._lock = RLock()
self._task_manager =
PromptCacheTaskManager(threads=max_prompt_refresh_workers)
logger.debug("Prompt cache initialized.")
def get(self, key: str) -> Optional[PromptCacheItem]:
- return self._cache.get(key, None)
+ with self._lock:
+ return self._cache.get(key, None)
def set(self, key: str, value: PromptClient, ttl_seconds: Optional[int])
-> None:
if ttl_seconds is None:
ttl_seconds = DEFAULT_PROMPT_CACHE_TTL_SECONDS
- self._cache[key] = PromptCacheItem(value, ttl_seconds)
+ with self._lock:
+ self._cache[key] = PromptCacheItem(value, ttl_seconds)
def delete(self, key: str) -> None:
- self._cache.pop(key, None)
+ with self._lock:
+ self._cache.pop(key, None)
def invalidate(self, prompt_name: str) -> None:
"""Invalidate all cached prompts with the given prompt name."""
- for key in list(self._cache):
- if key.startswith(prompt_name):
- del self._cache[key]
+ with self._lock:
+ for key in list(self._cache):
+ if key.startswith(prompt_name):
+ del self._cache[key]
def add_refresh_prompt_task(self, key: str, fetch_func: Callable[[],
None]) -> None:
logger.debug(f"Submitting refresh task for key: {key}")
self._task_manager.add_task(key, fetch_func)
+ def add_refresh_prompt_task_if_current(
+ self,
+ key: str,
+ expected_item: PromptCacheItem,
+ fetch_func: Callable[[], None],
+ ) -> None:
+ with self._lock:
+ current_item = self._cache.get(key)
+ if (
+ current_item is not None
+ and current_item is not expected_item
+ and not current_item.is_expired()
+ ):
+ logger.debug(
+ f"Skipping refresh task for key: {key} because cache is
already fresh."
+ )
+ return
+
+ self.add_refresh_prompt_task(key, fetch_func)
+
def clear(self) -> None:
"""Clear the entire prompt cache, removing all cached prompts."""
- self._cache.clear()
+ with self._lock:
+ self._cache.clear()
@staticmethod
def generate_cache_key(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langfuse-4.2.0/langfuse/api/blob_storage_integrations/client.py
new/langfuse-4.3.1/langfuse/api/blob_storage_integrations/client.py
--- old/langfuse-4.2.0/langfuse/api/blob_storage_integrations/client.py
1970-01-01 01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/api/blob_storage_integrations/client.py
1970-01-01 01:00:00.000000000 +0100
@@ -108,7 +108,7 @@
type : BlobStorageIntegrationType
bucket_name : str
- Name of the storage bucket
+ Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a
valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens
only, must start and end with a letter or number, no consecutive hyphens).
region : str
Storage region
@@ -367,7 +367,7 @@
type : BlobStorageIntegrationType
bucket_name : str
- Name of the storage bucket
+ Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a
valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens
only, must start and end with a letter or number, no consecutive hyphens).
region : str
Storage region
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langfuse-4.2.0/langfuse/api/blob_storage_integrations/raw_client.py
new/langfuse-4.3.1/langfuse/api/blob_storage_integrations/raw_client.py
--- old/langfuse-4.2.0/langfuse/api/blob_storage_integrations/raw_client.py
1970-01-01 01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/api/blob_storage_integrations/raw_client.py
1970-01-01 01:00:00.000000000 +0100
@@ -165,7 +165,7 @@
type : BlobStorageIntegrationType
bucket_name : str
- Name of the storage bucket
+ Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a
valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens
only, must start and end with a letter or number, no consecutive hyphens).
region : str
Storage region
@@ -642,7 +642,7 @@
type : BlobStorageIntegrationType
bucket_name : str
- Name of the storage bucket
+ Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a
valid Azure container name (3-63 chars, lowercase letters, numbers, and hyphens
only, must start and end with a letter or number, no consecutive hyphens).
region : str
Storage region
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langfuse-4.2.0/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py
new/langfuse-4.3.1/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py
---
old/langfuse-4.2.0/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py
1970-01-01 01:00:00.000000000 +0100
+++
new/langfuse-4.3.1/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py
1970-01-01 01:00:00.000000000 +0100
@@ -26,7 +26,7 @@
pydantic.Field()
)
"""
- Name of the storage bucket
+ Name of the storage bucket. For AZURE_BLOB_STORAGE, must be a valid Azure
container name (3-63 chars, lowercase letters, numbers, and hyphens only, must
start and end with a letter or number, no consecutive hyphens).
"""
endpoint: typing.Optional[str] = pydantic.Field(default=None)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langfuse-4.2.0/langfuse/langchain/CallbackHandler.py
new/langfuse-4.3.1/langfuse/langchain/CallbackHandler.py
--- old/langfuse-4.2.0/langfuse/langchain/CallbackHandler.py 1970-01-01
01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/langfuse/langchain/CallbackHandler.py 1970-01-01
01:00:00.000000000 +0100
@@ -287,6 +287,11 @@
):
attributes["user_id"] = metadata["langfuse_user_id"]
+ if "langfuse_trace_name" in metadata and isinstance(
+ metadata["langfuse_trace_name"], str
+ ):
+ attributes["trace_name"] = metadata["langfuse_trace_name"]
+
if tags is not None or (
"langfuse_tags" in metadata and
isinstance(metadata["langfuse_tags"], list)
):
@@ -369,6 +374,7 @@
session_id=parsed_trace_attributes.get("session_id", None),
tags=parsed_trace_attributes.get("tags", None),
metadata=parsed_trace_attributes.get("metadata", None),
+ trace_name=parsed_trace_attributes.get("trace_name", None),
)
self._propagation_context_manager.__enter__()
@@ -1403,6 +1409,7 @@
"langfuse_session_id",
"langfuse_user_id",
"langfuse_tags",
+ "langfuse_trace_name",
]
metadata_copy = metadata.copy()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langfuse-4.2.0/pyproject.toml
new/langfuse-4.3.1/pyproject.toml
--- old/langfuse-4.2.0/pyproject.toml 1970-01-01 01:00:00.000000000 +0100
+++ new/langfuse-4.3.1/pyproject.toml 1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
[project]
name = "langfuse"
-version = "4.2.0"
+version = "4.3.1"
description = "A client library for accessing langfuse"
readme = "README.md"
authors = [{ name = "langfuse", email = "[email protected]" }]
@@ -53,6 +53,12 @@
[tool.pytest.ini_options]
log_cli = true
+markers = [
+ "unit: deterministic tests that run without a Langfuse server",
+ "e2e: tests that require a real Langfuse server or persisted backend
behaviour",
+ "serial_e2e: e2e tests that must not share server concurrency with the
rest of the suite",
+ "live_provider: tests that call live model providers and run as a
dedicated CI suite",
+]
[tool.mypy]
python_version = "3.12"