diff --git a/.github/langfuse-v4-dual-write.override.yml b/.github/langfuse-v4-dual-write.override.yml deleted file mode 100644 index 48ab6ff48..000000000 --- a/.github/langfuse-v4-dual-write.override.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - langfuse-worker: - environment: - LANGFUSE_MIGRATION_V4_WRITE_MODE: dual - langfuse-web: - environment: - LANGFUSE_MIGRATION_V4_WRITE_MODE: dual diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8789b586e..ba53ceefb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,17 +70,23 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" + - "3.15" name: Unit tests on Python ${{ matrix.python-version }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The pinned uv only knows Python 3.15 pre-releases; install the final release for uv to pick up. + - name: Set up Python 3.15 + if: ${{ matrix.python-version == '3.15' }} + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.15" - name: Install uv and set Python version uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 with: @@ -99,6 +105,14 @@ jobs: python --version uv run --frozen pytest -n auto --dist worksteal -s -v --log-cli-level=INFO tests/unit + # pydantic < 2.8 matches undiscriminated unions left to right, so the + # CreateScoresResponse member order matters there. + - name: Run score response parsing on pydantic 2.7 + if: ${{ matrix.python-version == '3.11' }} + run: | + uv run --frozen --with "pydantic==2.7.4" python -c "import pydantic; assert pydantic.VERSION == '2.7.4', pydantic.VERSION" + uv run --frozen --with "pydantic==2.7.4" pytest -v tests/unit/test_scores_batch_response.py + e2e-tests: runs-on: ubuntu-latest timeout-minutes: 30 @@ -160,8 +174,6 @@ jobs: LANGFUSE_SERVER_SHA="$(git ls-remote https://github.com/langfuse/langfuse.git HEAD | cut -f1)" curl -fsSL "https://raw.githubusercontent.com/langfuse/langfuse/${LANGFUSE_SERVER_SHA}/docker-compose.yml" \ -o ./langfuse-server/docker-compose.yml - cp ./.github/langfuse-v4-dual-write.override.yml \ - ./langfuse-server/docker-compose.override.yml echo "${LANGFUSE_SERVER_SHA}" - name: Run langfuse server diff --git a/AGENTS.md b/AGENTS.md index 1d82ae5dd..950bf3055 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,7 +109,7 @@ Minimum verification matrix: The main CI workflow currently runs: - linting on Python 3.13 - mypy on Python 3.13 -- `tests/unit` on a Python 3.10-3.14 matrix +- `tests/unit` on a Python 3.11-3.15 matrix - `tests/e2e` in 2 mechanical shards plus a serial subset inside each shard - `tests/live_provider` as one always-on suite - PR title validation for Conventional Commits diff --git a/langfuse/_client/attributes.py b/langfuse/_client/attributes.py index 4660b50f0..c3ca70a09 100644 --- a/langfuse/_client/attributes.py +++ b/langfuse/_client/attributes.py @@ -20,6 +20,7 @@ ) from langfuse._utils.serializer import EventSerializer from langfuse.api import MapValue +from langfuse.logger import langfuse_logger from langfuse.model import PromptClient from langfuse.types import SpanLevel @@ -32,8 +33,6 @@ class LangfuseOtelSpanAttributes: TRACE_TAGS = "langfuse.trace.tags" TRACE_PUBLIC = "langfuse.trace.public" TRACE_METADATA = "langfuse.trace.metadata" - TRACE_INPUT = "langfuse.trace.input" - TRACE_OUTPUT = "langfuse.trace.output" # Langfuse-observation attributes OBSERVATION_TYPE = "langfuse.observation.type" @@ -68,6 +67,7 @@ class LangfuseOtelSpanAttributes: EXPERIMENT_METADATA = "langfuse.experiment.metadata" EXPERIMENT_DATASET_ID = "langfuse.experiment.dataset.id" EXPERIMENT_ITEM_ID = "langfuse.experiment.item.id" + EXPERIMENT_ITEM_VERSION = "langfuse.experiment.item.version" EXPERIMENT_ITEM_EXPECTED_OUTPUT = "langfuse.experiment.item.expected_output" EXPERIMENT_ITEM_METADATA = "langfuse.experiment.item.metadata" EXPERIMENT_ITEM_ROOT_OBSERVATION_ID = "langfuse.experiment.item.root_observation_id" @@ -75,13 +75,9 @@ class LangfuseOtelSpanAttributes: def create_trace_attributes( *, - input: Optional[Any] = None, - output: Optional[Any] = None, public: Optional[bool] = None, ) -> dict: attributes = { - LangfuseOtelSpanAttributes.TRACE_INPUT: _serialize(input), - LangfuseOtelSpanAttributes.TRACE_OUTPUT: _serialize(output), LangfuseOtelSpanAttributes.TRACE_PUBLIC: public, } @@ -165,30 +161,24 @@ def _serialize(obj: Any) -> Optional[str]: return json.dumps(obj, cls=EventSerializer) -def _flatten_and_serialize_metadata_values( - metadata: Optional[Dict[str, Any]], -) -> Optional[Dict[str, str]]: - if metadata is None: - return None - - flattened_metadata: Dict[str, str] = {} - - def flatten_value(path: str, value: Any) -> None: - if isinstance(value, dict): - for nested_key, nested_value in value.items(): - flatten_value(f"{path}.{nested_key}", nested_value) +def _serialize_metadata_value(value: Any) -> str: + """JSON-encode one metadata value, strings included. - return + The server decodes metadata values from v5 SDKs, so encoding every value + keeps "123" and 123 distinct. EventSerializer turns ints outside the + JS-safe range and NaN/Infinity into JSON strings and never raises, so one + bad value cannot drop the other keys. Compact separators and raw non-ASCII + match JS JSON.stringify. + """ + # Plain strings skip EventSerializer, which returns them unchanged and then + # uses the same string encoder, so the output is identical but cheaper. + # str subclasses (e.g. str enums) still go through EventSerializer. + if type(value) is str: + return json.dumps(value, ensure_ascii=False) - serialized_value = _serialize(value) - - if serialized_value is not None: - flattened_metadata[path] = serialized_value - - for key, value in metadata.items(): - flatten_value(str(key), value) - - return flattened_metadata + return json.dumps( + value, cls=EventSerializer, separators=(",", ":"), ensure_ascii=False + ) def _flatten_and_serialize_metadata( @@ -200,16 +190,24 @@ def _flatten_and_serialize_metadata( else LangfuseOtelSpanAttributes.TRACE_METADATA ) - metadata_attributes: Dict[str, Union[str, int, None]] = {} + metadata_attributes: Dict[str, Optional[str]] = {} if not isinstance(metadata, dict): - metadata_attributes[prefix] = _serialize(metadata) + # JSON-encode non-dict metadata too, strings included, matching the + # per-key values below. None stays None so it is not written. + metadata_attributes[prefix] = ( + None if metadata is None else _serialize_metadata_value(metadata) + ) else: for key, value in metadata.items(): - metadata_attributes[f"{prefix}.{key}"] = ( - value - if isinstance(value, str) or isinstance(value, int) - else _serialize(value) - ) + # Skip None so an update does not overwrite an earlier value. + if value is None: + langfuse_logger.debug( + 'Observation metadata key "%s" was not written because its value is None', + key, + ) + continue + + metadata_attributes[f"{prefix}.{key}"] = _serialize_metadata_value(value) return metadata_attributes diff --git a/langfuse/_client/client.py b/langfuse/_client/client.py index af06e69ef..c474f0640 100644 --- a/langfuse/_client/client.py +++ b/langfuse/_client/client.py @@ -4,13 +4,13 @@ """ import asyncio +import json import logging import os import re import urllib.parse import uuid -import warnings -from datetime import datetime +from datetime import datetime, timezone from hashlib import sha256 from time import time_ns from typing import ( @@ -39,11 +39,9 @@ _agnosticcontextmanager, ) from packaging.version import Version -from typing_extensions import deprecated from langfuse._client.attributes import ( LangfuseOtelSpanAttributes, - _flatten_and_serialize_metadata_values, _serialize, ) from langfuse._client.constants import ( @@ -85,6 +83,7 @@ LangfuseRetriever, LangfuseSpan, LangfuseTool, + _set_span_attributes_within_limit, ) from langfuse._client.utils import ( get_sha256_hash_hex, @@ -99,23 +98,20 @@ AsyncLangfuseAPI, CreateChatPromptRequest, CreateChatPromptType, + CreateScoreRequest, CreateTextPromptRequest, Dataset, DatasetItem, DatasetItemMediaReferenceField, - DatasetRunWithItems, DatasetStatus, - DeleteDatasetRunResponse, Error, LangfuseAPI, MapValue, NotFoundError, - PaginatedDatasetRuns, Prompt_Chat, Prompt_Text, - ScoreBody, - TraceBody, ) +from langfuse.api.core.datetime_utils import serialize_datetime from langfuse.batch_evaluation import ( BatchEvaluationResult, BatchEvaluationResumeToken, @@ -217,7 +213,7 @@ class Langfuse: release (Optional[str]): Release version/hash of your application. Used for grouping analytics by release. media_upload_thread_count (Optional[int]): Number of background threads for handling media uploads. Defaults to 1. Can also be set via LANGFUSE_MEDIA_UPLOAD_THREAD_COUNT environment variable. sample_rate (Optional[float]): Sampling rate for traces (0.0 to 1.0). Defaults to 1.0 (100% of traces are sampled). Can also be set via LANGFUSE_SAMPLE_RATE environment variable. - mask (Optional[MaskFunction]): Function to mask sensitive data synchronously when Langfuse SDK attributes are created. This applies only to data set through Langfuse SDK APIs such as `start_observation()`, `update()`, and `set_trace_io()`. + mask (Optional[MaskFunction]): Function to mask sensitive data synchronously when Langfuse SDK attributes are created. This applies only to data set through Langfuse SDK APIs such as `start_observation()` and `update()`. mask_otel_spans (Optional[MaskOtelSpansFunction]): Synchronous export-stage hook for masking raw OpenTelemetry span attributes before this Langfuse client sends them to Langfuse. Use this for spans created by third-party OpenTelemetry instrumentations, or when you need to inspect final span attributes after export filtering and Langfuse media handling. It does not modify spans already exported through other OpenTelemetry exporters. The hook receives one OpenTelemetry export batch. A batch is not guaranteed to contain a complete trace, request, or Langfuse observation tree. The hook usually runs on the OpenTelemetry batch span processor worker thread; during `flush()` and shutdown it may run on the caller thread. Keep it synchronous, deterministic, and fast. @@ -251,25 +247,12 @@ def mask_otel_spans( langfuse = Langfuse(mask_otel_spans=mask_otel_spans) ``` - blocked_instrumentation_scopes (Optional[List[str]]): Deprecated. Use `should_export_span` instead. Equivalent behavior: - ```python - from langfuse.span_filter import is_default_export_span - blocked = {"sqlite", "requests"} - - should_export_span = lambda span: ( - is_default_export_span(span) - and ( - span.instrumentation_scope is None - or span.instrumentation_scope.name not in blocked - ) - ) - ``` should_export_span (Optional[Callable[[ReadableSpan], bool]]): Callback to decide whether to export a span. If omitted, Langfuse uses the default filter (Langfuse SDK spans, spans with `gen_ai.*` attributes, and known LLM instrumentation scopes). additional_headers (Optional[Dict[str, str]]): Additional headers to include in all API requests and in the default OTLPSpanExporter requests. These headers will be merged with default headers. Note: If httpx_client is provided, additional_headers must be set directly on your custom httpx_client as well. If `span_exporter` is provided, these headers are not wired into that exporter and must be configured on the exporter instance directly. tracer_provider(Optional[TracerProvider]): OpenTelemetry TracerProvider to use for Langfuse. This can be useful to set to have disconnected tracing between Langfuse and other OpenTelemetry-span emitting libraries. Note: To track active spans, the context is still shared between TracerProviders. This may lead to broken trace trees. id_generator (Optional[IdGenerator]): OpenTelemetry ID generator to use when Langfuse creates its own TracerProvider. If omitted, the OpenTelemetry SDK default is used. If `tracer_provider` is provided, or an OpenTelemetry TracerProvider is already registered globally, configure the ID generator on that provider instead. span_exporter (Optional[SpanExporter]): Custom OpenTelemetry span exporter for the Langfuse span processor. If omitted, Langfuse creates an OTLPSpanExporter pointed at the Langfuse OTLP endpoint. If provided, Langfuse does not wire `base_url`, exporter headers, exporter auth, exporter timeout, `otel_compression`, or the `LANGFUSE_OTEL_MAX_BATCH_SIZE_BYTES` request size limit into it. Configure endpoint, headers, timeout, and compression on the exporter instance directly. If you are sending spans to Langfuse v4 or using Langfuse Cloud Fast Preview, include `x-langfuse-ingestion-version=4` on the exporter to enable real time processing of exported spans. - otel_compression (Optional[Literal["gzip", "none"]]): Compression for span batches sent by the default OTLP span exporter. Use "gzip" to reduce network bytes or "none" to send uncompressed. Can also be set via LANGFUSE_OTEL_COMPRESSION environment variable. If unset, the standard OTEL_EXPORTER_OTLP_TRACES_COMPRESSION and OTEL_EXPORTER_OTLP_COMPRESSION environment variables apply (no compression by default). "gzip" requires Langfuse server v3.30.0 or later. + otel_compression (Optional[Literal["gzip", "none"]]): Compression for span batches sent by the default OTLP span exporter: "gzip" (default) or "none" to send uncompressed. If unset, the LANGFUSE_OTEL_COMPRESSION, OTEL_EXPORTER_OTLP_TRACES_COMPRESSION and OTEL_EXPORTER_OTLP_COMPRESSION environment variables apply, in that order, then gzip. Values are case-insensitive; an invalid value falls through to the next setting. Example: ```python @@ -330,7 +313,6 @@ def __init__( sample_rate: Optional[float] = None, mask: Optional[MaskFunction] = None, mask_otel_spans: Optional[MaskOtelSpansFunction] = None, - blocked_instrumentation_scopes: Optional[List[str]] = None, should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, @@ -403,18 +385,6 @@ def __init__( "OTEL_SDK_DISABLED is set. Langfuse tracing will be disabled and no traces will appear in the UI." ) - if blocked_instrumentation_scopes is not None: - warnings.warn( - "`blocked_instrumentation_scopes` is deprecated and will be removed in a future release. " - "Use `should_export_span` instead. Example: " - "from langfuse.span_filter import is_default_export_span; " - 'blocked={"scope"}; should_export_span=lambda span: ' - "is_default_export_span(span) and (span.instrumentation_scope is None or " - "span.instrumentation_scope.name not in blocked).", - DeprecationWarning, - stacklevel=2, - ) - # Initialize api and tracer if requirements are met self._resources = LangfuseResourceManager( public_key=public_key, @@ -431,7 +401,6 @@ def __init__( mask=mask, mask_otel_spans=mask_otel_spans, tracing_enabled=self._tracing_enabled, - blocked_instrumentation_scopes=blocked_instrumentation_scopes, should_export_span=should_export_span, additional_headers=additional_headers, tracer_provider=tracer_provider, @@ -458,23 +427,17 @@ def api(self) -> LangfuseAPI: Semantics that are easy to miss: - **Ingestion is asynchronous.** `langfuse.flush()` only guarantees delivery to - the API, not read visibility: reads such as `api.trace.get(trace_id)` may - raise `langfuse.api.NotFoundError` until processing completes (typically - within 15-30 seconds; longer under load). The same applies to scores and - dataset run reads. Instead of a fixed sleep, retry with a deadline: - - - **List endpoints return lightweight views.** `api.trace.list(...)` returns - `TraceWithDetails`, where `observations` and `scores` are lists of ID strings. - Fetch the full objects with `api.trace.get(trace_id)` (`TraceWithFullDetails`), - or prefer `api.observations.get_many(trace_id=...)` for row-level observation - queries. The same list-view vs. get-detail pattern applies to other resources. - - - **Prefer the v2 data APIs — they are the defaults since SDK v4.** - `api.observations` and `api.metrics` map to the high-performance - `/api/public/v2/...` endpoints and are the recommended read path. Their v1 - equivalents remain available under `api.legacy.observations_v1` / - `api.legacy.metrics_v1` but are less performant at scale, not recommended - for new workflows, and will be deprecated. + the API, not read visibility: reads such as + `api.observations.get_many(trace_id=...)` may return no data until + processing completes (typically within 15-30 seconds; longer under load). + The same applies to scores and experiment reads. Instead of a fixed sleep, + poll with a deadline (see the ingestion-lag link below). + + - **Observations are the read model.** Read trace data with + `api.observations.get_many(trace_id=...)` (`/api/public/v2/observations`, + cursor-paginated; request field groups with `fields`). A trace's name, + user, session, tags and input/output are on its root observation. Read + scores with `api.scores.get_many` and experiments with `api.experiments`. - For large-scale aggregation (usage/cost by model, user, etc.), prefer the v2 Metrics API (`api.metrics.metrics(...)`) over paginating row-level data. @@ -482,7 +445,7 @@ def api(self) -> LangfuseAPI: See also: `async_api`, https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk - (ingestion lag: #ingestion-lag, list vs. get: #traces-list-vs-get), + (ingestion lag: #ingestion-lag), https://langfuse.com/docs/api-and-data-platform/features/observations-api, https://langfuse.com/docs/metrics/features/metrics-api """ @@ -726,7 +689,9 @@ def start_observation( cast(otel_trace_api.Span, remote_parent_span) ): otel_span = self._otel_tracer.start_span(name=name) - otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) + _set_span_attributes_within_limit( + otel_span, {LangfuseOtelSpanAttributes.AS_ROOT: True} + ) return self._create_observation_from_otel_span( otel_span=otel_span, @@ -1316,8 +1281,9 @@ def _create_span_with_parent_context( prompt=prompt, ) as langfuse_span: if remote_parent_span is not None: - langfuse_span._otel_span.set_attribute( - LangfuseOtelSpanAttributes.AS_ROOT, True + _set_span_attributes_within_limit( + langfuse_span._otel_span, + {LangfuseOtelSpanAttributes.AS_ROOT: True}, ) yield langfuse_span @@ -1565,55 +1531,6 @@ def update_current_span( status_message=status_message, ) - @deprecated( - "Trace-level input/output is deprecated. " - "For trace attributes (user_id, session_id, tags, etc.), use propagate_attributes() instead. " - "This method will be removed in a future major version." - ) - def set_current_trace_io( - self, - *, - input: Optional[Any] = None, - output: Optional[Any] = None, - ) -> None: - """Set trace-level input and output for the current span's trace. - - .. deprecated:: - This is a legacy method for backward compatibility with Langfuse platform - features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge - evaluators). It will be removed in a future major version. - - For setting other trace attributes (user_id, session_id, metadata, tags, version), - use :func:`langfuse.propagate_attributes` (top-level import) instead. - - Args: - input: Input data to associate with the trace. - output: Output data to associate with the trace. - """ - if not self._tracing_enabled: - langfuse_logger.debug( - "Operation skipped: set_current_trace_io - Tracing is disabled or client is in no-op mode." - ) - return - - current_otel_span = self._get_current_otel_span() - - if current_otel_span is not None and current_otel_span.is_recording(): - span_class = self._get_span_class( - self._get_observation_type_from_otel_span(current_otel_span) - ) - span = span_class( - otel_span=current_otel_span, - langfuse_client=self, - environment=self._environment, - release=self._release, - ) - - span.set_trace_io( - input=input, - output=output, - ) - def set_current_trace_as_public(self) -> None: """Make the current trace publicly accessible via its URL. @@ -1696,7 +1613,9 @@ def create_event( otel_span = self._otel_tracer.start_span( name=name, start_time=timestamp ) - otel_span.set_attribute(LangfuseOtelSpanAttributes.AS_ROOT, True) + _set_span_attributes_within_limit( + otel_span, {LangfuseOtelSpanAttributes.AS_ROOT: True} + ) return cast( LangfuseEvent, @@ -2022,28 +1941,34 @@ def create_score( score_id = score_id or self._create_observation_id() + # The API rejects timestamps without a UTC offset; naive values are UTC. + if timestamp is not None and timestamp.tzinfo is None: + timestamp = timestamp.replace(tzinfo=timezone.utc) + try: - new_body = ScoreBody( + request = CreateScoreRequest( id=score_id, - sessionId=session_id, - datasetRunId=dataset_run_id, - traceId=trace_id, - observationId=observation_id, + session_id=session_id, + dataset_run_id=dataset_run_id, + trace_id=trace_id, + observation_id=observation_id, name=name, value=value, - dataType=data_type, # type: ignore + data_type=data_type, # type: ignore comment=comment, - configId=config_id, + config_id=config_id, environment=environment or self._environment, - metadata=metadata, + timestamp=timestamp or _get_timestamp(), ) - - event = { - "id": self.create_trace_id(), - "type": "score-create", - "timestamp": timestamp or _get_timestamp(), - "body": new_body, + # Fern's dict() keeps fields explicitly set to None, so drop them here. + score = { + key: value + for key, value in request.dict(by_alias=True).items() + if value is not None } + # The API accepts any JSON value as metadata, the generated model only dicts. + if metadata is not None: + score["metadata"] = metadata if self._resources is not None: # Force the score to be in sample if it was for a legacy trace ID, i.e. non-32 hexchar @@ -2052,7 +1977,7 @@ def create_score( ) self._resources.add_score_task( - event, + score, force_sample=force_sample, ) @@ -2065,42 +1990,6 @@ def create_score( e, ) - def _create_trace_tags_via_ingestion( - self, - *, - trace_id: str, - tags: List[str], - ) -> None: - """Private helper to enqueue trace tag updates via ingestion API events.""" - if not self._tracing_enabled: - return - - if len(tags) == 0: - return - - try: - new_body = TraceBody( - id=trace_id, - tags=tags, - ) - - event = { - "id": self.create_trace_id(), - "type": "trace-create", - "timestamp": _get_timestamp(), - "body": new_body, - } - - if self._resources is not None: - self._resources.add_trace_task(event) - except Exception as e: - langfuse_logger.exception( - "Error updating trace tags: Failed to process trace update event for " - "trace_id=%s. Error: %s", - trace_id, - e, - ) - @overload def score_current_span( self, @@ -2328,9 +2217,8 @@ def flush(self) -> None: `flush()` guarantees data was *delivered* to the API, not that it is *readable* yet: server-side ingestion is asynchronous, so flushed data may not be queryable for 15-30 seconds — - `api.observations.get_many(trace_id=...)` may return empty results and - `api.trace.get()` may raise `langfuse.api.NotFoundError` right after a - successful flush. See the `api` property docs for a bounded retry + `api.observations.get_many(trace_id=...)` may return empty results right + after a successful flush. See the `api` property docs for a bounded retry pattern, or https://langfuse.com/docs/api-and-data-platform/features/query-via-sdk#ingestion-lag """ @@ -2532,87 +2420,6 @@ def get_dataset( handle_fern_exception(e) raise e - def get_dataset_run( - self, *, dataset_name: str, run_name: str - ) -> DatasetRunWithItems: - """Fetch a dataset run by dataset name and run name. - - Args: - dataset_name (str): The name of the dataset. - run_name (str): The name of the run. - - Returns: - DatasetRunWithItems: The dataset run with its items. - """ - try: - return cast( - DatasetRunWithItems, - self.api.datasets.get_run( - dataset_name=self._url_encode(dataset_name), - run_name=self._url_encode(run_name), - request_options=None, - ), - ) - except Error as e: - handle_fern_exception(e) - raise e - - def get_dataset_runs( - self, - *, - dataset_name: str, - page: Optional[int] = None, - limit: Optional[int] = None, - ) -> PaginatedDatasetRuns: - """Fetch all runs for a dataset. - - Args: - dataset_name (str): The name of the dataset. - page (Optional[int]): Page number, starts at 1. - limit (Optional[int]): Limit of items per page. - - Returns: - PaginatedDatasetRuns: Paginated list of dataset runs. - """ - try: - return cast( - PaginatedDatasetRuns, - self.api.datasets.get_runs( - dataset_name=self._url_encode(dataset_name), - page=page, - limit=limit, - request_options=None, - ), - ) - except Error as e: - handle_fern_exception(e) - raise e - - def delete_dataset_run( - self, *, dataset_name: str, run_name: str - ) -> DeleteDatasetRunResponse: - """Delete a dataset run and all its run items. This action is irreversible. - - Args: - dataset_name (str): The name of the dataset. - run_name (str): The name of the run. - - Returns: - DeleteDatasetRunResponse: Confirmation of deletion. - """ - try: - return cast( - DeleteDatasetRunResponse, - self.api.datasets.delete_run( - dataset_name=self._url_encode(dataset_name), - run_name=self._url_encode(run_name), - request_options=None, - ), - ) - except Error as e: - handle_fern_exception(e) - raise e - def run_experiment( self, *, @@ -2645,8 +2452,9 @@ def run_experiment( Args: name: Human-readable name for the experiment. Used for identification in the Langfuse UI. - run_name: Optional exact name for the experiment run. If provided, this will be - used as the exact dataset run name if the `data` contains Langfuse dataset items. + run_name: Optional exact name for the experiment run. If the `data` contains + Langfuse dataset items, runs with the same `run_name` on the same dataset + share one experiment ID and appear as a single experiment in Langfuse. If not provided, this will default to the experiment name appended with an ISO timestamp. description: Optional description explaining the experiment's purpose, methodology, or expected outcomes. @@ -2670,17 +2478,22 @@ def run_experiment( Controls the number of items processed simultaneously. Adjust based on API rate limits and system resources. metadata: Optional metadata dictionary to attach to all experiment traces. - This metadata will be included in every trace created during the experiment. - If `data` are Langfuse dataset items, the metadata will be attached to the dataset run, too. + This metadata will be included in every trace created during the experiment + and is shown as the experiment's metadata in Langfuse. Returns: ExperimentResult containing: - - run_name: The experiment run name. This is equal to the dataset run name if experiment was on Langfuse dataset. + - run_name: The experiment run name, shown as the experiment name in Langfuse. - item_results: List of results for each processed item with outputs and evaluations - - run_evaluations: List of aggregate evaluation results for the entire run - - experiment_id: Stable identifier for the experiment run across all items - - dataset_run_id: ID of the dataset run (if using Langfuse datasets) - - dataset_run_url: Direct URL to view results in Langfuse UI (if applicable) + - run_evaluations: List of aggregate evaluation results for the entire run. + They are stored as scores on the experiment run, also for local data. + - experiment_id: Identifier for the experiment run across all items. For + Langfuse dataset items it is derived from the project ID, dataset ID and + run name; for local data it is random. + - experiment_url: Direct URL to view results in Langfuse UI (None if the + project ID could not be resolved) + - dataset_run_id / dataset_run_url: Deprecated aliases of experiment_id / + experiment_url Raises: ValueError: If required parameters are missing or invalid @@ -2778,14 +2591,16 @@ def average_accuracy(*, item_results, **kwargs): ) # Results automatically linked to dataset in Langfuse UI - print(f"View results: {result['dataset_run_url']}") + print(f"View results: {result.experiment_url}") ``` Note: - Task and evaluator functions can be either synchronous or asynchronous - Individual item failures are logged but don't stop the experiment - All executions are automatically traced and visible in Langfuse UI - - When using Langfuse datasets, results are automatically linked for easy comparison + - Experiment runs are recorded only through the exported traces; no + separate dataset run is created via the API + - When using Langfuse datasets, results are automatically linked to the dataset for easy comparison - This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.) - Async execution is handled automatically with smart event loop detection """ @@ -2829,7 +2644,34 @@ async def _run_experiment_async( "Starting experiment '%s' run '%s' with %s items", name, run_name, len(data) ) - shared_fallback_experiment_id = self._create_observation_id() + project_id: Optional[str] = None + if self._tracing_enabled: + try: + project_id = await asyncio.to_thread(self._get_project_id) + except Exception as e: + langfuse_logger.warning( + "Failed to resolve project id for experiment: %s", e + ) + + # One experiment id per run: mixed-dataset data uses the first dataset item's dataset. + experiment_dataset_id = next( + ( + getattr(item, "dataset_id", None) + for item in data + if not isinstance(item, dict) and getattr(item, "dataset_id", None) + ), + None, + ) + experiment_id = self._create_experiment_id( + project_id=project_id, + dataset_id=experiment_dataset_id, + run_name=run_name, + ) + experiment_item_version = ( + self._format_experiment_item_version(dataset_version) + if dataset_version is not None + else None + ) # Set up concurrency control semaphore = asyncio.Semaphore(max_concurrency) @@ -2842,12 +2684,12 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: task, evaluators, composite_evaluator, - shared_fallback_experiment_id, + experiment_id, name, run_name, description, metadata, - dataset_version, + experiment_item_version, ) # Run all items concurrently @@ -2873,47 +2715,24 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: except Exception as e: langfuse_logger.error("Run evaluator failed: %s", e) - # Generate dataset run URL if applicable - dataset_run_id = next( - ( - result.dataset_run_id - for result in valid_results - if result.dataset_run_id - ), - None, + experiment_url = ( + f"{self._base_url}/project/{project_id}/experiments/results?baseline={experiment_id}" + if project_id + else None ) - dataset_run_url = None - if dataset_run_id and data: - try: - # Check if the first item has dataset_id (for DatasetItem objects) - first_item = data[0] - dataset_id = None - - if hasattr(first_item, "dataset_id"): - dataset_id = getattr(first_item, "dataset_id", None) - - if dataset_id: - project_id = self._get_project_id() - - if project_id: - dataset_run_url = f"{self._base_url}/project/{project_id}/datasets/{dataset_id}/runs/{dataset_run_id}" - - except Exception: - pass # URL generation is optional - # Store run-level evaluations as scores + # Run-level scores attach to the experiment via dataset_run_id == experiment_id. for evaluation in run_evaluations: try: - if dataset_run_id: - self.create_score( - dataset_run_id=dataset_run_id, - name=evaluation.name or "", - value=evaluation.value, # type: ignore - comment=evaluation.comment, - metadata=evaluation.metadata, - data_type=evaluation.data_type, # type: ignore - config_id=evaluation.config_id, - ) + self.create_score( + dataset_run_id=experiment_id, + name=evaluation.name or "", + value=evaluation.value, # type: ignore + comment=evaluation.comment, + metadata=evaluation.metadata, + data_type=evaluation.data_type, # type: ignore + config_id=evaluation.config_id, + ) except Exception as e: langfuse_logger.error("Failed to store run evaluation: %s", e) @@ -2927,23 +2746,54 @@ async def process_item(item: ExperimentItem) -> ExperimentItemResult: description=description, item_results=valid_results, run_evaluations=run_evaluations, - experiment_id=dataset_run_id or shared_fallback_experiment_id, - dataset_run_id=dataset_run_id, - dataset_run_url=dataset_run_url, + experiment_id=experiment_id, + experiment_url=experiment_url, + ) + + def _create_experiment_id( + self, + *, + project_id: Optional[str], + dataset_id: Optional[str], + run_name: str, + ) -> str: + if dataset_id is None: + return self._create_observation_id() + + if project_id is None: + langfuse_logger.warning( + "Could not resolve the project id; using a random experiment id for run '%s'. " + "Re-runs with the same run name will not be grouped into the same experiment.", + run_name, + ) + return self._create_observation_id() + + # Must match the Langfuse server's stable experiment id derivation byte for + # byte (JS JSON.stringify of the same array, SHA-256, first 16 hex chars). + payload = json.dumps( + ["langfuse-experiment-v1", project_id, dataset_id, run_name], + separators=(",", ":"), + ensure_ascii=False, ) + return sha256(payload.encode("utf-8")).hexdigest()[:16] + + @staticmethod + def _format_experiment_item_version(dataset_version: datetime) -> str: + return serialize_datetime(dataset_version) + async def _process_experiment_item( self, item: ExperimentItem, task: Callable, evaluators: List[Callable], composite_evaluator: Optional[CompositeEvaluatorFunction], - fallback_experiment_id: str, + experiment_id: str, experiment_name: str, experiment_run_name: str, experiment_description: Optional[str], experiment_metadata: Optional[Dict[str, Any]] = None, - dataset_version: Optional[datetime] = None, + experiment_item_version: Optional[str] = None, ) -> ExperimentItemResult: with self.start_as_current_observation(name="experiment-item-run") as span: try: @@ -2968,17 +2818,14 @@ async def _process_experiment_item( else getattr(item, "metadata", None) ) - final_observation_metadata = { - **(item_metadata if isinstance(item_metadata, dict) else {}), - **(experiment_metadata or {}), - "experiment_name": experiment_name, - "experiment_run_name": experiment_run_name, - } - trace_id = span.trace_id dataset_id = None dataset_item_id = None - dataset_run_id = None + + experiment_run_metadata: Dict[str, Any] = { + "experiment_name": experiment_name, + "experiment_run_name": experiment_run_name, + } if ( not isinstance(item, dict) @@ -2988,10 +2835,20 @@ async def _process_experiment_item( dataset_id = item.dataset_id dataset_item_id = item.id - final_observation_metadata.update( + experiment_run_metadata.update( {"dataset_id": dataset_id, "dataset_item_id": dataset_item_id} ) + # Experiment run keys go first so the span attribute limit drops + # user metadata before them, and last so they still win over + # user keys. + final_observation_metadata = { + **experiment_run_metadata, + **(item_metadata if isinstance(item_metadata, dict) else {}), + **(experiment_metadata or {}), + **experiment_run_metadata, + } + experiment_item_id = ( dataset_item_id or get_sha256_hash_hex(_serialize(input_data))[:16] ) @@ -3003,54 +2860,32 @@ async def _process_experiment_item( LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_EXPECTED_OUTPUT: _serialize( expected_output ), + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_VERSION: ( + experiment_item_version if dataset_id else None + ), }.items() if v is not None } - span._otel_span.set_attributes(experiment_span_attributes) + _set_span_attributes_within_limit( + span._otel_span, experiment_span_attributes + ) with span.start_as_current_observation( name="experiment-item-task", as_type="span", input=input_data, - metadata=final_observation_metadata, ) as task_span: - task_span._otel_span.set_attributes(experiment_span_attributes) - - # Link dataset runs to the canonical task observation so their - # latency excludes the subsequent evaluator subtree. - if hasattr(item, "id") and hasattr(item, "dataset_id"): - try: - # Use sync API to avoid event loop issues when - # run_async_safely creates multiple event loops across - # different threads. - dataset_run_item = await asyncio.to_thread( - self.api.dataset_run_items.create, - run_name=experiment_run_name, - run_description=experiment_description, - metadata=experiment_metadata, - dataset_item_id=item.id, # type: ignore - trace_id=trace_id, - observation_id=task_span.id, - dataset_version=dataset_version, - ) - - dataset_run_id = dataset_run_item.dataset_run_id - - except Exception as e: - langfuse_logger.error( - "Failed to create dataset run item: %s", e - ) + _set_span_attributes_within_limit( + task_span._otel_span, experiment_span_attributes + ) - experiment_id = dataset_run_id or fallback_experiment_id propagated_experiment_attributes = PropagatedExperimentAttributes( experiment_id=experiment_id, experiment_name=experiment_run_name, - experiment_metadata=_flatten_and_serialize_metadata_values( - experiment_metadata - ), + experiment_metadata=_serialize(experiment_metadata), experiment_dataset_id=dataset_id, experiment_item_id=experiment_item_id, - experiment_item_metadata=_flatten_and_serialize_metadata_values( + experiment_item_metadata=_serialize( item_metadata if isinstance(item_metadata, dict) else None ), experiment_item_root_observation_id=task_span.id, @@ -3061,11 +2896,16 @@ async def _process_experiment_item( ): # _propagate_attributes updates the current task span and future children. # Explicitly backfill the parent item-run span to preserve experiment association. - span._otel_span.set_attributes( + _set_span_attributes_within_limit( + span._otel_span, _get_propagated_attributes_from_context( otel_context_api.get_current() - ) + ), ) + # Write the observation metadata after the experiment + # attributes, so the span attribute limit trims the + # metadata instead of leaving no room for the output. + task_span.update(metadata=final_observation_metadata) try: output = await _run_task(task, item) except Exception as e: @@ -3243,7 +3083,7 @@ async def _process_experiment_item( output=output, evaluations=evaluations, trace_id=trace_id, - dataset_run_id=dataset_run_id, + experiment_id=experiment_id, ) def _create_experiment_run_name( @@ -3259,11 +3099,10 @@ def _create_experiment_run_name( def run_batched_evaluation( self, *, - scope: Literal["traces", "observations"], mapper: MapperFunction, filter: Optional[str] = None, fetch_batch_size: int = 50, - fetch_trace_fields: Optional[str] = None, + fields: Optional[str] = "core,basic,io,metadata", max_items: Optional[int] = None, max_retries: int = 3, evaluators: List[EvaluatorFunction], @@ -3271,16 +3110,15 @@ def run_batched_evaluation( max_concurrency: int = 5, metadata: Optional[Dict[str, Any]] = None, _add_observation_scores_to_trace: bool = False, - _additional_trace_tags: Optional[List[str]] = None, resume_from: Optional[BatchEvaluationResumeToken] = None, verbose: bool = False, ) -> BatchEvaluationResult: - """Fetch traces or observations using legacy read APIs and evaluate each item. + """Fetch observations from Langfuse and evaluate each of them. This method provides a powerful way to evaluate existing data in Langfuse at scale. - It fetches items based on filters, transforms them using a mapper function, runs - evaluators on each item, and creates scores that are linked back to the original - entities. This is ideal for: + It fetches observations based on filters, transforms them using a mapper function, + runs evaluators on each item, and creates scores that are linked back to the + original entities. This is ideal for: - Running evaluations on production traces after deployment - Backtesting new evaluation metrics on historical data @@ -3291,46 +3129,51 @@ def run_batched_evaluation( it memory-efficient for large datasets. It includes comprehensive error handling, retry logic, and resume capability for long-running evaluations. - Legacy platform compatibility: - This method reads traces from `GET /api/public/traces` and observations - from the legacy `GET /api/public/observations` endpoint. It is supported - with Langfuse platform v3 and is not yet supported with platform v4. + Items are read from `GET /api/public/v2/observations` with cursor pagination, + newest first (by start time). Args: - scope: The type of items to evaluate. Must be one of: - - "traces": Evaluate complete traces with all their observations - - "observations": Evaluate individual observations (spans, generations, events) - mapper: Function that transforms API response objects into evaluator inputs. - Receives a trace/observation object and returns an EvaluatorInputs + mapper: Function that transforms an `ObservationV2` into evaluator inputs. + Called as `mapper(item=observation)` and must return an EvaluatorInputs instance with input, output, expected_output, and metadata fields. - Can be sync or async. + `input`/`output` are raw strings (not JSON-parsed), and only the + requested `fields` groups are populated. Can be sync or async. evaluators: List of evaluation functions to run on each item. Each evaluator receives the mapped inputs and returns Evaluation object(s). Evaluator failures are logged but don't stop the batch evaluation. - filter: Optional JSON filter string for querying items (same format as Langfuse API). Examples: - - '{"tags": ["production"]}' - - '{"user_id": "user123", "timestamp": {"operator": ">", "value": "2024-01-01"}}' - Default: None (fetches all items). + filter: Optional JSON array of filter conditions in the v2 observations + filter format, for example: + - '[{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["production"]}]' + - '[{"type": "string", "column": "traceName", "operator": "=", "value": "chat"}]' + - '[{"type": "datetime", "column": "startTime", "operator": ">=", "value": "2026-01-01T00:00:00Z"}]' + Default: None (fetches all observations). fetch_batch_size: Number of items to fetch per API call and hold in memory. - Larger values may be faster but use more memory. Default: 50. - fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'. + Larger values may be faster but use more memory. Maximum 1000. Default: 50. + fields: Comma-separated list of observation field groups to fetch. Available + groups: 'core' (always included), 'basic', 'time', 'io', 'metadata', + 'model', 'usage', 'prompt', 'metrics', 'trace_context'. Fields of groups + that are not requested are None on the item passed to the mapper. + Metadata values longer than 200 characters are truncated. + Default: "core,basic,io,metadata". max_items: Maximum total number of items to process. If None, processes all items matching the filter. Useful for testing or limiting evaluation runs. Default: None (process all). max_concurrency: Maximum number of items to evaluate concurrently. Controls parallelism and resource usage. Default: 5. composite_evaluator: Optional function that creates a composite score from - item-level evaluations. Receives the original item and its evaluations, - returns a single Evaluation. Useful for weighted averages or combined metrics. - Default: None. + item-level evaluations. Receives the mapped inputs and the item's + evaluations, returns Evaluation(s). Useful for weighted averages or + combined metrics. Default: None. metadata: Optional metadata dict to add to all created scores. Useful for tracking evaluation runs, versions, or other context. Default: None. max_retries: Maximum number of retry attempts for failed batch fetches. - Uses exponential backoff (1s, 2s, 4s). Default: 3. + Default: 3. verbose: If True, logs progress information to console. Useful for monitoring long-running evaluations. Default: False. - resume_from: Optional resume token from a previous incomplete run. Allows - continuing evaluation after interruption or failure. Default: None. + resume_from: Optional resume token from a previous run that stopped early + (fetch failure or `max_items`). Continues exactly after the last + processed page. Pass the same `filter`, or omit it to reuse the + token's filter. Default: None. Returns: @@ -3342,134 +3185,112 @@ def run_batched_evaluation( - total_composite_scores_created: Scores created by composite evaluator - total_evaluations_failed: Individual evaluator failures - evaluator_stats: Per-evaluator statistics (success rate, scores created) - - resume_token: Token for resuming if incomplete (None if completed) - - completed: True if all items processed + - resume_token: Token for continuing the run (set after a fetch failure + or when max_items was reached while more items exist) + - completed: False if the run stopped because a fetch failed - duration_seconds: Total execution time - - failed_item_ids: IDs of items that failed + - failed_item_ids: Observation IDs of items that failed - error_summary: Error types and counts - has_more_items: True if max_items reached but more exist + - item_evaluations: Evaluations per observation ID Raises: - ValueError: If invalid scope is provided. + ValueError: If a non-array filter is provided, or the resume token was + created for a different filter. Examples: - Basic trace evaluation: + Evaluate production observations: ```python from langfuse import Langfuse, EvaluatorInputs, Evaluation client = Langfuse() - # Define mapper to extract fields from traces - def trace_mapper(trace): + def simple_mapper(*, item): return EvaluatorInputs( - input=trace.input, - output=trace.output, + input=item.input, + output=item.output, expected_output=None, - metadata={"trace_id": trace.id} + metadata={"trace_id": item.trace_id}, ) - # Define evaluator def length_evaluator(*, input, output, expected_output, metadata): return Evaluation( name="output_length", value=len(output) if output else 0 ) - # Run batch evaluation result = client.run_batched_evaluation( - scope="traces", - mapper=trace_mapper, + mapper=simple_mapper, evaluators=[length_evaluator], - filter='{"tags": ["production"]}', + filter='[{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["production"]}]', max_items=1000, verbose=True ) - print(f"Processed {result.total_items_processed} traces") + print(f"Processed {result.total_items_processed} observations") print(f"Created {result.total_scores_created} scores") ``` - Evaluation with composite scorer: + Evaluate generations with a composite scorer: ```python + import json + + def generation_mapper(*, item): + return EvaluatorInputs( + input=json.loads(item.input) if item.input else None, + output=item.output, + expected_output=None, + metadata={"model": item.model}, + ) + def accuracy_evaluator(*, input, output, expected_output, metadata): - # ... evaluation logic return Evaluation(name="accuracy", value=0.85) def relevance_evaluator(*, input, output, expected_output, metadata): - # ... evaluation logic return Evaluation(name="relevance", value=0.92) - def composite_evaluator(*, item, evaluations): - # Weighted average of evaluations + def composite_evaluator(*, input, output, expected_output, metadata, evaluations): weights = {"accuracy": 0.6, "relevance": 0.4} total = sum( e.value * weights.get(e.name, 0) for e in evaluations if isinstance(e.value, (int, float)) ) - return Evaluation( - name="composite_score", - value=total, - comment=f"Weighted average of {len(evaluations)} metrics" - ) + return Evaluation(name="composite_score", value=total) result = client.run_batched_evaluation( - scope="traces", - mapper=trace_mapper, + mapper=generation_mapper, evaluators=[accuracy_evaluator, relevance_evaluator], composite_evaluator=composite_evaluator, - filter='{"user_id": "important_user"}', - verbose=True + filter='[{"type": "string", "column": "type", "operator": "=", "value": "GENERATION"}]', + fields="core,basic,io,model", ) ``` - Handling incomplete runs with resume: + Continuing a run that stopped early: ```python - # Initial run that may fail or timeout result = client.run_batched_evaluation( - scope="observations", - mapper=obs_mapper, - evaluators=[my_evaluator], + mapper=generation_mapper, + evaluators=[accuracy_evaluator], max_items=10000, - verbose=True ) - # Check if incomplete - if not result.completed and result.resume_token: - print(f"Processed {result.resume_token.items_processed} items before interruption") - - # Resume from where it left off + while result.resume_token: result = client.run_batched_evaluation( - scope="observations", - mapper=obs_mapper, - evaluators=[my_evaluator], + mapper=generation_mapper, + evaluators=[accuracy_evaluator], + max_items=10000, resume_from=result.resume_token, - verbose=True ) - - print(f"Total items processed: {result.total_items_processed}") - ``` - - Monitoring evaluator performance: - ```python - result = client.run_batched_evaluation(...) - - for stats in result.evaluator_stats: - success_rate = stats.successful_runs / stats.total_runs - print(f"{stats.name}:") - print(f" Success rate: {success_rate:.1%}") - print(f" Scores created: {stats.total_scores_created}") - - if stats.failed_runs > 0: - print(f" ⚠️ Failed {stats.failed_runs} times") ``` Note: - Evaluator failures are logged but don't stop the batch evaluation - Individual item failures are tracked but don't stop processing - - Fetch failures are retried with exponential backoff + - Fetch failures are retried up to `max_retries` times - All scores are automatically flushed to Langfuse at the end - - The resume mechanism uses timestamp-based filtering to avoid duplicates + - Resuming uses the pagination cursor, so items are neither skipped nor + evaluated twice """ runner = BatchEvaluationRunner(self) @@ -3477,18 +3298,16 @@ def composite_evaluator(*, item, evaluations): BatchEvaluationResult, run_async_safely( runner.run_async( - scope=scope, mapper=mapper, evaluators=evaluators, filter=filter, fetch_batch_size=fetch_batch_size, - fetch_trace_fields=fetch_trace_fields, + fields=fields, max_items=max_items, max_concurrency=max_concurrency, composite_evaluator=composite_evaluator, metadata=metadata, _add_observation_scores_to_trace=_add_observation_scores_to_trace, - _additional_trace_tags=_additional_trace_tags, max_retries=max_retries, verbose=verbose, resume_from=resume_from, @@ -4137,7 +3956,6 @@ def create_prompt( Keyword Args: name : The name of the prompt to be created. prompt : The content of the prompt to be created. - is_active [DEPRECATED] : A flag indicating whether the prompt is active or not. This is deprecated and will be removed in a future release. Please use the 'production' label instead. labels: The labels of the prompt. Defaults to None. To create a default-served prompt, add the 'production' label. tags: The tags of the prompt. Defaults to None. Will be applied to all versions of the prompt. config: Additional structured data to be saved with the prompt. Defaults to None. diff --git a/langfuse/_client/constants.py b/langfuse/_client/constants.py index c2d0aa7aa..95968507a 100644 --- a/langfuse/_client/constants.py +++ b/langfuse/_client/constants.py @@ -11,6 +11,8 @@ LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT = "sdk-experiment" +MASK_FALLBACK_VALUE = "" + """Note: this type is used with .__args__ / get_args in some cases and therefore must remain flat""" ObservationTypeGenerationLike: TypeAlias = Literal[ "generation", diff --git a/langfuse/_client/datasets.py b/langfuse/_client/datasets.py index e28ddbebf..1070c76d3 100644 --- a/langfuse/_client/datasets.py +++ b/langfuse/_client/datasets.py @@ -94,11 +94,11 @@ def run_experiment( """Run an experiment on this Langfuse dataset with automatic tracking. This is a convenience method that runs an experiment using all items in this - dataset. It automatically creates a dataset run in Langfuse for tracking and - comparison purposes, linking all experiment results to the dataset. + dataset. All results are recorded as one experiment in Langfuse that is linked + to the dataset for tracking and comparison purposes. Key benefits of using dataset.run_experiment(): - - Automatic dataset run creation and linking in Langfuse UI + - Automatic experiment tracking and linking to the dataset in Langfuse UI - Built-in experiment tracking and versioning - Easy comparison between different experiment runs - Direct access to dataset items with their metadata and expected outputs @@ -106,10 +106,11 @@ def run_experiment( Args: name: Human-readable name for the experiment run. This will be used as - the dataset run name in Langfuse for tracking and identification. - run_name: Optional exact name for the dataset run. If provided, this will be - used as the exact dataset run name in Langfuse. If not provided, this will - default to the experiment name appended with an ISO timestamp. + the base of the experiment run name in Langfuse for tracking and identification. + run_name: Optional exact name for the experiment run. Runs with the same + `run_name` on this dataset share one experiment ID and appear as a single + experiment in Langfuse. If not provided, this will default to the + experiment name appended with an ISO timestamp. description: Optional description of the experiment's purpose, methodology, or what you're testing. Appears in the Langfuse UI for context. task: Function that processes each dataset item and returns output. @@ -131,12 +132,14 @@ def run_experiment( Returns: ExperimentResult object containing: - name: The experiment name. - - run_name: The experiment run name (equivalent to the dataset run name). + - run_name: The experiment run name, shown as the experiment name in Langfuse. - description: Optional experiment description. - item_results: Results for each dataset item with outputs and evaluations. - run_evaluations: Aggregate evaluation results for the entire run. - - dataset_run_id: ID of the created dataset run in Langfuse. - - dataset_run_url: Direct URL to view the experiment results in Langfuse UI. + - experiment_id: ID of the experiment run in Langfuse. + - experiment_url: Direct URL to view the experiment results in Langfuse UI. + - dataset_run_id / dataset_run_url: Deprecated aliases of experiment_id / + experiment_url. The result object provides a format() method for human-readable output: ```python @@ -176,8 +179,8 @@ def accuracy_evaluator(*, input, output, expected_output=None, **kwargs): evaluators=[accuracy_evaluator] ) - print(f"Evaluated {len(result['item_results'])} questions") - print(f"View detailed results: {result['dataset_run_url']}") + print(f"Evaluated {len(result.item_results)} questions") + print(f"View detailed results: {result.experiment_url}") ``` Advanced experiment with multiple evaluators and run-level analysis: @@ -244,11 +247,11 @@ def content_diversity(*, item_results, **kwargs): ) # Results are automatically linked to dataset in Langfuse - print(f"Experiment completed! View in Langfuse: {result['dataset_run_url']}") + print(f"Experiment completed! View in Langfuse: {result.experiment_url}") # Access individual results - for i, item_result in enumerate(result["item_results"]): - print(f"Item {i+1}: {item_result['evaluations']}") + for i, item_result in enumerate(result.item_results): + print(f"Item {i+1}: {item_result.evaluations}") ``` Comparing different model versions: @@ -274,15 +277,15 @@ def content_diversity(*, item_results, **kwargs): # Both experiments are now visible in Langfuse for easy comparison print("Compare results in Langfuse:") - print(f"GPT-4: {result_gpt4.dataset_run_url}") - print(f"Custom: {result_custom.dataset_run_url}") + print(f"GPT-4: {result_gpt4.experiment_url}") + print(f"Custom: {result_custom.experiment_url}") ``` Note: - - All experiment results are automatically tracked in Langfuse as dataset runs + - All experiment results are automatically tracked in Langfuse as experiments on this dataset - Dataset items provide .input, .expected_output, and .metadata attributes - Results can be easily compared across different experiment runs in the UI - - The dataset_run_url provides direct access to detailed results and analysis + - The experiment_url provides direct access to detailed results and analysis - Failed items are handled gracefully and logged without stopping the experiment - This method works in both sync and async contexts (Jupyter notebooks, web apps, etc.) - Async execution is handled automatically with smart event loop detection diff --git a/langfuse/_client/environment_variables.py b/langfuse/_client/environment_variables.py index b340c9ac4..3d2c84a92 100644 --- a/langfuse/_client/environment_variables.py +++ b/langfuse/_client/environment_variables.py @@ -77,13 +77,13 @@ """ .. envvar:: LANGFUSE_OTEL_COMPRESSION -Compression for span batches sent by the default OTLP exporter: ``gzip`` or ``none``. -The ``otel_compression`` client argument takes precedence. If unset, the standard -``OTEL_EXPORTER_OTLP_TRACES_COMPRESSION`` and ``OTEL_EXPORTER_OTLP_COMPRESSION`` -environment variables apply. ``gzip`` requires Langfuse server v3.30.0 or later. +Compression for span batches sent by the default OTLP exporter: ``gzip`` or ``none`` +(case-insensitive). The ``otel_compression`` client argument takes precedence. If this +variable is unset or invalid, ``OTEL_EXPORTER_OTLP_TRACES_COMPRESSION`` and then +``OTEL_EXPORTER_OTLP_COMPRESSION`` apply, and otherwise span exports are gzip-compressed. Custom span exporters are not affected. -**Default value:** unset +**Default value:** unset (span exports use gzip) """ LANGFUSE_DEBUG = "LANGFUSE_DEBUG" diff --git a/langfuse/_client/get_client.py b/langfuse/_client/get_client.py index ff06c7d29..eceaed5ef 100644 --- a/langfuse/_client/get_client.py +++ b/langfuse/_client/get_client.py @@ -51,7 +51,6 @@ def _create_client_from_instance( sample_rate=instance.sample_rate, mask=instance.mask, mask_otel_spans=instance.mask_otel_spans, - blocked_instrumentation_scopes=instance.blocked_instrumentation_scopes, should_export_span=instance.should_export_span, additional_headers=instance.additional_headers, tracer_provider=instance.tracer_provider, diff --git a/langfuse/_client/observe.py b/langfuse/_client/observe.py index 848506a46..5aa88ce2e 100644 --- a/langfuse/_client/observe.py +++ b/langfuse/_client/observe.py @@ -2,7 +2,6 @@ import contextvars import inspect import os -import sys from functools import wraps from typing import ( Any, @@ -49,7 +48,16 @@ P = ParamSpec("P") R = TypeVar("R") -_ASYNCIO_CREATE_TASK_SUPPORTS_CONTEXT = sys.version_info >= (3, 11) +# Set by asgiref's markcoroutinefunction on Python < 3.12, which +# inspect.iscoroutinefunction does not recognize. +_ASYNCIO_COROUTINE_MARKER = getattr(asyncio.coroutines, "_is_coroutine", None) + + +def _is_coroutine_function(func: Any) -> bool: + return inspect.iscoroutinefunction(func) or ( + _ASYNCIO_COROUTINE_MARKER is not None + and getattr(func, "_is_coroutine", None) is _ASYNCIO_COROUTINE_MARKER + ) class LangfuseDecorator: @@ -217,7 +225,7 @@ def decorator(func: F) -> F: capture_output=should_capture_output, transform_to_string=transform_to_string, ) - if asyncio.iscoroutinefunction(func) + if _is_coroutine_function(func) else self._sync_observe( func, name=name, @@ -747,15 +755,7 @@ async def aclose(self) -> None: self._finalize() async def _close_generator(self) -> None: - if _ASYNCIO_CREATE_TASK_SUPPORTS_CONTEXT: - close_task = asyncio.create_task( - self.generator.aclose(), - context=self.context, - ) # type: ignore - else: - close_task = self.context.run(asyncio.create_task, self.generator.aclose()) - - await close_task + await asyncio.create_task(self.generator.aclose(), context=self.context) async def close(self) -> None: await self.aclose() @@ -769,16 +769,10 @@ def __del__(self) -> None: async def __anext__(self) -> Any: try: # Run the generator's __anext__ in the preserved context - if _ASYNCIO_CREATE_TASK_SUPPORTS_CONTEXT: - item = await asyncio.create_task( - self.generator.__anext__(), # type: ignore - context=self.context, - ) # type: ignore - else: - item = await self.context.run( - asyncio.create_task, - self.generator.__anext__(), # type: ignore - ) + item = await asyncio.create_task( + self.generator.__anext__(), # type: ignore + context=self.context, + ) if self.capture_output: self.items.append(item) diff --git a/langfuse/_client/propagation.py b/langfuse/_client/propagation.py index ecf961e4f..d36ca534a 100644 --- a/langfuse/_client/propagation.py +++ b/langfuse/_client/propagation.py @@ -38,8 +38,12 @@ _agnosticcontextmanager, ) -from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse._client.attributes import ( + LangfuseOtelSpanAttributes, + _serialize_metadata_value, +) from langfuse._client.constants import LANGFUSE_SDK_EXPERIMENT_ENVIRONMENT +from langfuse._client.span import _set_span_attributes_within_limit from langfuse.logger import langfuse_logger from langfuse.model import PromptClient @@ -88,10 +92,10 @@ class PropagatedExperimentAttributes(TypedDict): experiment_id: str experiment_name: str - experiment_metadata: Optional[Dict[str, str]] + experiment_metadata: Optional[str] # serialized JSON experiment_dataset_id: Optional[str] experiment_item_id: str - experiment_item_metadata: Optional[Dict[str, str]] + experiment_item_metadata: Optional[str] # serialized JSON experiment_item_root_observation_id: str @@ -153,8 +157,8 @@ def propagate_attributes( within a user session (e.g., a conversation thread, multi-turn interaction). metadata: Additional key-value metadata to propagate to all spans. - Keys must be US-ASCII strings - - Values are coerced to strings - - Coerced values must be ≤200 characters + - Values are JSON-encoded, strings included + - Encoded values must be ≤200 characters - Use for dimensions like internal correlating identifiers - AVOID: large payloads or sensitive data version: Version identfier for parts of your application that are independently versioned, e.g. agents @@ -282,7 +286,12 @@ def propagate_attributes( trace_name) must be strings ≤200 characters. Environment must also match Langfuse's environment format: lowercase alphanumeric with optional hyphens or underscores, must be ≤40 characters, and it must not start with "langfuse". Metadata - values are coerced to strings before the 200 character limit is applied. + values, strings included, are JSON-encoded the same way as + observation metadata (compact separators, non-ASCII kept as is, + integers outside the JavaScript safe range and NaN/Infinity become + JSON strings) before the 200 character limit is applied, so `"123"` + stays a string on the server. None values are dropped with a warning, + like observation metadata skips them. Invalid values will be dropped with a warning logged. - **OpenTelemetry**: This uses OpenTelemetry context propagation under the hood, making it compatible with other OTel-instrumented libraries. @@ -358,17 +367,6 @@ def _propagate_attributes( "metadata": metadata, } - if experiment: - for key, value in experiment.items(): - if key in ("experiment_metadata", "experiment_item_metadata"): - propagated_metadata_attributes[key] = cast( - Optional[Dict[str, str]], value - ) - else: - propagated_string_attributes[key] = cast( - Optional[Union[str, List[str]]], value - ) - # Filter out None values propagated_string_attributes = { k: v for k, v in propagated_string_attributes.items() if v is not None @@ -393,10 +391,20 @@ def _propagate_attributes( validated_metadata: Dict[str, str] = {} for key, value in metadata_value.items(): - coerced_value = value if isinstance(value, str) else str(value) + if value is None: + langfuse_logger.warning( + "Propagated attribute '%s.%s' is None. Dropping value.", + metadata_key, + key, + ) + continue + + serialized_value = _serialize_metadata_value(value) - if _validate_string_value(value=coerced_value, key=f"{metadata_key}.{key}"): - validated_metadata[key] = coerced_value + if _validate_string_value( + value=serialized_value, key=f"{metadata_key}.{key}" + ): + validated_metadata[key] = serialized_value if validated_metadata: context = _set_propagated_attribute( @@ -407,6 +415,19 @@ def _propagate_attributes( as_baggage=as_baggage, ) + # Experiment attributes are set by the SDK and already serialized, so they + # skip validation. Mirrors langfuse-js. + if experiment: + for experiment_key, experiment_value in experiment.items(): + if experiment_value is not None: + context = _set_propagated_attribute( + key=experiment_key, + value=cast(str, experiment_value), + context=context, + span=current_span, + as_baggage=as_baggage, + ) + # Activate context, execute, and detach context token = otel_context_api.attach(context=context) @@ -583,14 +604,13 @@ def _set_propagated_attribute( if span is not None and span.is_recording(): if isinstance(value, dict): # Handle metadata - for k, v in value.items(): - span.set_attribute( - key=f"{span_key}.{k}", - value=v, - ) - + span_attributes: Dict[str, Any] = { + f"{span_key}.{k}": v for k, v in value.items() + } else: - span.set_attribute(key=span_key, value=value) + span_attributes = {span_key: value} + + _set_span_attributes_within_limit(span, span_attributes) # Set on baggage if as_baggage: diff --git a/langfuse/_client/resource_manager.py b/langfuse/_client/resource_manager.py index 4395872db..b5e104812 100644 --- a/langfuse/_client/resource_manager.py +++ b/langfuse/_client/resource_manager.py @@ -8,7 +8,7 @@ - Thread-safe OpenTelemetry tracer with Langfuse-specific span processors and sampling - Configurable batch processing of spans and scores with intelligent flushing behavior - Asynchronous background media upload processing with dedicated worker threads -- Concurrent score ingestion with batching and retry mechanisms +- Background score delivery with batching and retry mechanisms - Automatic project ID discovery and caching - Graceful shutdown handling with proper resource cleanup - Fault tolerance with detailed error logging and recovery mechanisms @@ -43,10 +43,9 @@ from langfuse._client.span_processor import LangfuseSpanProcessor from langfuse._task_manager.media_manager import MediaManager from langfuse._task_manager.media_upload_consumer import MediaUploadConsumer -from langfuse._task_manager.score_ingestion_consumer import ScoreIngestionConsumer +from langfuse._task_manager.score_consumer import ScoreConsumer from langfuse._utils.environment import get_common_release_envs from langfuse._utils.prompt_cache import PromptCache -from langfuse._utils.request import LangfuseClient from langfuse.api import AsyncLangfuseAPI, LangfuseAPI from langfuse.logger import langfuse_logger from langfuse.types import MaskFunction, MaskOtelSpansFunction @@ -60,14 +59,14 @@ class LangfuseResourceManager: This class implements a thread-safe singleton pattern keyed by the public API key, ensuring that only one tracer instance exists per API key combination. It manages the lifecycle of the OpenTelemetry tracer provider, span processors, and resource - attributes, as well as background threads for media uploads and score ingestion. + attributes, as well as background threads for media uploads and score delivery. The tracer is responsible for: 1. Setting up the OpenTelemetry tracer with appropriate sampling and configuration 2. Managing the span processor for exporting spans to the Langfuse API 3. Creating and managing Langfuse API clients (both synchronous and asynchronous) 4. Handling background media upload processing via dedicated worker threads - 5. Processing and batching score ingestion events with configurable flush settings + 5. Batching scores for POST /api/public/scores with configurable flush settings 6. Retrieving and caching project information for URL generation and media handling 7. Coordinating graceful shutdown of all background processes with proper resource cleanup @@ -85,7 +84,7 @@ class LangfuseResourceManager: _otel_tracer: Tracer _media_manager: MediaManager _media_upload_consumers: List[MediaUploadConsumer] - _ingestion_consumers: List[ScoreIngestionConsumer] + _score_consumers: List[ScoreConsumer] @classmethod def get_singleton_httpx_client(cls) -> Optional[httpx.Client]: @@ -127,7 +126,6 @@ def __new__( mask: Optional[MaskFunction] = None, mask_otel_spans: Optional[MaskOtelSpansFunction] = None, tracing_enabled: Optional[bool] = None, - blocked_instrumentation_scopes: Optional[List[str]] = None, should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, @@ -166,7 +164,6 @@ def __new__( tracing_enabled=tracing_enabled if tracing_enabled is not None else True, - blocked_instrumentation_scopes=blocked_instrumentation_scopes, should_export_span=should_export_span, additional_headers=additional_headers, tracer_provider=tracer_provider, @@ -196,7 +193,6 @@ def _initialize_instance( mask: Optional[MaskFunction] = None, mask_otel_spans: Optional[MaskOtelSpansFunction] = None, tracing_enabled: bool = True, - blocked_instrumentation_scopes: Optional[List[str]] = None, should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, tracer_provider: Optional[TracerProvider] = None, @@ -220,7 +216,6 @@ def _initialize_instance( self.release = release self.media_upload_thread_count = media_upload_thread_count self.sample_rate = sample_rate - self.blocked_instrumentation_scopes = blocked_instrumentation_scopes self.should_export_span = should_export_span self.additional_headers = additional_headers self.id_generator = id_generator @@ -259,7 +254,6 @@ def _initialize_instance( timeout=timeout, flush_at=flush_at, flush_interval=flush_interval, - blocked_instrumentation_scopes=blocked_instrumentation_scopes, should_export_span=should_export_span, additional_headers=additional_headers, span_exporter=span_exporter, @@ -286,7 +280,7 @@ def _initialize_instance( # Register fork handler to reinitialize consumer threads in child process. # When using Gunicorn with --preload, os.fork() copies memory but not threads # (POSIX.1: https://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html). - # Without this, media upload and score ingestion threads are lost after fork, + # Without this, media upload and score threads are lost after fork, # causing silent data loss. # # Note: LangfuseSpanProcessor (BatchSpanProcessor) already handles fork-safety @@ -369,17 +363,9 @@ def _init_api_clients(self) -> None: x_langfuse_public_key=self.public_key, timeout=self.timeout, ) - self._score_ingestion_client = LangfuseClient( - public_key=self.public_key, - secret_key=self.secret_key, - base_url=self.base_url, - version=langfuse_version, - timeout=self.timeout or 20, - session=self.httpx_client, - ) def _init_consumer_threads(self) -> None: - """Initialize media upload and score ingestion consumer threads.""" + """Initialize media upload and score consumer threads.""" if self._media_upload_enabled: for i in range(self._media_upload_thread_count): media_upload_consumer = MediaUploadConsumer( @@ -389,21 +375,19 @@ def _init_consumer_threads(self) -> None: media_upload_consumer.start() self._media_upload_consumers.append(media_upload_consumer) - # Score ingestion - self._score_ingestion_queue: Queue[Any] = Queue(100_000) - self._ingestion_consumers = [] + self._score_queue: Queue[Any] = Queue(100_000) + self._score_consumers = [] - ingestion_consumer = ScoreIngestionConsumer( - ingestion_queue=self._score_ingestion_queue, + score_consumer = ScoreConsumer( + score_queue=self._score_queue, identifier=0, - client=self._score_ingestion_client, + api_client=self.api, flush_at=self.flush_at, flush_interval=self.flush_interval, max_retries=3, - public_key=self.public_key, ) - ingestion_consumer.start() - self._ingestion_consumers.append(ingestion_consumer) + score_consumer.start() + self._score_consumers.append(score_consumer) def _at_fork_reinit(self) -> None: """Reinitialize consumer threads after fork in child process. @@ -412,7 +396,7 @@ def _at_fork_reinit(self) -> None: Necessary for Gunicorn --preload deployments where os.fork() is used: threads are not copied to child processes (POSIX standard), so without reinitialization, the child process has no consumer threads and all - media upload and score ingestion events are silently lost. + media uploads and scores are silently lost. Note: LangfuseSpanProcessor (BatchSpanProcessor) handles span export fork-safety separately via its own os.register_at_fork handler. @@ -475,7 +459,7 @@ def _at_fork_reinit(self) -> None: except Exception as e: langfuse_logger.error( "[PID %s] Failed to reinitialize consumer threads after fork: %s. Media " - "upload, score ingestion, and prompt cache refresh will be unavailable in " + "upload, score delivery, and prompt cache refresh will be unavailable in " "this worker.", os.getpid(), e, @@ -495,84 +479,60 @@ def reset(cls) -> None: cls._instances.clear() - def add_score_task(self, event: dict, *, force_sample: bool = False) -> None: + def add_score_task(self, score: dict, *, force_sample: bool = False) -> None: + """Enqueue a score for POST /api/public/scores. + + Args: + score: Request body of a single score, keyed by API field names + (for example ``traceId``). + force_sample: Enqueue the score even if its trace is sampled out. + """ try: - # Sample scores with the same sampler that is used for tracing - tracer_provider = cast(TracerProvider, otel_trace_api.get_tracer_provider()) + trace_id = score.get("traceId") + tracer_provider = ( + self.tracer_provider or otel_trace_api.get_tracer_provider() + ) + sampler = getattr(tracer_provider, "sampler", None) should_sample = ( force_sample - or isinstance( - tracer_provider, otel_trace_api.ProxyTracerProvider - ) # default to in-sample if otel sampler is not available - or ( - ( - tracer_provider.sampler.should_sample( - parent_context=None, - trace_id=int(event["body"].trace_id, 16), - name="score", - ).decision - == Decision.RECORD_AND_SAMPLE - if hasattr(event["body"], "trace_id") - else True - ) - if event["body"].trace_id - is not None # do not sample out session / dataset run scores - else True - ) + # do not sample out session / dataset run scores + or trace_id is None + # Only a trace-id ratio sampler gives the trace's actual decision + # from the trace id alone. Parent-based, random or attribute-based + # samplers may have recorded the trace, so keep its scores. + or not isinstance(sampler, TraceIdRatioBased) + or sampler.should_sample( + parent_context=None, + trace_id=int(trace_id, 16), + name="score", + ).decision + == Decision.RECORD_AND_SAMPLE ) if should_sample: langfuse_logger.debug( - "Score: Enqueuing event type=%s for trace_id=%s name=%s value=%s", - event["type"], - event["body"].trace_id, - event["body"].name, - event["body"].value, + "Score: Enqueuing score for trace_id=%s name=%s value=%s", + trace_id, + score.get("name"), + score.get("value"), ) - self._score_ingestion_queue.put(event, block=False) + self._score_queue.put(score, block=False) except Full: langfuse_logger.warning( - "System overload: Score ingestion queue has reached capacity (100,000 items). Score will be dropped. Consider increasing flush frequency or decreasing event volume." + "System overload: Score queue has reached capacity (100,000 items). Score will be dropped. Consider increasing flush frequency or decreasing event volume." ) return except Exception as e: langfuse_logger.error( - "Unexpected error: Failed to process score event. The score will be " + "Unexpected error: Failed to process score. The score will be " "dropped. Error details: %s", e, ) return - def add_trace_task( - self, - event: dict, - ) -> None: - try: - langfuse_logger.debug( - "Trace: Enqueuing event type=%s for trace_id=%s", - event["type"], - event["body"].id, - ) - self._score_ingestion_queue.put(event, block=False) - - except Full: - langfuse_logger.warning( - "System overload: Trace ingestion queue has reached capacity (100,000 items). Trace update will be dropped. Consider increasing flush frequency or decreasing event volume." - ) - - return - except Exception as e: - langfuse_logger.error( - "Unexpected error: Failed to process trace event. The trace update will " - "be dropped. Error details: %s", - e, - ) - - return - @property def tracer(self) -> Optional[Tracer]: return self._otel_tracer @@ -608,22 +568,22 @@ def _stop_and_join_consumer_threads(self) -> None: ) langfuse_logger.debug( - "Shutdown: Waiting for %s score ingestion thread(s) to complete processing", - len(self._ingestion_consumers), + "Shutdown: Waiting for %s score thread(s) to complete processing", + len(self._score_consumers), ) - for score_ingestion_consumer in self._ingestion_consumers: - score_ingestion_consumer.pause() + for score_consumer in self._score_consumers: + score_consumer.pause() - for score_ingestion_consumer in self._ingestion_consumers: + for score_consumer in self._score_consumers: try: - score_ingestion_consumer.join() + score_consumer.join() except RuntimeError: # consumer thread has not started pass langfuse_logger.debug( - "Shutdown: Score ingestion thread #%s successfully terminated", - score_ingestion_consumer._identifier, + "Shutdown: Score thread #%s successfully terminated", + score_consumer._identifier, ) def flush(self) -> None: @@ -633,8 +593,8 @@ def flush(self) -> None: self.tracer_provider.force_flush() langfuse_logger.debug("Successfully flushed OTEL tracer provider") - self._score_ingestion_queue.join() - langfuse_logger.debug("Successfully flushed score ingestion queue") + self._score_queue.join() + langfuse_logger.debug("Successfully flushed score queue") self._media_upload_queue.join() langfuse_logger.debug("Successfully flushed media upload queue") diff --git a/langfuse/_client/span.py b/langfuse/_client/span.py index 96879499c..ffdcd8c55 100644 --- a/langfuse/_client/span.py +++ b/langfuse/_client/span.py @@ -19,8 +19,10 @@ TYPE_CHECKING, Any, Dict, + List, Literal, Optional, + Tuple, Type, Union, cast, @@ -36,7 +38,6 @@ if TYPE_CHECKING: from langfuse._client.client import Langfuse -from typing_extensions import deprecated from langfuse._client.attributes import ( LangfuseOtelSpanAttributes, @@ -45,6 +46,7 @@ create_trace_attributes, ) from langfuse._client.constants import ( + MASK_FALLBACK_VALUE, ObservationTypeGenerationLike, ObservationTypeLiteral, ObservationTypeLiteralNoEvent, @@ -62,6 +64,191 @@ _OBSERVATION_CLASS_MAP: Dict[str, Type["LangfuseObservationWrapper"]] = {} +_METADATA_KEY_PREFIXES: Tuple[str, ...] = ( + LangfuseOtelSpanAttributes.OBSERVATION_METADATA, + LangfuseOtelSpanAttributes.TRACE_METADATA, +) + + +def _is_metadata_key(key: str) -> bool: + return any( + key == prefix or key.startswith(prefix + ".") + for prefix in _METADATA_KEY_PREFIXES + ) + + +# Attributes the SDK may write in later updates of an observation. Metadata +# leaves room for them, so that a later update (e.g. the output) still fits +# into the span. Span-like observations and events only use the shared keys; +# generation-like observations, and writes whose observation type is unknown, +# also reserve the model keys. Mirrors langfuse-js. +_RESERVED_SPAN_ATTRIBUTE_KEYS: Tuple[str, ...] = ( + LangfuseOtelSpanAttributes.OBSERVATION_TYPE, + LangfuseOtelSpanAttributes.OBSERVATION_LEVEL, + LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE, + LangfuseOtelSpanAttributes.VERSION, + LangfuseOtelSpanAttributes.ENVIRONMENT, + LangfuseOtelSpanAttributes.OBSERVATION_INPUT, + LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, +) +_RESERVED_OBSERVATION_ATTRIBUTE_KEYS: Tuple[str, ...] = ( + *_RESERVED_SPAN_ATTRIBUTE_KEYS, + LangfuseOtelSpanAttributes.OBSERVATION_MODEL, + LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS, + LangfuseOtelSpanAttributes.OBSERVATION_COST_DETAILS, + LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME, + LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS, + LangfuseOtelSpanAttributes.OBSERVATION_PROMPT_NAME, + LangfuseOtelSpanAttributes.OBSERVATION_PROMPT_VERSION, +) + + +def _reserved_attribute_keys(observation_type: Optional[str]) -> Tuple[str, ...]: + if ( + observation_type in get_observation_types_list(ObservationTypeSpanLike) + or observation_type == "event" + ): + return _RESERVED_SPAN_ATTRIBUTE_KEYS + + return _RESERVED_OBSERVATION_ATTRIBUTE_KEYS + + +def _drop_attributes_over_span_limit( + span: otel_trace_api.Span, + attributes: Dict[str, Any], + reserved_keys: Tuple[str, ...] = _RESERVED_OBSERVATION_ATTRIBUTE_KEYS, +) -> Dict[str, Any]: + """Drop new attributes that would exceed the span's attribute limit. + + Once a span is full, the Python OTel SDK evicts the oldest attribute, which + would drop Langfuse core attributes (input, model, ...) written earlier. + OTel JS drops the newest attribute instead; this helper gives Python the + same semantics so existing attributes are never evicted. + + 1. Metadata budget: counts the attributes already on the span, the new + keys, and the reserved keys not yet on the span, which later updates + may still write. Excess new metadata keys are dropped from the tail. + ``reserved_keys`` defaults to the full generation set, the safe choice + when the observation type is unknown. + 2. Hard guard: if the remaining new keys still exceed the physical + capacity (limit minus attributes already on the span), the excess new + keys are dropped from the tail. Reserved slots do not apply here, so + reserved attributes such as the output still land. + + Overwrites of existing keys are always kept, as they need no extra slot. + At most one warning is logged per write. Spans without SDK limits (e.g. + non-recording spans) are left untouched. Never raises. + """ + try: + max_attributes = getattr( + getattr(span, "_limits", None), "max_span_attributes", None + ) + if not isinstance(max_attributes, int): + return attributes + + existing = getattr(span, "attributes", None) or {} + new_keys = [ + key + for key, value in attributes.items() + if value is not None and key not in existing + ] + new_key_set = set(new_keys) + reserved_count = sum( + 1 for key in reserved_keys if key not in existing and key not in new_key_set + ) + if len(existing) + reserved_count + len(new_keys) <= max_attributes: + return attributes + + new_metadata_keys = [key for key in new_keys if _is_metadata_key(key)] + free_metadata_slots = max( + 0, + max_attributes + - len(existing) + - reserved_count + - (len(new_keys) - len(new_metadata_keys)), + ) + dropped_metadata = new_metadata_keys[free_metadata_slots:] + dropped_metadata_set = set(dropped_metadata) + + remaining_new_keys = [ + key for key in new_keys if key not in dropped_metadata_set + ] + capacity = max(0, max_attributes - len(existing)) + dropped_other = remaining_new_keys[capacity:] + # The metadata budget is never looser than the hard guard, so only + # non-metadata keys can be dropped here. Count any metadata defensively. + dropped_metadata += [key for key in dropped_other if _is_metadata_key(key)] + dropped_other = [key for key in dropped_other if not _is_metadata_key(key)] + + if not dropped_metadata and not dropped_other: + return attributes + + _warn_dropped_attributes( + span_name=getattr(span, "name", None), + max_attributes=max_attributes, + dropped_metadata=dropped_metadata, + dropped_other=dropped_other, + ) + + dropped_keys = set(dropped_metadata) | set(dropped_other) + return { + key: value for key, value in attributes.items() if key not in dropped_keys + } + except Exception as e: + langfuse_logger.debug("Failed to apply span attribute limit: %s", e) + return attributes + + +def _warn_dropped_attributes( + *, + span_name: Optional[str], + max_attributes: int, + dropped_metadata: List[str], + dropped_other: List[str], +) -> None: + metadata_prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + "." + dropped_keys = ", ".join( + ( + [key.removeprefix(metadata_prefix) for key in dropped_metadata] + + dropped_other + )[:5] + ) + if not dropped_other: + langfuse_logger.warning( + "Dropped %s metadata key(s) from observation '%s' to stay within the " + "span attribute limit of %s (SpanLimits.max_span_attributes / " + "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT). Dropped keys include: %s", + len(dropped_metadata), + span_name, + max_attributes, + dropped_keys, + ) + return + + langfuse_logger.warning( + "Dropped %s metadata key(s) and %s other attribute(s) from observation " + "'%s' to stay within the span attribute limit of %s " + "(SpanLimits.max_span_attributes / OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT). " + "Dropped keys include: %s", + len(dropped_metadata), + len(dropped_other), + span_name, + max_attributes, + dropped_keys, + ) + + +def _set_span_attributes_within_limit( + span: otel_trace_api.Span, + attributes: Dict[str, Any], + reserved_keys: Tuple[str, ...] = _RESERVED_OBSERVATION_ATTRIBUTE_KEYS, +) -> None: + """Set attributes on a span without evicting existing attributes.""" + span.set_attributes( + _drop_attributes_over_span_limit(span, attributes, reserved_keys) + ) + + class LangfuseObservationWrapper: """Abstract base class for all Langfuse span types. @@ -122,8 +309,11 @@ def __init__( prompt: Associated prompt template from Langfuse prompt management """ self._otel_span = otel_span - self._otel_span.set_attribute( - LangfuseOtelSpanAttributes.OBSERVATION_TYPE, as_type + reserved_keys = _reserved_attribute_keys(as_type) + _set_span_attributes_within_limit( + self._otel_span, + {LangfuseOtelSpanAttributes.OBSERVATION_TYPE: as_type}, + reserved_keys, ) self._langfuse_client = langfuse_client self._observation_type = as_type @@ -138,14 +328,18 @@ def __init__( existing_environment or environment or self._langfuse_client._environment ) if self._environment is not None: - self._otel_span.set_attribute( - LangfuseOtelSpanAttributes.ENVIRONMENT, self._environment + _set_span_attributes_within_limit( + self._otel_span, + {LangfuseOtelSpanAttributes.ENVIRONMENT: self._environment}, + reserved_keys, ) self._release = release or self._langfuse_client._release if self._release is not None: - self._otel_span.set_attribute( - LangfuseOtelSpanAttributes.RELEASE, self._release + _set_span_attributes_within_limit( + self._otel_span, + {LangfuseOtelSpanAttributes.RELEASE: self._release}, + reserved_keys, ) # Handle media only if span is sampled @@ -204,8 +398,10 @@ def __init__( # We don't want to overwrite the observation type, and already set it attributes.pop(LangfuseOtelSpanAttributes.OBSERVATION_TYPE, None) - self._otel_span.set_attributes( - {k: v for k, v in attributes.items() if v is not None} + _set_span_attributes_within_limit( + self._otel_span, + {k: v for k, v in attributes.items() if v is not None}, + reserved_keys, ) # Set OTEL span status if level is ERROR self._set_otel_span_status_if_error( @@ -226,53 +422,6 @@ def end(self, *, end_time: Optional[int] = None) -> "LangfuseObservationWrapper" return self - @deprecated( - "Trace-level input/output is deprecated. " - "For trace attributes (user_id, session_id, tags, etc.), use propagate_attributes() instead. " - "This method will be removed in a future major version." - ) - def set_trace_io( - self, - *, - input: Optional[Any] = None, - output: Optional[Any] = None, - ) -> "LangfuseObservationWrapper": - """Set trace-level input and output for the trace this span belongs to. - - .. deprecated:: - This is a legacy method for backward compatibility with Langfuse platform - features that still rely on trace-level input/output (e.g., legacy LLM-as-a-judge - evaluators). It will be removed in a future major version. - - For setting other trace attributes (user_id, session_id, metadata, tags, version), - use :func:`langfuse.propagate_attributes` (top-level import) instead. - - Args: - input: Input data to associate with the trace. - output: Output data to associate with the trace. - - Returns: - The span instance for method chaining. - """ - if not self._otel_span.is_recording(): - return self - - media_processed_input = self._process_media_and_apply_mask( - data=input, field="input", span=self._otel_span - ) - media_processed_output = self._process_media_and_apply_mask( - data=output, field="output", span=self._otel_span - ) - - attributes = create_trace_attributes( - input=media_processed_input, - output=media_processed_output, - ) - - self._otel_span.set_attributes(attributes) - - return self - def set_trace_as_public(self) -> "LangfuseObservationWrapper": """Make this trace publicly accessible via its URL. @@ -289,7 +438,11 @@ def set_trace_as_public(self) -> "LangfuseObservationWrapper": attributes = create_trace_attributes(public=True) - self._otel_span.set_attributes(attributes) + _set_span_attributes_within_limit( + self._otel_span, + attributes, + _reserved_attribute_keys(self._observation_type), + ) return self @@ -529,7 +682,11 @@ def _set_processed_span_attributes( ) ) - span.set_attributes(media_processed_attributes) + _set_span_attributes_within_limit( + span, + media_processed_attributes, + _reserved_attribute_keys(as_type or self._observation_type), + ) def _process_media_and_apply_mask( self, @@ -551,11 +708,21 @@ def _process_media_and_apply_mask( Returns: The processed and masked data """ + if data is None: + return None + return self._mask_attribute( - data=self._process_media_in_attribute(data=data, field=field) + data=self._process_media_in_attribute(data=data, field=field), field=field ) - def _mask_attribute(self, *, data: Any) -> Any: + def _mask_attribute( + self, + *, + data: Any, + field: Optional[ + Union[Literal["input"], Literal["output"], Literal["metadata"]] + ] = None, + ) -> Any: """Apply the configured mask function to data. Internal method that applies the client's configured masking function to @@ -563,6 +730,7 @@ def _mask_attribute(self, *, data: Any) -> Any: Args: data: The data to mask + field: The attribute the data belongs to Returns: The masked data, or the original data if no mask is configured @@ -579,7 +747,13 @@ def _mask_attribute(self, *, data: Any) -> Any: e, ) - return "" + # Dict metadata is written per key, so mask each key instead of + # writing a plain string to the bare metadata attribute. An empty dict + # has no keys to mask, so it keeps the plain string to stay visible + if field == "metadata" and isinstance(data, dict) and data: + return {key: MASK_FALLBACK_VALUE for key in data} + + return MASK_FALLBACK_VALUE def _process_media_in_attribute( self, @@ -729,7 +903,11 @@ def update( ), ) - self._otel_span.set_attributes(attributes=attributes) + _set_span_attributes_within_limit( + self._otel_span, + attributes, + _reserved_attribute_keys(self._observation_type), + ) # Set OTEL span status if level is ERROR self._set_otel_span_status_if_error(level=level, status_message=status_message) diff --git a/langfuse/_client/span_exporter.py b/langfuse/_client/span_exporter.py index 661a1a26c..ccad5e843 100644 --- a/langfuse/_client/span_exporter.py +++ b/langfuse/_client/span_exporter.py @@ -27,7 +27,6 @@ _INPUT_MEDIA_ATTRIBUTE_KEYS = frozenset( { - LangfuseOtelSpanAttributes.TRACE_INPUT, LangfuseOtelSpanAttributes.OBSERVATION_INPUT, "ai.prompt.messages", "ai.prompt", @@ -54,7 +53,6 @@ _OUTPUT_MEDIA_ATTRIBUTE_KEYS = frozenset( { - LangfuseOtelSpanAttributes.TRACE_OUTPUT, LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT, "ai.response.text", "ai.result.text", @@ -90,6 +88,9 @@ "llm.output_messages", ) +# Includes the leading quote of a JSON-encoded data URI. +_MAX_DATA_URI_HEADER_LENGTH = 256 + class LangfuseTransformingSpanExporter(SpanExporter): """Apply Langfuse export-stage transformations before delegating export.""" @@ -213,6 +214,13 @@ def _process_media_string( attribute_key: str, value: str, ) -> str: + if value.startswith('"data:') and _is_langfuse_metadata_attribute( + attribute_key + ): + return self._process_json_encoded_media_string( + span=span, attribute_key=attribute_key, value=value + ) + media_manager = cast(MediaManager, self._media_manager) field = _media_field_for_attribute(attribute_key) @@ -262,6 +270,40 @@ def _process_media_string( return _serialize_media_value(processed_json_value, fallback=value) + def _process_json_encoded_media_string( + self, + *, + span: ReadableSpan, + attribute_key: str, + value: str, + ) -> str: + # Langfuse metadata values are JSON-encoded, so a top-level data URI + # arrives quoted. A JSON string without backslashes holds its characters + # literally, so stripping the outer quotes yields the data URI without + # json.loads. The media reference is built from that backslash- and + # quote-free data URI, so quoting it again is valid JSON. Escaped or + # malformed values are left unchanged. + header_end = value.find(",", 0, _MAX_DATA_URI_HEADER_LENGTH) + + if ( + header_end == -1 + or not value.endswith('"') + or not value[:header_end].endswith(";base64") + or "\\" in value + or value.find('"', 1, -1) != -1 + ): + return value + + data_uri = value[1:-1] + processed_value = self._process_media_string( + span=span, attribute_key=attribute_key, value=data_uri + ) + + if processed_value == data_uri: + return value + + return f'"{processed_value}"' + def _apply_mask_otel_spans( self, *, @@ -558,6 +600,16 @@ def _is_attribute_sequence(value: AttributeValue) -> bool: return isinstance(value, SequenceCollection) and not isinstance(value, (str, bytes)) +def _is_langfuse_metadata_attribute(attribute_key: str) -> bool: + return any( + attribute_key == prefix or attribute_key.startswith(f"{prefix}.") + for prefix in ( + LangfuseOtelSpanAttributes.OBSERVATION_METADATA, + LangfuseOtelSpanAttributes.TRACE_METADATA, + ) + ) + + def _is_base64_data_uri(value: str) -> bool: if not value.startswith("data:") or "," not in value: return False diff --git a/langfuse/_client/span_filter.py b/langfuse/_client/span_filter.py index 071ff48ce..cf3718545 100644 --- a/langfuse/_client/span_filter.py +++ b/langfuse/_client/span_filter.py @@ -78,7 +78,7 @@ def is_genai_span(span: ReadableSpan) -> bool: return False return any( - isinstance(key, str) and key.startswith("gen_ai") + isinstance(key, str) and key.startswith("gen_ai.") for key in span.attributes.keys() ) diff --git a/langfuse/_client/span_processor.py b/langfuse/_client/span_processor.py index b362b8da1..438551fd6 100644 --- a/langfuse/_client/span_processor.py +++ b/langfuse/_client/span_processor.py @@ -15,12 +15,16 @@ import logging import os import threading -from typing import Callable, Dict, List, Literal, Optional, cast +from typing import Callable, Dict, Literal, Optional, cast from opentelemetry import context as context_api from opentelemetry.context import Context from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_COMPRESSION, + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, +) from opentelemetry.sdk.trace import ReadableSpan, Span from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.trace import format_span_id, format_trace_id @@ -37,6 +41,7 @@ _get_langfuse_trace_id_from_baggage, _get_propagated_attributes_from_context, ) +from langfuse._client.span import _set_span_attributes_within_limit from langfuse._client.span_exporter import LangfuseTransformingSpanExporter from langfuse._client.span_filter import ( is_app_root_eligible, @@ -71,23 +76,18 @@ def _resolve_max_batch_size_bytes() -> Optional[int]: _COMPRESSION_BY_NAME = {"gzip": Compression.Gzip, "none": Compression.NoCompression} -def _resolve_compression(otel_compression: Optional[str]) -> Optional[Compression]: - """Return the configured compression, or None to defer to OTEL_EXPORTER_OTLP_*COMPRESSION.""" - setting = "otel_compression" - raw_value = otel_compression - if raw_value is None: - setting = LANGFUSE_OTEL_COMPRESSION - raw_value = os.environ.get(LANGFUSE_OTEL_COMPRESSION, "") - - value = raw_value.strip().lower() +def _parse_compression( + raw_value: Optional[str], *, setting: str, warn: bool +) -> Optional[Compression]: + value = (raw_value or "").strip().lower() if not value: return None compression = _COMPRESSION_BY_NAME.get(value) - if compression is None: + if compression is None and warn: langfuse_logger.warning( - "Invalid %s=%r. Expected 'gzip' or 'none'. Falling back to the " - "OTEL_EXPORTER_OTLP_*COMPRESSION environment variables.", + "Invalid %s=%r. Expected 'gzip' or 'none'. Falling back to the next " + "compression setting, then gzip.", setting, raw_value, ) @@ -95,6 +95,36 @@ def _resolve_compression(otel_compression: Optional[str]) -> Optional[Compressio return compression +def _resolve_compression(otel_compression: Optional[str]) -> Compression: + """Resolve compression for the default exporter; gzip unless configured otherwise. + + Order: the ``otel_compression`` argument, ``LANGFUSE_OTEL_COMPRESSION``, + ``OTEL_EXPORTER_OTLP_TRACES_COMPRESSION``, ``OTEL_EXPORTER_OTLP_COMPRESSION``, + then gzip. Values are case-insensitive, and an invalid value falls through to + the next setting, matching the JS SDK. + """ + candidates = ( + ("otel_compression", otel_compression, True), + (LANGFUSE_OTEL_COMPRESSION, os.environ.get(LANGFUSE_OTEL_COMPRESSION), True), + ( + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION, + os.environ.get(OTEL_EXPORTER_OTLP_TRACES_COMPRESSION), + False, + ), + ( + OTEL_EXPORTER_OTLP_COMPRESSION, + os.environ.get(OTEL_EXPORTER_OTLP_COMPRESSION), + False, + ), + ) + for setting, raw_value, warn in candidates: + compression = _parse_compression(raw_value, setting=setting, warn=warn) + if compression is not None: + return compression + + return Compression.Gzip + + class LangfuseSpanProcessor(BatchSpanProcessor): """OpenTelemetry span processor that exports spans to the Langfuse API. @@ -121,7 +151,6 @@ def __init__( timeout: Optional[int] = None, flush_at: Optional[int] = None, flush_interval: Optional[float] = None, - blocked_instrumentation_scopes: Optional[List[str]] = None, should_export_span: Optional[Callable[[ReadableSpan], bool]] = None, additional_headers: Optional[Dict[str, str]] = None, span_exporter: Optional[SpanExporter] = None, @@ -130,11 +159,6 @@ def __init__( otel_compression: Optional[Literal["gzip", "none"]] = None, ): self.public_key = public_key - self.blocked_instrumentation_scopes = ( - blocked_instrumentation_scopes - if blocked_instrumentation_scopes is not None - else [] - ) self._should_export_span = should_export_span or is_default_export_span self._app_root_lock = threading.Lock() @@ -159,6 +183,7 @@ def __init__( "x-langfuse-sdk-name": "python", "x-langfuse-sdk-version": langfuse_version, "x-langfuse-public-key": public_key, + "x-langfuse-ingestion-version": "4", } # Merge additional headers if provided @@ -219,7 +244,7 @@ def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None } if propagated_attributes: - span.set_attributes(propagated_attributes) + _set_span_attributes_within_limit(span, propagated_attributes) langfuse_logger.debug( "Propagated %s attributes to span '%s': %s", @@ -257,16 +282,6 @@ def on_end(self, span: ReadableSpan) -> None: ) return - # Do not export spans from blocked instrumentation scopes - if self._is_blocked_instrumentation_scope(span): - langfuse_logger.debug( - "Trace: Dropping span due to blocked instrumentation scope | " - "span_name='%s' | instrumentation_scope='%s'", - span.name, - self._get_scope_name(span), - ) - return - # Apply custom or default span filter try: should_export = self._should_export_span(span) @@ -325,7 +340,9 @@ def _mark_app_root_candidate(self, *, span: Span, parent_context: Context) -> No ) if mark_app_root: - span.set_attribute(LangfuseOtelSpanAttributes.IS_APP_ROOT, True) + _set_span_attributes_within_limit( + span, {LangfuseOtelSpanAttributes.IS_APP_ROOT: True} + ) def _cleanup_app_root_state(self, span: ReadableSpan) -> None: span_id = format_span_id(span.context.span_id) @@ -341,9 +358,6 @@ def _is_expected_exported_at_start(self, span: Span) -> bool: ): return False - if self._is_blocked_instrumentation_scope(readable_span): - return False - try: return bool(self._should_export_span(readable_span)) except Exception as error: @@ -358,12 +372,6 @@ def _is_expected_exported_at_start(self, span: Span) -> bool: return False - def _is_blocked_instrumentation_scope(self, span: ReadableSpan) -> bool: - return ( - span.instrumentation_scope is not None - and span.instrumentation_scope.name in self.blocked_instrumentation_scopes - ) - def _is_langfuse_project_span(self, span: ReadableSpan) -> bool: if not is_langfuse_span(span): return False diff --git a/langfuse/_task_manager/score_consumer.py b/langfuse/_task_manager/score_consumer.py new file mode 100644 index 000000000..cdeb81404 --- /dev/null +++ b/langfuse/_task_manager/score_consumer.py @@ -0,0 +1,245 @@ +import json +import os +import threading +import time +from queue import Empty, Full, Queue +from typing import Any, Dict, List, Optional, cast + +import backoff +import httpx + +from langfuse._utils.parse_error import handle_fern_exception +from langfuse._utils.serializer import EventSerializer +from langfuse.api import LangfuseAPI +from langfuse.api.core import ApiError +from langfuse.api.scores.types.create_score_batch_results import ( + CreateScoreBatchResults, +) +from langfuse.api.scores.types.create_scores_request import CreateScoresRequest +from langfuse.logger import langfuse_logger as logger + +# POST /api/public/scores uses the default Next.js body limit of 1 MiB. +_SCORES_BODY_LIMIT_BYTES = 1_000_000 +_configured_batch_size_bytes = int( + os.environ.get("LANGFUSE_MAX_BATCH_SIZE_BYTES", _SCORES_BODY_LIMIT_BYTES) +) +if _configured_batch_size_bytes > _SCORES_BODY_LIMIT_BYTES: + logger.warning( + "Configuration: LANGFUSE_MAX_BATCH_SIZE_BYTES=%s exceeds the %s byte body " + "limit of POST /api/public/scores; score batches are capped at %s bytes.", + _configured_batch_size_bytes, + _SCORES_BODY_LIMIT_BYTES, + _SCORES_BODY_LIMIT_BYTES, + ) +MAX_BATCH_SIZE_BYTES = min(_configured_batch_size_bytes, _SCORES_BODY_LIMIT_BYTES) +_SHUTDOWN_SENTINEL = object() + + +def _is_retryable(error: Exception) -> bool: + if isinstance(error, httpx.TransportError): + return True + + if isinstance(error, ApiError): + status = error.status_code + return status is None or status >= 500 or status == 429 + + return False + + +class ScoreConsumer(threading.Thread): + """Send queued scores to POST /api/public/scores in batches.""" + + def __init__( + self, + *, + score_queue: Queue, + identifier: int, + api_client: LangfuseAPI, + flush_at: Optional[int] = None, + flush_interval: Optional[float] = None, + max_retries: Optional[int] = None, + ): + """Create a consumer thread.""" + super().__init__() + # It's important to set running in the constructor: if we are asked to + # pause immediately after construction, we might set running to True in + # run() *after* we set it to False in pause... and keep running + # forever. + self.running = True + # Make consumer a daemon thread so that it doesn't block program exit + self.daemon = True + self._score_queue = score_queue + self._identifier = identifier + self._api_client = api_client + self._flush_at = flush_at or 15 + self._flush_interval = flush_interval or 1 + self._max_retries = max_retries or 3 + # A score taken from the queue that did not fit into the previous batch. + # It has not been acknowledged yet and starts the next batch. + self._carry_over: Optional[Dict[str, Any]] = None + + def _next(self) -> List[Dict[str, Any]]: + """Return the next batch of JSON-ready scores to upload.""" + scores: List[Dict[str, Any]] = [] + total_size = 2 # enclosing brackets of the JSON list + + if self._carry_over is not None: + scores.append(self._carry_over) + total_size += self._get_item_size(self._carry_over) + self._carry_over = None + + start_time = time.monotonic() + + while len(scores) < self._flush_at: + elapsed = time.monotonic() - start_time + if elapsed >= self._flush_interval: + break + try: + score = self._score_queue.get( + block=True, timeout=self._flush_interval - elapsed + ) + + if score is _SHUTDOWN_SENTINEL: + self._score_queue.task_done() + break + + try: + serialized = json.loads(json.dumps(score, cls=EventSerializer)) + except Exception as e: + logger.error( + "Data error: Failed to serialize score. Score will be " + "dropped. Error: %s", + e, + ) + self._score_queue.task_done() + + continue + + item_size = self._get_item_size(serialized) + + if item_size + 2 > MAX_BATCH_SIZE_BYTES: + logger.error( + "Data error: Score name=%s is %s bytes, which exceeds the " + "request limit of %s bytes. Score will be dropped.", + serialized.get("name"), + item_size, + MAX_BATCH_SIZE_BYTES, + ) + self._score_queue.task_done() + + continue + + # +1 for the separating comma + if scores and total_size + item_size + 1 > MAX_BATCH_SIZE_BYTES: + logger.debug( + "Batch management: Reached maximum batch size limit (%s bytes). " + "Processing %s scores now.", + total_size, + len(scores), + ) + self._carry_over = serialized + break + + scores.append(serialized) + total_size += item_size + (1 if len(scores) > 1 else 0) + + except Empty: + break + + except Exception as e: + logger.warning( + "Data processing error: Failed to process score in consumer thread " + "#%s. Score will be dropped. Error: %s", + self._identifier, + str(e), + exc_info=True, + ) + self._score_queue.task_done() + + return scores + + def _get_item_size(self, item: Any) -> int: + """Return the size of the item in bytes.""" + return len(json.dumps(item).encode()) + + def run(self) -> None: + """Run the consumer.""" + logger.debug( + "Startup: Score consumer thread #%s started with batch size %s " + "and interval %ss", + self._identifier, + self._flush_at, + self._flush_interval, + ) + while self.running: + self.upload() + + while self._carry_over is not None: + self.upload() + + def upload(self) -> None: + """Upload the next batch of scores.""" + batch = self._next() + if len(batch) == 0: + return + + try: + self._upload_batch(batch) + except ApiError as e: + handle_fern_exception(e) + except Exception as e: + logger.error( + "API error: Failed to send %s scores to Langfuse. Scores will be " + "dropped. Error: %s", + len(batch), + e, + ) + finally: + # mark items as acknowledged from queue + for _ in batch: + self._score_queue.task_done() + + def pause(self) -> None: + """Pause the consumer.""" + self.running = False + try: + self._score_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[Dict[str, Any]]) -> None: + logger.debug("API: Sending batch of %s scores to Langfuse API", len(batch)) + + @backoff.on_exception( + backoff.expo, + Exception, + max_tries=self._max_retries, + giveup=lambda e: not _is_retryable(e), + logger=None, + ) + def send_with_backoff() -> Any: + # The items are JSON-ready dicts with API field names. Unlike the + # generated request model, the API accepts any JSON value as score + # metadata, so the dicts are sent as they are. + return self._api_client.scores.create( + request=cast(CreateScoresRequest, batch), + # Retries are handled here so that transport errors are retried too. + request_options={"max_retries": 0}, + ) + + response = send_with_backoff() + + if isinstance(response, CreateScoreBatchResults): + logger.error( + "API error: Langfuse accepted %s and rejected %s of %s scores. " + "Rejected scores are not retried. Errors: %s", + response.accepted, + response.rejected, + len(batch), + "; ".join(error.message for error in response.errors), + ) + return + + logger.debug("API: Successfully sent %s scores to Langfuse API", len(batch)) diff --git a/langfuse/_task_manager/score_ingestion_consumer.py b/langfuse/_task_manager/score_ingestion_consumer.py deleted file mode 100644 index 1dce00f80..000000000 --- a/langfuse/_task_manager/score_ingestion_consumer.py +++ /dev/null @@ -1,198 +0,0 @@ -import json -import os -import threading -import time -from queue import Empty, Full, Queue -from typing import Any, List, Optional - -import backoff -from pydantic import BaseModel - -from langfuse._utils.parse_error import handle_exception -from langfuse._utils.request import APIError, LangfuseClient -from langfuse._utils.serializer import EventSerializer -from langfuse.logger import langfuse_logger as logger - -from .._version import __version__ as langfuse_version - -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): - batch_size: int - sdk_name: str - sdk_version: str - public_key: str - - -class ScoreIngestionConsumer(threading.Thread): - def __init__( - self, - *, - ingestion_queue: Queue, - identifier: int, - client: LangfuseClient, - public_key: str, - flush_at: Optional[int] = None, - flush_interval: Optional[float] = None, - max_retries: Optional[int] = None, - ): - """Create a consumer thread.""" - super().__init__() - # It's important to set running in the constructor: if we are asked to - # pause immediately after construction, we might set running to True in - # run() *after* we set it to False in pause... and keep running - # forever. - self.running = True - # Make consumer a daemon thread so that it doesn't block program exit - self.daemon = True - self._ingestion_queue = ingestion_queue - self._identifier = identifier - self._client = client - self._flush_at = flush_at or 15 - self._flush_interval = flush_interval or 1 - self._max_retries = max_retries or 3 - self._public_key = public_key - - def _next(self) -> list: - """Return the next batch of items to upload.""" - events: list = [] - - start_time = time.monotonic() - total_size = 0 - - while len(events) < self._flush_at: - elapsed = time.monotonic() - start_time - if elapsed >= self._flush_interval: - break - try: - event = self._ingestion_queue.get( - 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) - - item_size = self._get_item_size(event) - - # check for serialization errors - try: - json.dumps(event, cls=EventSerializer) - except Exception as e: - logger.error( - "Data error: Failed to serialize score object for ingestion. Score will " - "be dropped. Error: %s", - e, - ) - self._ingestion_queue.task_done() - - continue - - events.append(event) - - total_size += item_size - if total_size >= MAX_BATCH_SIZE_BYTES: - logger.debug( - "Batch management: Reached maximum batch size limit (%s bytes). " - "Processing %s events now.", - total_size, - len(events), - ) - break - - except Empty: - break - - except Exception as e: - logger.warning( - "Data processing error: Failed to process score event in consumer thread " - "#%s. Event will be dropped. Error: %s", - self._identifier, - str(e), - exc_info=True, - ) - self._ingestion_queue.task_done() - - return events - - def _get_item_size(self, item: Any) -> int: - """Return the size of the item in bytes.""" - return len(json.dumps(item, cls=EventSerializer).encode()) - - def run(self) -> None: - """Run the consumer.""" - logger.debug( - "Startup: Score ingestion consumer thread #%s started with batch size %s " - "and interval %ss", - self._identifier, - self._flush_at, - self._flush_interval, - ) - while self.running: - self.upload() - - def upload(self) -> None: - """Upload the next batch of items, return whether successful.""" - batch = self._next() - if len(batch) == 0: - return - - try: - self._upload_batch(batch) - except Exception as e: - handle_exception(e) - finally: - # mark items as acknowledged from queue - for _ in batch: - self._ingestion_queue.task_done() - - 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( - "API: Uploading batch of %s score events to Langfuse API", len(batch) - ) - - metadata = ScoreIngestionMetadata( - batch_size=len(batch), - sdk_name="python", - sdk_version=langfuse_version, - public_key=self._public_key, - ).model_dump() - - @backoff.on_exception( - backoff.expo, Exception, max_tries=self._max_retries, logger=None - ) - def execute_task_with_backoff(batch: List[Any]) -> None: - try: - self._client.batch_post(batch=batch, metadata=metadata) - except Exception as e: - if ( - isinstance(e, APIError) - and 400 <= int(e.status) < 500 - and int(e.status) != 429 # retry if rate-limited - ): - return - - raise e - - execute_task_with_backoff(batch) - logger.debug( - "API: Successfully sent %s score events to Langfuse API in batch mode", - len(batch), - ) diff --git a/langfuse/_utils/parse_error.py b/langfuse/_utils/parse_error.py index 2b9a7bd6f..e16a4eb71 100644 --- a/langfuse/_utils/parse_error.py +++ b/langfuse/_utils/parse_error.py @@ -1,12 +1,5 @@ -from typing import Union - -# our own api errors -from langfuse._utils.request import APIError, APIErrors - -# fern api errors from langfuse.api import ( AccessDeniedError, - Error, MethodNotAllowedError, NotFoundError, ServiceUnavailableError, @@ -44,7 +37,7 @@ } -def generate_error_message_fern(error: Error) -> str: +def generate_error_message_fern(error: ApiError) -> str: if isinstance(error, AccessDeniedError): return errorResponseByCode.get(403, defaultErrorResponse) elif isinstance(error, MethodNotAllowedError): @@ -66,34 +59,7 @@ def generate_error_message_fern(error: Error) -> str: return defaultErrorResponse # type: ignore -def handle_fern_exception(exception: Error) -> None: +def handle_fern_exception(exception: ApiError) -> None: logger.debug(exception) error_message = generate_error_message_fern(exception) logger.error(error_message) - - -def generate_error_message(exception: Union[APIError, APIErrors, Exception]) -> str: - if isinstance(exception, APIError): - status_code = ( - int(exception.status) - if isinstance(exception.status, str) - else exception.status - ) - return f"API error occurred: {errorResponseByCode.get(status_code, defaultErrorResponse)}" - elif isinstance(exception, APIErrors): - error_messages = [ - errorResponseByCode.get( - int(error.status) if isinstance(error.status, str) else error.status, - defaultErrorResponse, - ) - for error in exception.errors - ] - return "API errors occurred: " + "\n".join(error_messages) - else: - return defaultErrorResponse - - -def handle_exception(exception: Union[APIError, APIErrors, Exception]) -> None: - logger.debug(exception) - error_message = generate_error_message(exception) - logger.error(error_message) diff --git a/langfuse/_utils/request.py b/langfuse/_utils/request.py deleted file mode 100644 index 402d0b5a7..000000000 --- a/langfuse/_utils/request.py +++ /dev/null @@ -1,136 +0,0 @@ -"""@private""" - -import json -from base64 import b64encode -from typing import Any, List, Union - -import httpx - -from langfuse._utils.serializer import EventSerializer -from langfuse.logger import langfuse_logger as logger - - -class LangfuseClient: - _public_key: str - _secret_key: str - _base_url: str - _version: str - _timeout: int - _session: httpx.Client - - def __init__( - self, - public_key: str, - secret_key: str, - base_url: str, - version: str, - timeout: int, - session: httpx.Client, - ): - self._public_key = public_key - self._secret_key = secret_key - self._base_url = base_url - self._version = version - self._timeout = timeout - self._session = session - - def generate_headers(self) -> dict: - return { - "Authorization": "Basic " - + b64encode( - f"{self._public_key}:{self._secret_key}".encode("utf-8") - ).decode("ascii"), - "Content-Type": "application/json", - "x-langfuse-sdk-name": "python", - "x-langfuse-sdk-version": self._version, - "x-langfuse-public-key": self._public_key, - } - - def batch_post(self, **kwargs: Any) -> httpx.Response: - """Post the `kwargs` to the batch API endpoint for events""" - res = self.post(**kwargs) - - return self._process_response( - res, success_message="data uploaded successfully", return_json=False - ) - - def post(self, **kwargs: Any) -> httpx.Response: - """Post the `kwargs` to the API""" - url = self._remove_trailing_slash(self._base_url) + "/api/public/ingestion" - data = json.dumps(kwargs, cls=EventSerializer) - logger.debug("making request: %s to %s", data, url) - headers = self.generate_headers() - res = self._session.post( - url, content=data, headers=headers, timeout=self._timeout - ) - - if res.status_code == 200: - logger.debug("data uploaded successfully") - - return res - - def _remove_trailing_slash(self, url: str) -> str: - """Removes the trailing slash from a URL""" - if url.endswith("/"): - return url[:-1] - return url - - def _process_response( - self, res: httpx.Response, success_message: str, *, return_json: bool = True - ) -> Union[httpx.Response, Any]: - logger.debug("received response: %s", res.text) - if res.status_code in (200, 201): - logger.debug(success_message) - if return_json: - try: - return res.json() - except json.JSONDecodeError: - raise APIError(res.status_code, "Invalid JSON response received") - else: - return res - elif res.status_code == 207: - try: - payload = res.json() - errors = payload.get("errors", []) - if errors: - raise APIErrors( - [ - APIError( - error.get("status"), - error.get("message", "No message provided"), - error.get("error", "No error details provided"), - ) - for error in errors - ] - ) - else: - return res.json() if return_json else res - except json.JSONDecodeError: - raise APIError(res.status_code, "Invalid JSON response received") - - try: - payload = res.json() - raise APIError(res.status_code, payload) - except (KeyError, ValueError): - raise APIError(res.status_code, res.text) - - -class APIError(Exception): - def __init__(self, status: Union[int, str], message: str, details: Any = None): - self.message = message - self.status = status - self.details = details - - def __str__(self) -> str: - msg = "{0} ({1}): {2}" - return msg.format(self.message, self.status, self.details) - - -class APIErrors(Exception): - def __init__(self, errors: List[APIError]): - self.errors = errors - - def __str__(self) -> str: - errors = ", ".join(str(error) for error in self.errors) - - return f"[Langfuse] {errors}" diff --git a/langfuse/api/__init__.py b/langfuse/api/__init__.py index 275fdff7c..e4763032e 100644 --- a/langfuse/api/__init__.py +++ b/langfuse/api/__init__.py @@ -12,7 +12,6 @@ comments, commons, dataset_items, - dataset_run_items, datasets, evaluation_commons, evaluation_errors, @@ -21,8 +20,6 @@ experiments, feedback, health, - ingestion, - legacy, llm_connections, media, metrics, @@ -36,8 +33,6 @@ scim, score_configs, scores, - scores_v3, - sessions, trace, unstable, utils, @@ -81,11 +76,8 @@ from .commons import ( AccessDeniedError, BaseScore, - BaseScoreV1, BooleanScore, - BooleanScoreV1, CategoricalScore, - CategoricalScoreV1, Comment, CommentObjectType, ConfigCategory, @@ -96,9 +88,6 @@ DatasetItemMediaReference, DatasetItemMediaReferenceField, DatasetItemMediaReferenceMedia, - DatasetRun, - DatasetRunItem, - DatasetRunWithItems, DatasetStatus, Deprecation, Error, @@ -109,12 +98,9 @@ ModelUsageUnit, NotFoundError, NumericScore, - NumericScoreV1, - Observation, ObservationLevel, ObservationV2, - ObservationsView, - ObservationsViewSingle, + OpenAiUsage, PricingTier, PricingTierAttributeCondition, PricingTierAttributeSource, @@ -129,23 +115,12 @@ ScoreConfigDataType, ScoreDataType, ScoreSource, - ScoreV1, - ScoreV1_Boolean, - ScoreV1_Categorical, - ScoreV1_Numeric, - ScoreV1_Text, Score_Boolean, Score_Categorical, Score_Correction, Score_Numeric, Score_Text, - Session, - SessionWithTraces, TextScore, - TextScoreV1, - Trace, - TraceWithDetails, - TraceWithFullDetails, UnauthorizedError, Usage, ) @@ -154,13 +129,7 @@ DeleteDatasetItemResponse, PaginatedDatasetItems, ) - from .dataset_run_items import CreateDatasetRunItemRequest, PaginatedDatasetRunItems - from .datasets import ( - CreateDatasetRequest, - DeleteDatasetRunResponse, - PaginatedDatasetRuns, - PaginatedDatasets, - ) + from .datasets import CreateDatasetRequest, PaginatedDatasets from .evaluation_commons import ( ArrayOptionsEvaluationRuleFilter, BooleanEvaluationRuleFilter, @@ -288,50 +257,6 @@ SubmitFeedbackResponse, ) from .health import HealthResponse, ServiceUnavailableError - from .ingestion import ( - BaseEvent, - CreateEventBody, - CreateEventEvent, - CreateGenerationBody, - CreateGenerationEvent, - CreateObservationEvent, - CreateSpanBody, - CreateSpanEvent, - IngestionError, - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, - IngestionResponse, - IngestionSuccess, - IngestionUsage, - ObservationBody, - ObservationType, - OpenAiCompletionUsageSchema, - OpenAiResponseUsageSchema, - OpenAiUsage, - OptionalObservationBody, - ScoreBody, - ScoreEvent, - SdkLogBody, - SdkLogEvent, - TraceBody, - TraceEvent, - UpdateEventBody, - UpdateGenerationBody, - UpdateGenerationEvent, - UpdateObservationEvent, - UpdateSpanBody, - UpdateSpanEvent, - UsageDetails, - ) from .llm_connections import ( DeleteLlmConnectionResponse, LlmAdapter, @@ -427,31 +352,21 @@ UpdateScoreConfigRequest, ) from .scores import ( - CreateScoreRequest, - CreateScoreResponse, - CreateScoreSource, - GetScoresResponse, - GetScoresResponseData, - GetScoresResponseDataBoolean, - GetScoresResponseDataCategorical, - GetScoresResponseDataCorrection, - GetScoresResponseDataNumeric, - GetScoresResponseDataText, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Correction, - GetScoresResponseData_Numeric, - GetScoresResponseData_Text, - GetScoresResponseTraceData, - ) - from .scores_v3 import ( BaseScoreV3, BooleanScoreV3, CategoricalScoreV3, CorrectionScoreV3, + CreateScoreBatchError, + CreateScoreBatchResponse, + CreateScoreBatchResults, + CreateScoreRequest, + CreateScoreSource, + CreateScoresRequest, + CreateScoresResponse, GetScoresV3Meta, GetScoresV3Response, NumericScoreV3, + ScoreBody, ScoreSubjectExperimentV3, ScoreSubjectObservationV3, ScoreSubjectSessionV3, @@ -469,8 +384,7 @@ ScoreV3_Text, TextScoreV3, ) - from .sessions import PaginatedSessions - from .trace import DeleteTraceResponse, Sort, Traces + from .trace import DeleteTraceResponse _dynamic_imports: typing.Dict[str, str] = { "AccessDeniedError": ".commons", "AnnotationQueue": ".annotation_queues", @@ -486,11 +400,9 @@ "AsyncLangfuseAPI": ".client", "AuthenticationScheme": ".scim", "BadRequestError": ".evaluation_errors", - "BaseEvent": ".ingestion", "BasePrompt": ".prompts", "BaseScore": ".commons", - "BaseScoreV1": ".commons", - "BaseScoreV3": ".scores_v3", + "BaseScoreV3": ".scores", "BlobStorageExportFieldGroup": ".blob_storage_integrations", "BlobStorageExportFrequency": ".blob_storage_integrations", "BlobStorageExportMode": ".blob_storage_integrations", @@ -505,12 +417,10 @@ "BlobStorageSyncStatus": ".blob_storage_integrations", "BooleanEvaluationRuleFilter": ".evaluation_commons", "BooleanScore": ".commons", - "BooleanScoreV1": ".commons", - "BooleanScoreV3": ".scores_v3", + "BooleanScoreV3": ".scores", "BulkConfig": ".scim", "CategoricalScore": ".commons", - "CategoricalScoreV1": ".commons", - "CategoricalScoreV3": ".scores_v3", + "CategoricalScoreV3": ".scores", "CategoryOptionsEvaluationRuleFilter": ".evaluation_commons", "ChatMessage": ".prompts", "ChatMessageType": ".prompts", @@ -524,7 +434,7 @@ "ConfigCategory": ".commons", "ConflictError": ".evaluation_errors", "CorrectionScore": ".commons", - "CorrectionScoreV3": ".scores_v3", + "CorrectionScoreV3": ".scores", "CreateAnnotationQueueAssignmentResponse": ".annotation_queues", "CreateAnnotationQueueItemRequest": ".annotation_queues", "CreateAnnotationQueueRequest": ".annotation_queues", @@ -536,26 +446,22 @@ "CreateCommentResponse": ".comments", "CreateDatasetItemRequest": ".dataset_items", "CreateDatasetRequest": ".datasets", - "CreateDatasetRunItemRequest": ".dataset_run_items", "CreateEvaluationRuleRequest": ".evaluation_rules", "CreateEvaluatorRequest": ".evaluators", "CreateEvaluatorRequest_Code": ".evaluators", "CreateEvaluatorRequest_LlmAsJudge": ".evaluators", - "CreateEventBody": ".ingestion", - "CreateEventEvent": ".ingestion", - "CreateGenerationBody": ".ingestion", - "CreateGenerationEvent": ".ingestion", "CreateLlmAsJudgeEvaluatorRequest": ".evaluators", "CreateModelRequest": ".models", - "CreateObservationEvent": ".ingestion", "CreatePromptRequest": ".prompts", + "CreateScoreBatchError": ".scores", + "CreateScoreBatchResponse": ".scores", + "CreateScoreBatchResults": ".scores", "CreateScoreConfigRequest": ".score_configs", "CreateScoreRequest": ".scores", - "CreateScoreResponse": ".scores", "CreateScoreSource": ".scores", "CreateScoreValue": ".commons", - "CreateSpanBody": ".ingestion", - "CreateSpanEvent": ".ingestion", + "CreateScoresRequest": ".scores", + "CreateScoresResponse": ".scores", "CreateTextPromptRequest": ".prompts", "CreateTextPromptType": ".prompts", "Creator": ".evaluators", @@ -565,15 +471,11 @@ "DatasetItemMediaReference": ".commons", "DatasetItemMediaReferenceField": ".commons", "DatasetItemMediaReferenceMedia": ".commons", - "DatasetRun": ".commons", - "DatasetRunItem": ".commons", - "DatasetRunWithItems": ".commons", "DatasetStatus": ".commons", "DateTimeEvaluationRuleFilter": ".evaluation_commons", "DeleteAnnotationQueueAssignmentResponse": ".annotation_queues", "DeleteAnnotationQueueItemResponse": ".annotation_queues", "DeleteDatasetItemResponse": ".dataset_items", - "DeleteDatasetRunResponse": ".datasets", "DeleteLlmConnectionResponse": ".llm_connections", "DeleteMembershipRequest": ".organizations", "DeleteTraceResponse": ".trace", @@ -647,37 +549,9 @@ "GetMediaResponse": ".media", "GetMediaUploadUrlRequest": ".media", "GetMediaUploadUrlResponse": ".media", - "GetScoresResponse": ".scores", - "GetScoresResponseData": ".scores", - "GetScoresResponseDataBoolean": ".scores", - "GetScoresResponseDataCategorical": ".scores", - "GetScoresResponseDataCorrection": ".scores", - "GetScoresResponseDataNumeric": ".scores", - "GetScoresResponseDataText": ".scores", - "GetScoresResponseData_Boolean": ".scores", - "GetScoresResponseData_Categorical": ".scores", - "GetScoresResponseData_Correction": ".scores", - "GetScoresResponseData_Numeric": ".scores", - "GetScoresResponseData_Text": ".scores", - "GetScoresResponseTraceData": ".scores", - "GetScoresV3Meta": ".scores_v3", - "GetScoresV3Response": ".scores_v3", + "GetScoresV3Meta": ".scores", + "GetScoresV3Response": ".scores", "HealthResponse": ".health", - "IngestionError": ".ingestion", - "IngestionEvent": ".ingestion", - "IngestionEvent_EventCreate": ".ingestion", - "IngestionEvent_GenerationCreate": ".ingestion", - "IngestionEvent_GenerationUpdate": ".ingestion", - "IngestionEvent_ObservationCreate": ".ingestion", - "IngestionEvent_ObservationUpdate": ".ingestion", - "IngestionEvent_ScoreCreate": ".ingestion", - "IngestionEvent_SdkLog": ".ingestion", - "IngestionEvent_SpanCreate": ".ingestion", - "IngestionEvent_SpanUpdate": ".ingestion", - "IngestionEvent_TraceCreate": ".ingestion", - "IngestionResponse": ".ingestion", - "IngestionSuccess": ".ingestion", - "IngestionUsage": ".ingestion", "InternalServerError": ".evaluation_errors", "LangfuseAPI": ".client", "LegacyEvaluationObject": ".evaluation_commons", @@ -704,21 +578,12 @@ "NumberEvaluationRuleFilter": ".evaluation_commons", "NumberObjectEvaluationRuleFilter": ".evaluation_commons", "NumericScore": ".commons", - "NumericScoreV1": ".commons", - "NumericScoreV3": ".scores_v3", - "Observation": ".commons", - "ObservationBody": ".ingestion", + "NumericScoreV3": ".scores", "ObservationLevel": ".commons", - "ObservationType": ".ingestion", "ObservationV2": ".commons", "ObservationsV2Meta": ".observations", "ObservationsV2Response": ".observations", - "ObservationsView": ".commons", - "ObservationsViewSingle": ".commons", - "OpenAiCompletionUsageSchema": ".ingestion", - "OpenAiResponseUsageSchema": ".ingestion", - "OpenAiUsage": ".ingestion", - "OptionalObservationBody": ".ingestion", + "OpenAiUsage": ".commons", "Organization": ".projects", "OrganizationApiKey": ".organizations", "OrganizationApiKeysResponse": ".organizations", @@ -735,12 +600,9 @@ "PaginatedAnnotationQueueItems": ".annotation_queues", "PaginatedAnnotationQueues": ".annotation_queues", "PaginatedDatasetItems": ".dataset_items", - "PaginatedDatasetRunItems": ".dataset_run_items", - "PaginatedDatasetRuns": ".datasets", "PaginatedDatasets": ".datasets", "PaginatedLlmConnections": ".llm_connections", "PaginatedModels": ".models", - "PaginatedSessions": ".sessions", "PatchMediaBody": ".media", "PlaceholderMessage": ".prompts", "PlaceholderMessageType": ".prompts", @@ -790,45 +652,34 @@ "ScimUser": ".scim", "ScimUsersListResponse": ".scim", "Score": ".commons", - "ScoreBody": ".ingestion", + "ScoreBody": ".scores", "ScoreConfig": ".commons", "ScoreConfigDataType": ".commons", "ScoreConfigs": ".score_configs", "ScoreDataType": ".commons", - "ScoreEvent": ".ingestion", "ScoreSource": ".commons", - "ScoreSubjectExperimentV3": ".scores_v3", - "ScoreSubjectObservationV3": ".scores_v3", - "ScoreSubjectSessionV3": ".scores_v3", - "ScoreSubjectTraceV3": ".scores_v3", - "ScoreSubjectV3": ".scores_v3", - "ScoreSubjectV3_Experiment": ".scores_v3", - "ScoreSubjectV3_Observation": ".scores_v3", - "ScoreSubjectV3_Session": ".scores_v3", - "ScoreSubjectV3_Trace": ".scores_v3", - "ScoreV1": ".commons", - "ScoreV1_Boolean": ".commons", - "ScoreV1_Categorical": ".commons", - "ScoreV1_Numeric": ".commons", - "ScoreV1_Text": ".commons", - "ScoreV3": ".scores_v3", - "ScoreV3_Boolean": ".scores_v3", - "ScoreV3_Categorical": ".scores_v3", - "ScoreV3_Correction": ".scores_v3", - "ScoreV3_Numeric": ".scores_v3", - "ScoreV3_Text": ".scores_v3", + "ScoreSubjectExperimentV3": ".scores", + "ScoreSubjectObservationV3": ".scores", + "ScoreSubjectSessionV3": ".scores", + "ScoreSubjectTraceV3": ".scores", + "ScoreSubjectV3": ".scores", + "ScoreSubjectV3_Experiment": ".scores", + "ScoreSubjectV3_Observation": ".scores", + "ScoreSubjectV3_Session": ".scores", + "ScoreSubjectV3_Trace": ".scores", + "ScoreV3": ".scores", + "ScoreV3_Boolean": ".scores", + "ScoreV3_Categorical": ".scores", + "ScoreV3_Correction": ".scores", + "ScoreV3_Numeric": ".scores", + "ScoreV3_Text": ".scores", "Score_Boolean": ".commons", "Score_Categorical": ".commons", "Score_Correction": ".commons", "Score_Numeric": ".commons", "Score_Text": ".commons", - "SdkLogBody": ".ingestion", - "SdkLogEvent": ".ingestion", "ServiceProviderConfig": ".scim", "ServiceUnavailableError": ".health", - "Session": ".commons", - "SessionWithTraces": ".commons", - "Sort": ".trace", "StringEvaluationRuleFilter": ".evaluation_commons", "StringObjectEvaluationRuleFilter": ".evaluation_commons", "StringOptionsEvaluationRuleFilter": ".evaluation_commons", @@ -836,39 +687,24 @@ "SubmitFeedbackResponse": ".feedback", "TextPrompt": ".prompts", "TextScore": ".commons", - "TextScoreV1": ".commons", - "TextScoreV3": ".scores_v3", + "TextScoreV3": ".scores", "TooManyRequestsError": ".evaluation_errors", - "Trace": ".commons", - "TraceBody": ".ingestion", - "TraceEvent": ".ingestion", - "TraceWithDetails": ".commons", - "TraceWithFullDetails": ".commons", - "Traces": ".trace", "UnauthorizedError": ".commons", "UpdateAnnotationQueueItemRequest": ".annotation_queues", "UpdateCodeEvaluatorRequest": ".evaluators", "UpdateEvaluationRuleRequest": ".evaluation_rules", "UpdateEvaluatorMetadataRequest": ".evaluators", "UpdateEvaluatorRequest": ".evaluators", - "UpdateEventBody": ".ingestion", - "UpdateGenerationBody": ".ingestion", - "UpdateGenerationEvent": ".ingestion", "UpdateLlmAsJudgeEvaluatorRequest": ".evaluators", - "UpdateObservationEvent": ".ingestion", "UpdateScoreConfigRequest": ".score_configs", - "UpdateSpanBody": ".ingestion", - "UpdateSpanEvent": ".ingestion", "UpsertLlmConnectionRequest": ".llm_connections", "Usage": ".commons", - "UsageDetails": ".ingestion", "UserMeta": ".scim", "annotation_queues": ".annotation_queues", "blob_storage_integrations": ".blob_storage_integrations", "comments": ".comments", "commons": ".commons", "dataset_items": ".dataset_items", - "dataset_run_items": ".dataset_run_items", "datasets": ".datasets", "evaluation_commons": ".evaluation_commons", "evaluation_errors": ".evaluation_errors", @@ -877,8 +713,6 @@ "experiments": ".experiments", "feedback": ".feedback", "health": ".health", - "ingestion": ".ingestion", - "legacy": ".legacy", "llm_connections": ".llm_connections", "media": ".media", "metrics": ".metrics", @@ -892,8 +726,6 @@ "scim": ".scim", "score_configs": ".score_configs", "scores": ".scores", - "scores_v3": ".scores_v3", - "sessions": ".sessions", "trace": ".trace", "unstable": ".unstable", "utils": ".utils", @@ -942,10 +774,8 @@ def __dir__(): "AsyncLangfuseAPI", "AuthenticationScheme", "BadRequestError", - "BaseEvent", "BasePrompt", "BaseScore", - "BaseScoreV1", "BaseScoreV3", "BlobStorageExportFieldGroup", "BlobStorageExportFrequency", @@ -961,11 +791,9 @@ def __dir__(): "BlobStorageSyncStatus", "BooleanEvaluationRuleFilter", "BooleanScore", - "BooleanScoreV1", "BooleanScoreV3", "BulkConfig", "CategoricalScore", - "CategoricalScoreV1", "CategoricalScoreV3", "CategoryOptionsEvaluationRuleFilter", "ChatMessage", @@ -992,26 +820,22 @@ def __dir__(): "CreateCommentResponse", "CreateDatasetItemRequest", "CreateDatasetRequest", - "CreateDatasetRunItemRequest", "CreateEvaluationRuleRequest", "CreateEvaluatorRequest", "CreateEvaluatorRequest_Code", "CreateEvaluatorRequest_LlmAsJudge", - "CreateEventBody", - "CreateEventEvent", - "CreateGenerationBody", - "CreateGenerationEvent", "CreateLlmAsJudgeEvaluatorRequest", "CreateModelRequest", - "CreateObservationEvent", "CreatePromptRequest", + "CreateScoreBatchError", + "CreateScoreBatchResponse", + "CreateScoreBatchResults", "CreateScoreConfigRequest", "CreateScoreRequest", - "CreateScoreResponse", "CreateScoreSource", "CreateScoreValue", - "CreateSpanBody", - "CreateSpanEvent", + "CreateScoresRequest", + "CreateScoresResponse", "CreateTextPromptRequest", "CreateTextPromptType", "Creator", @@ -1021,15 +845,11 @@ def __dir__(): "DatasetItemMediaReference", "DatasetItemMediaReferenceField", "DatasetItemMediaReferenceMedia", - "DatasetRun", - "DatasetRunItem", - "DatasetRunWithItems", "DatasetStatus", "DateTimeEvaluationRuleFilter", "DeleteAnnotationQueueAssignmentResponse", "DeleteAnnotationQueueItemResponse", "DeleteDatasetItemResponse", - "DeleteDatasetRunResponse", "DeleteLlmConnectionResponse", "DeleteMembershipRequest", "DeleteTraceResponse", @@ -1103,37 +923,9 @@ def __dir__(): "GetMediaResponse", "GetMediaUploadUrlRequest", "GetMediaUploadUrlResponse", - "GetScoresResponse", - "GetScoresResponseData", - "GetScoresResponseDataBoolean", - "GetScoresResponseDataCategorical", - "GetScoresResponseDataCorrection", - "GetScoresResponseDataNumeric", - "GetScoresResponseDataText", - "GetScoresResponseData_Boolean", - "GetScoresResponseData_Categorical", - "GetScoresResponseData_Correction", - "GetScoresResponseData_Numeric", - "GetScoresResponseData_Text", - "GetScoresResponseTraceData", "GetScoresV3Meta", "GetScoresV3Response", "HealthResponse", - "IngestionError", - "IngestionEvent", - "IngestionEvent_EventCreate", - "IngestionEvent_GenerationCreate", - "IngestionEvent_GenerationUpdate", - "IngestionEvent_ObservationCreate", - "IngestionEvent_ObservationUpdate", - "IngestionEvent_ScoreCreate", - "IngestionEvent_SdkLog", - "IngestionEvent_SpanCreate", - "IngestionEvent_SpanUpdate", - "IngestionEvent_TraceCreate", - "IngestionResponse", - "IngestionSuccess", - "IngestionUsage", "InternalServerError", "LangfuseAPI", "LegacyEvaluationObject", @@ -1160,21 +952,12 @@ def __dir__(): "NumberEvaluationRuleFilter", "NumberObjectEvaluationRuleFilter", "NumericScore", - "NumericScoreV1", "NumericScoreV3", - "Observation", - "ObservationBody", "ObservationLevel", - "ObservationType", "ObservationV2", "ObservationsV2Meta", "ObservationsV2Response", - "ObservationsView", - "ObservationsViewSingle", - "OpenAiCompletionUsageSchema", - "OpenAiResponseUsageSchema", "OpenAiUsage", - "OptionalObservationBody", "Organization", "OrganizationApiKey", "OrganizationApiKeysResponse", @@ -1191,12 +974,9 @@ def __dir__(): "PaginatedAnnotationQueueItems", "PaginatedAnnotationQueues", "PaginatedDatasetItems", - "PaginatedDatasetRunItems", - "PaginatedDatasetRuns", "PaginatedDatasets", "PaginatedLlmConnections", "PaginatedModels", - "PaginatedSessions", "PatchMediaBody", "PlaceholderMessage", "PlaceholderMessageType", @@ -1251,7 +1031,6 @@ def __dir__(): "ScoreConfigDataType", "ScoreConfigs", "ScoreDataType", - "ScoreEvent", "ScoreSource", "ScoreSubjectExperimentV3", "ScoreSubjectObservationV3", @@ -1262,11 +1041,6 @@ def __dir__(): "ScoreSubjectV3_Observation", "ScoreSubjectV3_Session", "ScoreSubjectV3_Trace", - "ScoreV1", - "ScoreV1_Boolean", - "ScoreV1_Categorical", - "ScoreV1_Numeric", - "ScoreV1_Text", "ScoreV3", "ScoreV3_Boolean", "ScoreV3_Categorical", @@ -1278,13 +1052,8 @@ def __dir__(): "Score_Correction", "Score_Numeric", "Score_Text", - "SdkLogBody", - "SdkLogEvent", "ServiceProviderConfig", "ServiceUnavailableError", - "Session", - "SessionWithTraces", - "Sort", "StringEvaluationRuleFilter", "StringObjectEvaluationRuleFilter", "StringOptionsEvaluationRuleFilter", @@ -1292,39 +1061,24 @@ def __dir__(): "SubmitFeedbackResponse", "TextPrompt", "TextScore", - "TextScoreV1", "TextScoreV3", "TooManyRequestsError", - "Trace", - "TraceBody", - "TraceEvent", - "TraceWithDetails", - "TraceWithFullDetails", - "Traces", "UnauthorizedError", "UpdateAnnotationQueueItemRequest", "UpdateCodeEvaluatorRequest", "UpdateEvaluationRuleRequest", "UpdateEvaluatorMetadataRequest", "UpdateEvaluatorRequest", - "UpdateEventBody", - "UpdateGenerationBody", - "UpdateGenerationEvent", "UpdateLlmAsJudgeEvaluatorRequest", - "UpdateObservationEvent", "UpdateScoreConfigRequest", - "UpdateSpanBody", - "UpdateSpanEvent", "UpsertLlmConnectionRequest", "Usage", - "UsageDetails", "UserMeta", "annotation_queues", "blob_storage_integrations", "comments", "commons", "dataset_items", - "dataset_run_items", "datasets", "evaluation_commons", "evaluation_errors", @@ -1333,8 +1087,6 @@ def __dir__(): "experiments", "feedback", "health", - "ingestion", - "legacy", "llm_connections", "media", "metrics", @@ -1348,8 +1100,6 @@ def __dir__(): "scim", "score_configs", "scores", - "scores_v3", - "sessions", "trace", "unstable", "utils", diff --git a/langfuse/api/blob_storage_integrations/client.py b/langfuse/api/blob_storage_integrations/client.py index 54c827908..0e609769d 100644 --- a/langfuse/api/blob_storage_integrations/client.py +++ b/langfuse/api/blob_storage_integrations/client.py @@ -114,10 +114,10 @@ def upsert_blob_storage_integration( type : BlobStorageIntegrationType bucket_name : str - 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). + 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). For GOOGLE_CLOUD_STORAGE with default credentials, the bucket must be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. region : str - Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. + Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. Not used by GOOGLE_CLOUD_STORAGE; pass auto. export_frequency : BlobStorageExportFrequency @@ -132,13 +132,15 @@ def upsert_blob_storage_integration( export_mode : BlobStorageExportMode endpoint : typing.Optional[str] - Custom endpoint URL (required for S3_COMPATIBLE type) + Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE. access_key_id : typing.Optional[str] - Access key ID for authentication + Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE. secret_access_key : typing.Optional[str] - Secret access key for authentication (will be encrypted when stored) + Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type. + + For GOOGLE_CLOUD_STORAGE, a GCP service account JSON key. On self-hosted deployments, pass `__GCS_DEFAULT_CREDENTIALS__` instead to use the deployment's Application Default Credentials; the bucket must then be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. Default credentials are not available on Langfuse Cloud. prefix : typing.Optional[str] Path prefix for exported files (must end with forward slash if provided) @@ -391,10 +393,10 @@ async def upsert_blob_storage_integration( type : BlobStorageIntegrationType bucket_name : str - 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). + 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). For GOOGLE_CLOUD_STORAGE with default credentials, the bucket must be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. region : str - Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. + Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. Not used by GOOGLE_CLOUD_STORAGE; pass auto. export_frequency : BlobStorageExportFrequency @@ -409,13 +411,15 @@ async def upsert_blob_storage_integration( export_mode : BlobStorageExportMode endpoint : typing.Optional[str] - Custom endpoint URL (required for S3_COMPATIBLE type) + Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE. access_key_id : typing.Optional[str] - Access key ID for authentication + Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE. secret_access_key : typing.Optional[str] - Secret access key for authentication (will be encrypted when stored) + Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type. + + For GOOGLE_CLOUD_STORAGE, a GCP service account JSON key. On self-hosted deployments, pass `__GCS_DEFAULT_CREDENTIALS__` instead to use the deployment's Application Default Credentials; the bucket must then be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. Default credentials are not available on Langfuse Cloud. prefix : typing.Optional[str] Path prefix for exported files (must end with forward slash if provided) diff --git a/langfuse/api/blob_storage_integrations/raw_client.py b/langfuse/api/blob_storage_integrations/raw_client.py index df1bb9012..827ab1a4b 100644 --- a/langfuse/api/blob_storage_integrations/raw_client.py +++ b/langfuse/api/blob_storage_integrations/raw_client.py @@ -171,10 +171,10 @@ def upsert_blob_storage_integration( type : BlobStorageIntegrationType bucket_name : str - 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). + 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). For GOOGLE_CLOUD_STORAGE with default credentials, the bucket must be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. region : str - Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. + Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. Not used by GOOGLE_CLOUD_STORAGE; pass auto. export_frequency : BlobStorageExportFrequency @@ -189,13 +189,15 @@ def upsert_blob_storage_integration( export_mode : BlobStorageExportMode endpoint : typing.Optional[str] - Custom endpoint URL (required for S3_COMPATIBLE type) + Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE. access_key_id : typing.Optional[str] - Access key ID for authentication + Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE. secret_access_key : typing.Optional[str] - Secret access key for authentication (will be encrypted when stored) + Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type. + + For GOOGLE_CLOUD_STORAGE, a GCP service account JSON key. On self-hosted deployments, pass `__GCS_DEFAULT_CREDENTIALS__` instead to use the deployment's Application Default Credentials; the bucket must then be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. Default credentials are not available on Langfuse Cloud. prefix : typing.Optional[str] Path prefix for exported files (must end with forward slash if provided) @@ -666,10 +668,10 @@ async def upsert_blob_storage_integration( type : BlobStorageIntegrationType bucket_name : str - 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). + 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). For GOOGLE_CLOUD_STORAGE with default credentials, the bucket must be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. region : str - Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. + Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. Not used by GOOGLE_CLOUD_STORAGE; pass auto. export_frequency : BlobStorageExportFrequency @@ -684,13 +686,15 @@ async def upsert_blob_storage_integration( export_mode : BlobStorageExportMode endpoint : typing.Optional[str] - Custom endpoint URL (required for S3_COMPATIBLE type) + Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE. access_key_id : typing.Optional[str] - Access key ID for authentication + Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE. secret_access_key : typing.Optional[str] - Secret access key for authentication (will be encrypted when stored) + Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type. + + For GOOGLE_CLOUD_STORAGE, a GCP service account JSON key. On self-hosted deployments, pass `__GCS_DEFAULT_CREDENTIALS__` instead to use the deployment's Application Default Credentials; the bucket must then be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. Default credentials are not available on Langfuse Cloud. prefix : typing.Optional[str] Path prefix for exported files (must end with forward slash if provided) diff --git a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py index 66828a62d..bd69ad0e6 100644 --- a/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py +++ b/langfuse/api/blob_storage_integrations/types/blob_storage_integration_type.py @@ -11,12 +11,14 @@ class BlobStorageIntegrationType(enum.StrEnum): S3 = "S3" S3COMPATIBLE = "S3_COMPATIBLE" AZURE_BLOB_STORAGE = "AZURE_BLOB_STORAGE" + GOOGLE_CLOUD_STORAGE = "GOOGLE_CLOUD_STORAGE" def visit( self, s3: typing.Callable[[], T_Result], s3compatible: typing.Callable[[], T_Result], azure_blob_storage: typing.Callable[[], T_Result], + google_cloud_storage: typing.Callable[[], T_Result], ) -> T_Result: if self is BlobStorageIntegrationType.S3: return s3() @@ -24,3 +26,5 @@ def visit( return s3compatible() if self is BlobStorageIntegrationType.AZURE_BLOB_STORAGE: return azure_blob_storage() + if self is BlobStorageIntegrationType.GOOGLE_CLOUD_STORAGE: + return google_cloud_storage() diff --git a/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py b/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py index 2f17c0aea..600a8be84 100644 --- a/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py +++ b/langfuse/api/blob_storage_integrations/types/create_blob_storage_integration_request.py @@ -28,31 +28,33 @@ class CreateBlobStorageIntegrationRequest(UniversalBaseModel): pydantic.Field() ) """ - 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). + 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). For GOOGLE_CLOUD_STORAGE with default credentials, the bucket must be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. """ endpoint: typing.Optional[str] = pydantic.Field(default=None) """ - Custom endpoint URL (required for S3_COMPATIBLE type) + Custom endpoint URL (required for S3_COMPATIBLE type). Ignored for GOOGLE_CLOUD_STORAGE. """ region: str = pydantic.Field() """ - Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. + Storage region used by S3-compatible clients (AWS, GCS, Cloudflare R2, MinIO, Azure location IDs such as eastus, OCI). Leading and trailing whitespace is removed. The remaining value must be 1-63 letters, numbers, or hyphens, and cannot start or end with a hyphen. Examples: us-east-1, europe-west1, eastus, auto. Not used by GOOGLE_CLOUD_STORAGE; pass auto. """ access_key_id: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="accessKeyId") ] = pydantic.Field(default=None) """ - Access key ID for authentication + Access key ID for authentication. Not used for GOOGLE_CLOUD_STORAGE. """ secret_access_key: typing_extensions.Annotated[ typing.Optional[str], FieldMetadata(alias="secretAccessKey") ] = pydantic.Field(default=None) """ - Secret access key for authentication (will be encrypted when stored) + Secret access key for authentication (will be encrypted when stored). When omitted on update, the stored secret is kept, unless the type changes between GOOGLE_CLOUD_STORAGE and another type. + + For GOOGLE_CLOUD_STORAGE, a GCP service account JSON key. On self-hosted deployments, pass `__GCS_DEFAULT_CREDENTIALS__` instead to use the deployment's Application Default Credentials; the bucket must then be listed in `LANGFUSE_BLOB_STORAGE_GCS_ALLOWED_BUCKETS`. Default credentials are not available on Langfuse Cloud. """ prefix: typing.Optional[str] = pydantic.Field(default=None) diff --git a/langfuse/api/client.py b/langfuse/api/client.py index 3781b4fcc..a6d960093 100644 --- a/langfuse/api/client.py +++ b/langfuse/api/client.py @@ -18,10 +18,6 @@ ) from .comments.client import AsyncCommentsClient, CommentsClient from .dataset_items.client import AsyncDatasetItemsClient, DatasetItemsClient - from .dataset_run_items.client import ( - AsyncDatasetRunItemsClient, - DatasetRunItemsClient, - ) from .datasets.client import AsyncDatasetsClient, DatasetsClient from .evaluation_rules.client import ( AsyncEvaluationRulesClient, @@ -31,8 +27,6 @@ from .experiments.client import AsyncExperimentsClient, ExperimentsClient from .feedback.client import AsyncFeedbackClient, FeedbackClient from .health.client import AsyncHealthClient, HealthClient - from .ingestion.client import AsyncIngestionClient, IngestionClient - from .legacy.client import AsyncLegacyClient, LegacyClient from .llm_connections.client import AsyncLlmConnectionsClient, LlmConnectionsClient from .media.client import AsyncMediaClient, MediaClient from .metrics.client import AsyncMetricsClient, MetricsClient @@ -46,8 +40,6 @@ from .scim.client import AsyncScimClient, ScimClient from .score_configs.client import AsyncScoreConfigsClient, ScoreConfigsClient from .scores.client import AsyncScoresClient, ScoresClient - from .scores_v3.client import AsyncScoresV3Client, ScoresV3Client - from .sessions.client import AsyncSessionsClient, SessionsClient from .trace.client import AsyncTraceClient, TraceClient from .unstable.client import AsyncUnstableClient, UnstableClient @@ -136,15 +128,12 @@ def __init__( ] = None self._comments: typing.Optional[CommentsClient] = None self._dataset_items: typing.Optional[DatasetItemsClient] = None - self._dataset_run_items: typing.Optional[DatasetRunItemsClient] = None self._datasets: typing.Optional[DatasetsClient] = None self._evaluation_rules: typing.Optional[EvaluationRulesClient] = None self._evaluators: typing.Optional[EvaluatorsClient] = None self._experiments: typing.Optional[ExperimentsClient] = None self._feedback: typing.Optional[FeedbackClient] = None self._health: typing.Optional[HealthClient] = None - self._ingestion: typing.Optional[IngestionClient] = None - self._legacy: typing.Optional[LegacyClient] = None self._llm_connections: typing.Optional[LlmConnectionsClient] = None self._media: typing.Optional[MediaClient] = None self._metrics: typing.Optional[MetricsClient] = None @@ -157,9 +146,7 @@ def __init__( self._prompts: typing.Optional[PromptsClient] = None self._scim: typing.Optional[ScimClient] = None self._score_configs: typing.Optional[ScoreConfigsClient] = None - self._scores_v3: typing.Optional[ScoresV3Client] = None self._scores: typing.Optional[ScoresClient] = None - self._sessions: typing.Optional[SessionsClient] = None self._trace: typing.Optional[TraceClient] = None self._unstable: typing.Optional[UnstableClient] = None @@ -201,16 +188,6 @@ def dataset_items(self): ) return self._dataset_items - @property - def dataset_run_items(self): - if self._dataset_run_items is None: - from .dataset_run_items.client import DatasetRunItemsClient # noqa: E402 - - self._dataset_run_items = DatasetRunItemsClient( - client_wrapper=self._client_wrapper - ) - return self._dataset_run_items - @property def datasets(self): if self._datasets is None: @@ -261,22 +238,6 @@ def health(self): self._health = HealthClient(client_wrapper=self._client_wrapper) return self._health - @property - def ingestion(self): - if self._ingestion is None: - from .ingestion.client import IngestionClient # noqa: E402 - - self._ingestion = IngestionClient(client_wrapper=self._client_wrapper) - return self._ingestion - - @property - def legacy(self): - if self._legacy is None: - from .legacy.client import LegacyClient # noqa: E402 - - self._legacy = LegacyClient(client_wrapper=self._client_wrapper) - return self._legacy - @property def llm_connections(self): if self._llm_connections is None: @@ -383,14 +344,6 @@ def score_configs(self): ) return self._score_configs - @property - def scores_v3(self): - if self._scores_v3 is None: - from .scores_v3.client import ScoresV3Client # noqa: E402 - - self._scores_v3 = ScoresV3Client(client_wrapper=self._client_wrapper) - return self._scores_v3 - @property def scores(self): if self._scores is None: @@ -399,14 +352,6 @@ def scores(self): self._scores = ScoresClient(client_wrapper=self._client_wrapper) return self._scores - @property - def sessions(self): - if self._sessions is None: - from .sessions.client import SessionsClient # noqa: E402 - - self._sessions = SessionsClient(client_wrapper=self._client_wrapper) - return self._sessions - @property def trace(self): if self._trace is None: @@ -508,15 +453,12 @@ def __init__( ] = None self._comments: typing.Optional[AsyncCommentsClient] = None self._dataset_items: typing.Optional[AsyncDatasetItemsClient] = None - self._dataset_run_items: typing.Optional[AsyncDatasetRunItemsClient] = None self._datasets: typing.Optional[AsyncDatasetsClient] = None self._evaluation_rules: typing.Optional[AsyncEvaluationRulesClient] = None self._evaluators: typing.Optional[AsyncEvaluatorsClient] = None self._experiments: typing.Optional[AsyncExperimentsClient] = None self._feedback: typing.Optional[AsyncFeedbackClient] = None self._health: typing.Optional[AsyncHealthClient] = None - self._ingestion: typing.Optional[AsyncIngestionClient] = None - self._legacy: typing.Optional[AsyncLegacyClient] = None self._llm_connections: typing.Optional[AsyncLlmConnectionsClient] = None self._media: typing.Optional[AsyncMediaClient] = None self._metrics: typing.Optional[AsyncMetricsClient] = None @@ -529,9 +471,7 @@ def __init__( self._prompts: typing.Optional[AsyncPromptsClient] = None self._scim: typing.Optional[AsyncScimClient] = None self._score_configs: typing.Optional[AsyncScoreConfigsClient] = None - self._scores_v3: typing.Optional[AsyncScoresV3Client] = None self._scores: typing.Optional[AsyncScoresClient] = None - self._sessions: typing.Optional[AsyncSessionsClient] = None self._trace: typing.Optional[AsyncTraceClient] = None self._unstable: typing.Optional[AsyncUnstableClient] = None @@ -575,16 +515,6 @@ def dataset_items(self): ) return self._dataset_items - @property - def dataset_run_items(self): - if self._dataset_run_items is None: - from .dataset_run_items.client import AsyncDatasetRunItemsClient # noqa: E402 - - self._dataset_run_items = AsyncDatasetRunItemsClient( - client_wrapper=self._client_wrapper - ) - return self._dataset_run_items - @property def datasets(self): if self._datasets is None: @@ -639,22 +569,6 @@ def health(self): self._health = AsyncHealthClient(client_wrapper=self._client_wrapper) return self._health - @property - def ingestion(self): - if self._ingestion is None: - from .ingestion.client import AsyncIngestionClient # noqa: E402 - - self._ingestion = AsyncIngestionClient(client_wrapper=self._client_wrapper) - return self._ingestion - - @property - def legacy(self): - if self._legacy is None: - from .legacy.client import AsyncLegacyClient # noqa: E402 - - self._legacy = AsyncLegacyClient(client_wrapper=self._client_wrapper) - return self._legacy - @property def llm_connections(self): if self._llm_connections is None: @@ -763,14 +677,6 @@ def score_configs(self): ) return self._score_configs - @property - def scores_v3(self): - if self._scores_v3 is None: - from .scores_v3.client import AsyncScoresV3Client # noqa: E402 - - self._scores_v3 = AsyncScoresV3Client(client_wrapper=self._client_wrapper) - return self._scores_v3 - @property def scores(self): if self._scores is None: @@ -779,14 +685,6 @@ def scores(self): self._scores = AsyncScoresClient(client_wrapper=self._client_wrapper) return self._scores - @property - def sessions(self): - if self._sessions is None: - from .sessions.client import AsyncSessionsClient # noqa: E402 - - self._sessions = AsyncSessionsClient(client_wrapper=self._client_wrapper) - return self._sessions - @property def trace(self): if self._trace is None: diff --git a/langfuse/api/commons/__init__.py b/langfuse/api/commons/__init__.py index fffe7234d..dc0e9f1a4 100644 --- a/langfuse/api/commons/__init__.py +++ b/langfuse/api/commons/__init__.py @@ -8,11 +8,8 @@ if typing.TYPE_CHECKING: from .types import ( BaseScore, - BaseScoreV1, BooleanScore, - BooleanScoreV1, CategoricalScore, - CategoricalScoreV1, Comment, CommentObjectType, ConfigCategory, @@ -23,9 +20,6 @@ DatasetItemMediaReference, DatasetItemMediaReferenceField, DatasetItemMediaReferenceMedia, - DatasetRun, - DatasetRunItem, - DatasetRunWithItems, DatasetStatus, Deprecation, MapValue, @@ -33,12 +27,9 @@ ModelPrice, ModelUsageUnit, NumericScore, - NumericScoreV1, - Observation, ObservationLevel, ObservationV2, - ObservationsView, - ObservationsViewSingle, + OpenAiUsage, PricingTier, PricingTierAttributeCondition, PricingTierAttributeSource, @@ -53,23 +44,12 @@ ScoreConfigDataType, ScoreDataType, ScoreSource, - ScoreV1, - ScoreV1_Boolean, - ScoreV1_Categorical, - ScoreV1_Numeric, - ScoreV1_Text, Score_Boolean, Score_Categorical, Score_Correction, Score_Numeric, Score_Text, - Session, - SessionWithTraces, TextScore, - TextScoreV1, - Trace, - TraceWithDetails, - TraceWithFullDetails, Usage, ) from .errors import ( @@ -82,11 +62,8 @@ _dynamic_imports: typing.Dict[str, str] = { "AccessDeniedError": ".errors", "BaseScore": ".types", - "BaseScoreV1": ".types", "BooleanScore": ".types", - "BooleanScoreV1": ".types", "CategoricalScore": ".types", - "CategoricalScoreV1": ".types", "Comment": ".types", "CommentObjectType": ".types", "ConfigCategory": ".types", @@ -97,9 +74,6 @@ "DatasetItemMediaReference": ".types", "DatasetItemMediaReferenceField": ".types", "DatasetItemMediaReferenceMedia": ".types", - "DatasetRun": ".types", - "DatasetRunItem": ".types", - "DatasetRunWithItems": ".types", "DatasetStatus": ".types", "Deprecation": ".types", "Error": ".errors", @@ -110,12 +84,9 @@ "ModelUsageUnit": ".types", "NotFoundError": ".errors", "NumericScore": ".types", - "NumericScoreV1": ".types", - "Observation": ".types", "ObservationLevel": ".types", "ObservationV2": ".types", - "ObservationsView": ".types", - "ObservationsViewSingle": ".types", + "OpenAiUsage": ".types", "PricingTier": ".types", "PricingTierAttributeCondition": ".types", "PricingTierAttributeSource": ".types", @@ -130,23 +101,12 @@ "ScoreConfigDataType": ".types", "ScoreDataType": ".types", "ScoreSource": ".types", - "ScoreV1": ".types", - "ScoreV1_Boolean": ".types", - "ScoreV1_Categorical": ".types", - "ScoreV1_Numeric": ".types", - "ScoreV1_Text": ".types", "Score_Boolean": ".types", "Score_Categorical": ".types", "Score_Correction": ".types", "Score_Numeric": ".types", "Score_Text": ".types", - "Session": ".types", - "SessionWithTraces": ".types", "TextScore": ".types", - "TextScoreV1": ".types", - "Trace": ".types", - "TraceWithDetails": ".types", - "TraceWithFullDetails": ".types", "UnauthorizedError": ".errors", "Usage": ".types", } @@ -182,11 +142,8 @@ def __dir__(): __all__ = [ "AccessDeniedError", "BaseScore", - "BaseScoreV1", "BooleanScore", - "BooleanScoreV1", "CategoricalScore", - "CategoricalScoreV1", "Comment", "CommentObjectType", "ConfigCategory", @@ -197,9 +154,6 @@ def __dir__(): "DatasetItemMediaReference", "DatasetItemMediaReferenceField", "DatasetItemMediaReferenceMedia", - "DatasetRun", - "DatasetRunItem", - "DatasetRunWithItems", "DatasetStatus", "Deprecation", "Error", @@ -210,12 +164,9 @@ def __dir__(): "ModelUsageUnit", "NotFoundError", "NumericScore", - "NumericScoreV1", - "Observation", "ObservationLevel", "ObservationV2", - "ObservationsView", - "ObservationsViewSingle", + "OpenAiUsage", "PricingTier", "PricingTierAttributeCondition", "PricingTierAttributeSource", @@ -230,23 +181,12 @@ def __dir__(): "ScoreConfigDataType", "ScoreDataType", "ScoreSource", - "ScoreV1", - "ScoreV1_Boolean", - "ScoreV1_Categorical", - "ScoreV1_Numeric", - "ScoreV1_Text", "Score_Boolean", "Score_Categorical", "Score_Correction", "Score_Numeric", "Score_Text", - "Session", - "SessionWithTraces", "TextScore", - "TextScoreV1", - "Trace", - "TraceWithDetails", - "TraceWithFullDetails", "UnauthorizedError", "Usage", ] diff --git a/langfuse/api/commons/types/__init__.py b/langfuse/api/commons/types/__init__.py index 6d3303261..2fbabb37e 100644 --- a/langfuse/api/commons/types/__init__.py +++ b/langfuse/api/commons/types/__init__.py @@ -7,11 +7,8 @@ if typing.TYPE_CHECKING: from .base_score import BaseScore - from .base_score_v1 import BaseScoreV1 from .boolean_score import BooleanScore - from .boolean_score_v1 import BooleanScoreV1 from .categorical_score import CategoricalScore - from .categorical_score_v1 import CategoricalScoreV1 from .comment import Comment from .comment_object_type import CommentObjectType from .config_category import ConfigCategory @@ -22,9 +19,6 @@ from .dataset_item_media_reference import DatasetItemMediaReference from .dataset_item_media_reference_field import DatasetItemMediaReferenceField from .dataset_item_media_reference_media import DatasetItemMediaReferenceMedia - from .dataset_run import DatasetRun - from .dataset_run_item import DatasetRunItem - from .dataset_run_with_items import DatasetRunWithItems from .dataset_status import DatasetStatus from .deprecation import Deprecation from .map_value import MapValue @@ -32,12 +26,9 @@ from .model_price import ModelPrice from .model_usage_unit import ModelUsageUnit from .numeric_score import NumericScore - from .numeric_score_v1 import NumericScoreV1 - from .observation import Observation from .observation_level import ObservationLevel from .observation_v2 import ObservationV2 - from .observations_view import ObservationsView - from .observations_view_single import ObservationsViewSingle + from .open_ai_usage import OpenAiUsage from .pricing_tier import PricingTier from .pricing_tier_attribute_condition import PricingTierAttributeCondition from .pricing_tier_attribute_source import PricingTierAttributeSource @@ -59,28 +50,12 @@ from .score_config_data_type import ScoreConfigDataType from .score_data_type import ScoreDataType from .score_source import ScoreSource - from .score_v1 import ( - ScoreV1, - ScoreV1_Boolean, - ScoreV1_Categorical, - ScoreV1_Numeric, - ScoreV1_Text, - ) - from .session import Session - from .session_with_traces import SessionWithTraces from .text_score import TextScore - from .text_score_v1 import TextScoreV1 - from .trace import Trace - from .trace_with_details import TraceWithDetails - from .trace_with_full_details import TraceWithFullDetails from .usage import Usage _dynamic_imports: typing.Dict[str, str] = { "BaseScore": ".base_score", - "BaseScoreV1": ".base_score_v1", "BooleanScore": ".boolean_score", - "BooleanScoreV1": ".boolean_score_v1", "CategoricalScore": ".categorical_score", - "CategoricalScoreV1": ".categorical_score_v1", "Comment": ".comment", "CommentObjectType": ".comment_object_type", "ConfigCategory": ".config_category", @@ -91,9 +66,6 @@ "DatasetItemMediaReference": ".dataset_item_media_reference", "DatasetItemMediaReferenceField": ".dataset_item_media_reference_field", "DatasetItemMediaReferenceMedia": ".dataset_item_media_reference_media", - "DatasetRun": ".dataset_run", - "DatasetRunItem": ".dataset_run_item", - "DatasetRunWithItems": ".dataset_run_with_items", "DatasetStatus": ".dataset_status", "Deprecation": ".deprecation", "MapValue": ".map_value", @@ -101,12 +73,9 @@ "ModelPrice": ".model_price", "ModelUsageUnit": ".model_usage_unit", "NumericScore": ".numeric_score", - "NumericScoreV1": ".numeric_score_v1", - "Observation": ".observation", "ObservationLevel": ".observation_level", "ObservationV2": ".observation_v2", - "ObservationsView": ".observations_view", - "ObservationsViewSingle": ".observations_view_single", + "OpenAiUsage": ".open_ai_usage", "PricingTier": ".pricing_tier", "PricingTierAttributeCondition": ".pricing_tier_attribute_condition", "PricingTierAttributeSource": ".pricing_tier_attribute_source", @@ -121,23 +90,12 @@ "ScoreConfigDataType": ".score_config_data_type", "ScoreDataType": ".score_data_type", "ScoreSource": ".score_source", - "ScoreV1": ".score_v1", - "ScoreV1_Boolean": ".score_v1", - "ScoreV1_Categorical": ".score_v1", - "ScoreV1_Numeric": ".score_v1", - "ScoreV1_Text": ".score_v1", "Score_Boolean": ".score", "Score_Categorical": ".score", "Score_Correction": ".score", "Score_Numeric": ".score", "Score_Text": ".score", - "Session": ".session", - "SessionWithTraces": ".session_with_traces", "TextScore": ".text_score", - "TextScoreV1": ".text_score_v1", - "Trace": ".trace", - "TraceWithDetails": ".trace_with_details", - "TraceWithFullDetails": ".trace_with_full_details", "Usage": ".usage", } @@ -171,11 +129,8 @@ def __dir__(): __all__ = [ "BaseScore", - "BaseScoreV1", "BooleanScore", - "BooleanScoreV1", "CategoricalScore", - "CategoricalScoreV1", "Comment", "CommentObjectType", "ConfigCategory", @@ -186,9 +141,6 @@ def __dir__(): "DatasetItemMediaReference", "DatasetItemMediaReferenceField", "DatasetItemMediaReferenceMedia", - "DatasetRun", - "DatasetRunItem", - "DatasetRunWithItems", "DatasetStatus", "Deprecation", "MapValue", @@ -196,12 +148,9 @@ def __dir__(): "ModelPrice", "ModelUsageUnit", "NumericScore", - "NumericScoreV1", - "Observation", "ObservationLevel", "ObservationV2", - "ObservationsView", - "ObservationsViewSingle", + "OpenAiUsage", "PricingTier", "PricingTierAttributeCondition", "PricingTierAttributeSource", @@ -216,22 +165,11 @@ def __dir__(): "ScoreConfigDataType", "ScoreDataType", "ScoreSource", - "ScoreV1", - "ScoreV1_Boolean", - "ScoreV1_Categorical", - "ScoreV1_Numeric", - "ScoreV1_Text", "Score_Boolean", "Score_Categorical", "Score_Correction", "Score_Numeric", "Score_Text", - "Session", - "SessionWithTraces", "TextScore", - "TextScoreV1", - "Trace", - "TraceWithDetails", - "TraceWithFullDetails", "Usage", ] diff --git a/langfuse/api/commons/types/base_score_v1.py b/langfuse/api/commons/types/base_score_v1.py deleted file mode 100644 index 13845d997..000000000 --- a/langfuse/api/commons/types/base_score_v1.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from .score_source import ScoreSource - - -class BaseScoreV1(UniversalBaseModel): - id: str - trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] - name: str - source: ScoreSource - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = pydantic.Field(default=None) - """ - The observation ID associated with the score - """ - - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = pydantic.Field(default=None) - """ - The user ID of the author - """ - - comment: typing.Optional[str] = pydantic.Field(default=None) - """ - Comment on the score - """ - - metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field( - default=None - ) - """ - Metadata associated with the score - """ - - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = pydantic.Field(default=None) - """ - Reference a score config on a score. When set, config and score name must be equal and value must comply to optionally defined numerical range - """ - - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = pydantic.Field(default=None) - """ - The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. - """ - - environment: str = pydantic.Field() - """ - The environment from which this score originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/boolean_score_v1.py b/langfuse/api/commons/types/boolean_score_v1.py deleted file mode 100644 index cf5425255..000000000 --- a/langfuse/api/commons/types/boolean_score_v1.py +++ /dev/null @@ -1,26 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .base_score_v1 import BaseScoreV1 - - -class BooleanScoreV1(BaseScoreV1): - value: float = pydantic.Field() - """ - The numeric value of the score. Equals 1 for "True" and 0 for "False" - """ - - string_value: typing_extensions.Annotated[ - str, FieldMetadata(alias="stringValue") - ] = pydantic.Field() - """ - The string representation of the score value. Is inferred from the numeric value and equals "True" or "False" - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/categorical_score_v1.py b/langfuse/api/commons/types/categorical_score_v1.py deleted file mode 100644 index 8f98af1a8..000000000 --- a/langfuse/api/commons/types/categorical_score_v1.py +++ /dev/null @@ -1,26 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .base_score_v1 import BaseScoreV1 - - -class CategoricalScoreV1(BaseScoreV1): - value: float = pydantic.Field() - """ - Represents the numeric category mapping of the stringValue. If no config is linked, defaults to 0. - """ - - string_value: typing_extensions.Annotated[ - str, FieldMetadata(alias="stringValue") - ] = pydantic.Field() - """ - The string representation of the score value. If no config is linked, can be any string. Otherwise, must map to a config category - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/dataset_item.py b/langfuse/api/commons/types/dataset_item.py index cb1cc4cb0..2e3c367c0 100644 --- a/langfuse/api/commons/types/dataset_item.py +++ b/langfuse/api/commons/types/dataset_item.py @@ -53,6 +53,11 @@ class DatasetItem(UniversalBaseModel): updated_at: typing_extensions.Annotated[ dt.datetime, FieldMetadata(alias="updatedAt") ] + version: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + When this version of the dataset item became current. Listing dataset items with a `version` at or after this timestamp returns this item version, until the item is changed again. Self-hosted deployments that set `LANGFUSE_DATASET_SERVICE_WRITE_TO_VERSIONED_IMPLEMENTATION=false` update items in place, so there it stays at the item's first write. + """ + media_references: typing_extensions.Annotated[ typing.List[DatasetItemMediaReference], FieldMetadata(alias="mediaReferences") ] = pydantic.Field() diff --git a/langfuse/api/commons/types/dataset_run.py b/langfuse/api/commons/types/dataset_run.py deleted file mode 100644 index 0b5d8566c..000000000 --- a/langfuse/api/commons/types/dataset_run.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class DatasetRun(UniversalBaseModel): - id: str = pydantic.Field() - """ - Unique identifier of the dataset run - """ - - name: str = pydantic.Field() - """ - Name of the dataset run - """ - - description: typing.Optional[str] = pydantic.Field(default=None) - """ - Description of the run - """ - - metadata: typing.Any = pydantic.Field() - """ - Metadata of the dataset run - """ - - dataset_id: typing_extensions.Annotated[str, FieldMetadata(alias="datasetId")] = ( - pydantic.Field() - ) - """ - Id of the associated dataset - """ - - dataset_name: typing_extensions.Annotated[ - str, FieldMetadata(alias="datasetName") - ] = pydantic.Field() - """ - Name of the associated dataset - """ - - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] = pydantic.Field() - """ - The date and time when the dataset run was created - """ - - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] = pydantic.Field() - """ - The date and time when the dataset run was last updated - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/dataset_run_item.py b/langfuse/api/commons/types/dataset_run_item.py deleted file mode 100644 index 1ee364ffa..000000000 --- a/langfuse/api/commons/types/dataset_run_item.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class DatasetRunItem(UniversalBaseModel): - id: str - dataset_run_id: typing_extensions.Annotated[ - str, FieldMetadata(alias="datasetRunId") - ] - dataset_run_name: typing_extensions.Annotated[ - str, FieldMetadata(alias="datasetRunName") - ] - dataset_item_id: typing_extensions.Annotated[ - str, FieldMetadata(alias="datasetItemId") - ] - trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = pydantic.Field(default=None) - """ - The observation ID associated with this run item - """ - - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/dataset_run_with_items.py b/langfuse/api/commons/types/dataset_run_with_items.py deleted file mode 100644 index b16660b97..000000000 --- a/langfuse/api/commons/types/dataset_run_with_items.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .dataset_run import DatasetRun -from .dataset_run_item import DatasetRunItem -from .deprecation import Deprecation - - -class DatasetRunWithItems(DatasetRun): - dataset_run_items: typing_extensions.Annotated[ - typing.List[DatasetRunItem], FieldMetadata(alias="datasetRunItems") - ] - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/observation.py b/langfuse/api/commons/types/observation.py deleted file mode 100644 index 76c019b0d..000000000 --- a/langfuse/api/commons/types/observation.py +++ /dev/null @@ -1,144 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from .observation_level import ObservationLevel -from .usage import Usage - - -class Observation(UniversalBaseModel): - id: str = pydantic.Field() - """ - The unique identifier of the observation - """ - - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = pydantic.Field(default=None) - """ - The trace ID associated with the observation - """ - - type: str = pydantic.Field() - """ - The type of the observation - """ - - name: typing.Optional[str] = pydantic.Field(default=None) - """ - The name of the observation - """ - - start_time: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="startTime") - ] = pydantic.Field() - """ - The start time of the observation - """ - - end_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="endTime") - ] = pydantic.Field(default=None) - """ - The end time of the observation. - """ - - completion_start_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") - ] = pydantic.Field(default=None) - """ - The completion start time of the observation - """ - - model: typing.Optional[str] = pydantic.Field(default=None) - """ - The model used for the observation - """ - - model_parameters: typing_extensions.Annotated[ - typing.Any, FieldMetadata(alias="modelParameters") - ] = pydantic.Field() - """ - The parameters of the model used for the observation - """ - - input: typing.Any = pydantic.Field() - """ - The input data of the observation - """ - - version: typing.Optional[str] = pydantic.Field(default=None) - """ - The version of the observation - """ - - metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field( - default=None - ) - """ - Additional metadata of the observation - """ - - output: typing.Any = pydantic.Field() - """ - The output data of the observation - """ - - usage: Usage = pydantic.Field() - """ - (Deprecated. Use usageDetails and costDetails instead.) The usage data of the observation - """ - - level: ObservationLevel = pydantic.Field() - """ - The level of the observation - """ - - status_message: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="statusMessage") - ] = pydantic.Field(default=None) - """ - The status message of the observation - """ - - parent_observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="parentObservationId") - ] = pydantic.Field(default=None) - """ - The parent observation ID - """ - - prompt_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="promptId") - ] = pydantic.Field(default=None) - """ - The prompt ID associated with the observation - """ - - usage_details: typing_extensions.Annotated[ - typing.Dict[str, int], FieldMetadata(alias="usageDetails") - ] = pydantic.Field() - """ - The usage details of the observation. Key is the name of the usage metric, value is the number of units consumed. The total key is the sum of all (non-total) usage metrics or the total value ingested. - """ - - cost_details: typing_extensions.Annotated[ - typing.Dict[str, float], FieldMetadata(alias="costDetails") - ] = pydantic.Field() - """ - The cost details of the observation. Key is the name of the cost metric, value is the cost in USD. The total key is the sum of all (non-total) cost metrics or the total value ingested. - """ - - environment: str = pydantic.Field() - """ - The environment from which this observation originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/observations_view.py b/langfuse/api/commons/types/observations_view.py deleted file mode 100644 index fc97c27e7..000000000 --- a/langfuse/api/commons/types/observations_view.py +++ /dev/null @@ -1,89 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .observation import Observation - - -class ObservationsView(Observation): - prompt_name: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="promptName") - ] = pydantic.Field(default=None) - """ - The name of the prompt associated with the observation - """ - - prompt_version: typing_extensions.Annotated[ - typing.Optional[int], FieldMetadata(alias="promptVersion") - ] = pydantic.Field(default=None) - """ - The version of the prompt associated with the observation - """ - - model_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="modelId") - ] = pydantic.Field(default=None) - """ - The unique identifier of the model - """ - - input_price: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="inputPrice") - ] = pydantic.Field(default=None) - """ - The price of the input in USD - """ - - output_price: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="outputPrice") - ] = pydantic.Field(default=None) - """ - The price of the output in USD. - """ - - total_price: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="totalPrice") - ] = pydantic.Field(default=None) - """ - The total price in USD. - """ - - calculated_input_cost: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="calculatedInputCost") - ] = pydantic.Field(default=None) - """ - (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the input in USD - """ - - calculated_output_cost: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="calculatedOutputCost") - ] = pydantic.Field(default=None) - """ - (Deprecated. Use usageDetails and costDetails instead.) The calculated cost of the output in USD - """ - - calculated_total_cost: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="calculatedTotalCost") - ] = pydantic.Field(default=None) - """ - (Deprecated. Use usageDetails and costDetails instead.) The calculated total cost in USD - """ - - latency: typing.Optional[float] = pydantic.Field(default=None) - """ - The latency in seconds. - """ - - time_to_first_token: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="timeToFirstToken") - ] = pydantic.Field(default=None) - """ - The time to the first token in seconds - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/observations_view_single.py b/langfuse/api/commons/types/observations_view_single.py deleted file mode 100644 index 4c7d68ace..000000000 --- a/langfuse/api/commons/types/observations_view_single.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .deprecation import Deprecation -from .observations_view import ObservationsView - - -class ObservationsViewSingle(ObservationsView): - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/open_ai_usage.py b/langfuse/api/commons/types/open_ai_usage.py similarity index 100% rename from langfuse/api/ingestion/types/open_ai_usage.py rename to langfuse/api/commons/types/open_ai_usage.py diff --git a/langfuse/api/commons/types/score_v1.py b/langfuse/api/commons/types/score_v1.py deleted file mode 100644 index 32e0a6b4e..000000000 --- a/langfuse/api/commons/types/score_v1.py +++ /dev/null @@ -1,168 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from .score_source import ScoreSource - - -class ScoreV1_Numeric(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") - ] = "NUMERIC" - value: float - id: str - trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] - name: str - source: ScoreSource - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class ScoreV1_Categorical(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") - ] = "CATEGORICAL" - value: float - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] - name: str - source: ScoreSource - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class ScoreV1_Boolean(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") - ] = "BOOLEAN" - value: float - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] - name: str - source: ScoreSource - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class ScoreV1_Text(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["TEXT"], FieldMetadata(alias="dataType") - ] = "TEXT" - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[str, FieldMetadata(alias="traceId")] - name: str - source: ScoreSource - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -ScoreV1 = typing_extensions.Annotated[ - typing.Union[ScoreV1_Numeric, ScoreV1_Categorical, ScoreV1_Boolean, ScoreV1_Text], - pydantic.Field(discriminator="data_type"), -] diff --git a/langfuse/api/commons/types/session.py b/langfuse/api/commons/types/session.py deleted file mode 100644 index c254b27cd..000000000 --- a/langfuse/api/commons/types/session.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class Session(UniversalBaseModel): - id: str - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] - environment: str = pydantic.Field() - """ - The environment from which this session originated. - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/session_with_traces.py b/langfuse/api/commons/types/session_with_traces.py deleted file mode 100644 index e5159ade0..000000000 --- a/langfuse/api/commons/types/session_with_traces.py +++ /dev/null @@ -1,21 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .deprecation import Deprecation -from .session import Session -from .trace import Trace - - -class SessionWithTraces(Session): - traces: typing.List[Trace] - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/text_score_v1.py b/langfuse/api/commons/types/text_score_v1.py deleted file mode 100644 index 937ca281e..000000000 --- a/langfuse/api/commons/types/text_score_v1.py +++ /dev/null @@ -1,21 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .base_score_v1 import BaseScoreV1 - - -class TextScoreV1(BaseScoreV1): - string_value: typing_extensions.Annotated[ - str, FieldMetadata(alias="stringValue") - ] = pydantic.Field() - """ - The text content of the score (1-500 characters) - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/trace.py b/langfuse/api/commons/types/trace.py deleted file mode 100644 index 1b489c03c..000000000 --- a/langfuse/api/commons/types/trace.py +++ /dev/null @@ -1,86 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class Trace(UniversalBaseModel): - id: str = pydantic.Field() - """ - The unique identifier of a trace - """ - - timestamp: dt.datetime = pydantic.Field() - """ - The timestamp when the trace was created - """ - - name: typing.Optional[str] = pydantic.Field(default=None) - """ - The name of the trace - """ - - input: typing.Optional[typing.Any] = pydantic.Field(default=None) - """ - The input data of the trace. Can be any JSON. - """ - - output: typing.Optional[typing.Any] = pydantic.Field(default=None) - """ - The output data of the trace. Can be any JSON. - """ - - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = pydantic.Field(default=None) - """ - The session identifier associated with the trace - """ - - release: typing.Optional[str] = pydantic.Field(default=None) - """ - The release version of the application when the trace was created - """ - - version: typing.Optional[str] = pydantic.Field(default=None) - """ - The version of the trace - """ - - user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="userId") - ] = pydantic.Field(default=None) - """ - The user identifier associated with the trace - """ - - metadata: typing.Optional[typing.Dict[str, typing.Any]] = pydantic.Field( - default=None - ) - """ - The metadata associated with the trace. Values can be any JSON; non-object metadata sent at ingestion is returned under the `metadata` key. - """ - - tags: typing.List[str] = pydantic.Field() - """ - The tags associated with the trace. - """ - - public: bool = pydantic.Field() - """ - Public traces are accessible via url without login - """ - - environment: str = pydantic.Field() - """ - The environment from which this trace originated. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/trace_with_details.py b/langfuse/api/commons/types/trace_with_details.py deleted file mode 100644 index 958d76901..000000000 --- a/langfuse/api/commons/types/trace_with_details.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .trace import Trace - - -class TraceWithDetails(Trace): - html_path: typing_extensions.Annotated[str, FieldMetadata(alias="htmlPath")] = ( - pydantic.Field() - ) - """ - Path of trace in Langfuse UI - """ - - latency: typing.Optional[float] = pydantic.Field(default=None) - """ - Latency of trace in seconds - """ - - total_cost: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="totalCost") - ] = pydantic.Field(default=None) - """ - Cost of trace in USD - """ - - observations: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - List of observation ids - """ - - scores: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - List of score ids - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/commons/types/trace_with_full_details.py b/langfuse/api/commons/types/trace_with_full_details.py deleted file mode 100644 index 227730cf4..000000000 --- a/langfuse/api/commons/types/trace_with_full_details.py +++ /dev/null @@ -1,50 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .deprecation import Deprecation -from .observations_view import ObservationsView -from .score_v1 import ScoreV1 -from .trace import Trace - - -class TraceWithFullDetails(Trace): - html_path: typing_extensions.Annotated[str, FieldMetadata(alias="htmlPath")] = ( - pydantic.Field() - ) - """ - Path of trace in Langfuse UI - """ - - latency: typing.Optional[float] = pydantic.Field(default=None) - """ - Latency of trace in seconds - """ - - total_cost: typing_extensions.Annotated[ - typing.Optional[float], FieldMetadata(alias="totalCost") - ] = pydantic.Field(default=None) - """ - Cost of trace in USD - """ - - observations: typing.List[ObservationsView] = pydantic.Field() - """ - List of observations - """ - - scores: typing.List[ScoreV1] = pydantic.Field() - """ - List of scores - """ - - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/dataset_run_items/__init__.py b/langfuse/api/dataset_run_items/__init__.py deleted file mode 100644 index 9ff4e097e..000000000 --- a/langfuse/api/dataset_run_items/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import CreateDatasetRunItemRequest, PaginatedDatasetRunItems -_dynamic_imports: typing.Dict[str, str] = { - "CreateDatasetRunItemRequest": ".types", - "PaginatedDatasetRunItems": ".types", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["CreateDatasetRunItemRequest", "PaginatedDatasetRunItems"] diff --git a/langfuse/api/dataset_run_items/client.py b/langfuse/api/dataset_run_items/client.py deleted file mode 100644 index d9712be10..000000000 --- a/langfuse/api/dataset_run_items/client.py +++ /dev/null @@ -1,350 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import typing_extensions -from ..commons.types.dataset_run_item import DatasetRunItem -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from .raw_client import AsyncRawDatasetRunItemsClient, RawDatasetRunItemsClient -from .types.paginated_dataset_run_items import PaginatedDatasetRunItems - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class DatasetRunItemsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawDatasetRunItemsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawDatasetRunItemsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawDatasetRunItemsClient - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, create experiment data via the SDK experiment runner or the OpenTelemetry endpoint at POST /api/public/otel/v1/traces. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def create( - self, - *, - run_name: str, - dataset_item_id: str, - run_description: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Any] = OMIT, - observation_id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - dataset_version: typing.Optional[dt.datetime] = OMIT, - created_at: typing.Optional[dt.datetime] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunItem: - """ - Create a dataset run item - - Parameters - ---------- - run_name : str - - dataset_item_id : str - - run_description : typing.Optional[str] - Description of the run. If run exists, description will be updated. - - metadata : typing.Optional[typing.Any] - Metadata of the dataset run, updates run if run already exists - - observation_id : typing.Optional[str] - - trace_id : typing.Optional[str] - traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. - - dataset_version : typing.Optional[dt.datetime] - ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). - Specifies the dataset version to use for this experiment run. - If provided, the experiment will use dataset items as they existed at or before this timestamp. - If not provided, uses the latest version of dataset items. - - created_at : typing.Optional[dt.datetime] - Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunItem - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_run_items.create( - run_name="runName", - dataset_item_id="datasetItemId", - ) - """ - _response = self._raw_client.create( - run_name=run_name, - dataset_item_id=dataset_item_id, - run_description=run_description, - metadata=metadata, - observation_id=observation_id, - trace_id=trace_id, - dataset_version=dataset_version, - created_at=created_at, - request_options=request_options, - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset run items are replaced by experiment items; use `GET /api/public/experiment-items?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def list( - self, - *, - dataset_id: str, - run_name: str, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRunItems: - """ - List dataset run items - - Parameters - ---------- - dataset_id : str - - run_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRunItems - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.dataset_run_items.list( - dataset_id="datasetId", - run_name="runName", - ) - """ - _response = self._raw_client.list( - dataset_id=dataset_id, - run_name=run_name, - page=page, - limit=limit, - request_options=request_options, - ) - return _response.data - - -class AsyncDatasetRunItemsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawDatasetRunItemsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawDatasetRunItemsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawDatasetRunItemsClient - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, create experiment data via the SDK experiment runner or the OpenTelemetry endpoint at POST /api/public/otel/v1/traces. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def create( - self, - *, - run_name: str, - dataset_item_id: str, - run_description: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Any] = OMIT, - observation_id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - dataset_version: typing.Optional[dt.datetime] = OMIT, - created_at: typing.Optional[dt.datetime] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunItem: - """ - Create a dataset run item - - Parameters - ---------- - run_name : str - - dataset_item_id : str - - run_description : typing.Optional[str] - Description of the run. If run exists, description will be updated. - - metadata : typing.Optional[typing.Any] - Metadata of the dataset run, updates run if run already exists - - observation_id : typing.Optional[str] - - trace_id : typing.Optional[str] - traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. - - dataset_version : typing.Optional[dt.datetime] - ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). - Specifies the dataset version to use for this experiment run. - If provided, the experiment will use dataset items as they existed at or before this timestamp. - If not provided, uses the latest version of dataset items. - - created_at : typing.Optional[dt.datetime] - Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunItem - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_run_items.create( - run_name="runName", - dataset_item_id="datasetItemId", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.create( - run_name=run_name, - dataset_item_id=dataset_item_id, - run_description=run_description, - metadata=metadata, - observation_id=observation_id, - trace_id=trace_id, - dataset_version=dataset_version, - created_at=created_at, - request_options=request_options, - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset run items are replaced by experiment items; use `GET /api/public/experiment-items?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def list( - self, - *, - dataset_id: str, - run_name: str, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRunItems: - """ - List dataset run items - - Parameters - ---------- - dataset_id : str - - run_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRunItems - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.dataset_run_items.list( - dataset_id="datasetId", - run_name="runName", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list( - dataset_id=dataset_id, - run_name=run_name, - page=page, - limit=limit, - request_options=request_options, - ) - return _response.data diff --git a/langfuse/api/dataset_run_items/raw_client.py b/langfuse/api/dataset_run_items/raw_client.py deleted file mode 100644 index 374c1ebd2..000000000 --- a/langfuse/api/dataset_run_items/raw_client.py +++ /dev/null @@ -1,574 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -import typing_extensions -from json.decoder import JSONDecodeError - -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.dataset_run_item import DatasetRunItem -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from .types.paginated_dataset_run_items import PaginatedDatasetRunItems - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawDatasetRunItemsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, create experiment data via the SDK experiment runner or the OpenTelemetry endpoint at POST /api/public/otel/v1/traces. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def create( - self, - *, - run_name: str, - dataset_item_id: str, - run_description: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Any] = OMIT, - observation_id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - dataset_version: typing.Optional[dt.datetime] = OMIT, - created_at: typing.Optional[dt.datetime] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[DatasetRunItem]: - """ - Create a dataset run item - - Parameters - ---------- - run_name : str - - dataset_item_id : str - - run_description : typing.Optional[str] - Description of the run. If run exists, description will be updated. - - metadata : typing.Optional[typing.Any] - Metadata of the dataset run, updates run if run already exists - - observation_id : typing.Optional[str] - - trace_id : typing.Optional[str] - traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. - - dataset_version : typing.Optional[dt.datetime] - ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). - Specifies the dataset version to use for this experiment run. - If provided, the experiment will use dataset items as they existed at or before this timestamp. - If not provided, uses the latest version of dataset items. - - created_at : typing.Optional[dt.datetime] - Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[DatasetRunItem] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="POST", - json={ - "runName": run_name, - "runDescription": run_description, - "metadata": metadata, - "datasetItemId": dataset_item_id, - "observationId": observation_id, - "traceId": trace_id, - "datasetVersion": dataset_version, - "createdAt": created_at, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - DatasetRunItem, - parse_obj_as( - type_=DatasetRunItem, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset run items are replaced by experiment items; use `GET /api/public/experiment-items?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def list( - self, - *, - dataset_id: str, - run_name: str, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[PaginatedDatasetRunItems]: - """ - List dataset run items - - Parameters - ---------- - dataset_id : str - - run_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[PaginatedDatasetRunItems] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="GET", - params={ - "datasetId": dataset_id, - "runName": run_name, - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PaginatedDatasetRunItems, - parse_obj_as( - type_=PaginatedDatasetRunItems, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawDatasetRunItemsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, create experiment data via the SDK experiment runner or the OpenTelemetry endpoint at POST /api/public/otel/v1/traces. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def create( - self, - *, - run_name: str, - dataset_item_id: str, - run_description: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Any] = OMIT, - observation_id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - dataset_version: typing.Optional[dt.datetime] = OMIT, - created_at: typing.Optional[dt.datetime] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[DatasetRunItem]: - """ - Create a dataset run item - - Parameters - ---------- - run_name : str - - dataset_item_id : str - - run_description : typing.Optional[str] - Description of the run. If run exists, description will be updated. - - metadata : typing.Optional[typing.Any] - Metadata of the dataset run, updates run if run already exists - - observation_id : typing.Optional[str] - - trace_id : typing.Optional[str] - traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. - - dataset_version : typing.Optional[dt.datetime] - ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). - Specifies the dataset version to use for this experiment run. - If provided, the experiment will use dataset items as they existed at or before this timestamp. - If not provided, uses the latest version of dataset items. - - created_at : typing.Optional[dt.datetime] - Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[DatasetRunItem] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="POST", - json={ - "runName": run_name, - "runDescription": run_description, - "metadata": metadata, - "datasetItemId": dataset_item_id, - "observationId": observation_id, - "traceId": trace_id, - "datasetVersion": dataset_version, - "createdAt": created_at, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - DatasetRunItem, - parse_obj_as( - type_=DatasetRunItem, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset run items are replaced by experiment items; use `GET /api/public/experiment-items?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def list( - self, - *, - dataset_id: str, - run_name: str, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[PaginatedDatasetRunItems]: - """ - List dataset run items - - Parameters - ---------- - dataset_id : str - - run_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[PaginatedDatasetRunItems] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/dataset-run-items", - method="GET", - params={ - "datasetId": dataset_id, - "runName": run_name, - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PaginatedDatasetRunItems, - parse_obj_as( - type_=PaginatedDatasetRunItems, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/dataset_run_items/types/__init__.py b/langfuse/api/dataset_run_items/types/__init__.py deleted file mode 100644 index b520924c0..000000000 --- a/langfuse/api/dataset_run_items/types/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .create_dataset_run_item_request import CreateDatasetRunItemRequest - from .paginated_dataset_run_items import PaginatedDatasetRunItems -_dynamic_imports: typing.Dict[str, str] = { - "CreateDatasetRunItemRequest": ".create_dataset_run_item_request", - "PaginatedDatasetRunItems": ".paginated_dataset_run_items", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["CreateDatasetRunItemRequest", "PaginatedDatasetRunItems"] diff --git a/langfuse/api/dataset_run_items/types/create_dataset_run_item_request.py b/langfuse/api/dataset_run_items/types/create_dataset_run_item_request.py deleted file mode 100644 index 20397e162..000000000 --- a/langfuse/api/dataset_run_items/types/create_dataset_run_item_request.py +++ /dev/null @@ -1,58 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class CreateDatasetRunItemRequest(UniversalBaseModel): - run_name: typing_extensions.Annotated[str, FieldMetadata(alias="runName")] - run_description: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="runDescription") - ] = pydantic.Field(default=None) - """ - Description of the run. If run exists, description will be updated. - """ - - metadata: typing.Optional[typing.Any] = pydantic.Field(default=None) - """ - Metadata of the dataset run, updates run if run already exists - """ - - dataset_item_id: typing_extensions.Annotated[ - str, FieldMetadata(alias="datasetItemId") - ] - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = pydantic.Field(default=None) - """ - traceId should always be provided. For compatibility with older SDK versions it can also be inferred from the provided observationId. - """ - - dataset_version: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="datasetVersion") - ] = pydantic.Field(default=None) - """ - ISO 8601 timestamp (RFC 3339, Section 5.6) in UTC (e.g., "2026-01-21T14:35:42Z"). - Specifies the dataset version to use for this experiment run. - If provided, the experiment will use dataset items as they existed at or before this timestamp. - If not provided, uses the latest version of dataset items. - """ - - created_at: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="createdAt") - ] = pydantic.Field(default=None) - """ - Optional timestamp to set the createdAt field of the dataset run item. If not provided or null, defaults to current timestamp. - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/dataset_run_items/types/paginated_dataset_run_items.py b/langfuse/api/dataset_run_items/types/paginated_dataset_run_items.py deleted file mode 100644 index e3d3a4889..000000000 --- a/langfuse/api/dataset_run_items/types/paginated_dataset_run_items.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...commons.types.dataset_run_item import DatasetRunItem -from ...commons.types.deprecation import Deprecation -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from ...utils.pagination.types.meta_response import MetaResponse - - -class PaginatedDatasetRunItems(UniversalBaseModel): - data: typing.List[DatasetRunItem] - meta: MetaResponse - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/datasets/__init__.py b/langfuse/api/datasets/__init__.py index e9005285e..0749dcc3f 100644 --- a/langfuse/api/datasets/__init__.py +++ b/langfuse/api/datasets/__init__.py @@ -6,16 +6,9 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .types import ( - CreateDatasetRequest, - DeleteDatasetRunResponse, - PaginatedDatasetRuns, - PaginatedDatasets, - ) + from .types import CreateDatasetRequest, PaginatedDatasets _dynamic_imports: typing.Dict[str, str] = { "CreateDatasetRequest": ".types", - "DeleteDatasetRunResponse": ".types", - "PaginatedDatasetRuns": ".types", "PaginatedDatasets": ".types", } @@ -47,9 +40,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = [ - "CreateDatasetRequest", - "DeleteDatasetRunResponse", - "PaginatedDatasetRuns", - "PaginatedDatasets", -] +__all__ = ["CreateDatasetRequest", "PaginatedDatasets"] diff --git a/langfuse/api/datasets/client.py b/langfuse/api/datasets/client.py index 6a3b9b0c8..cfa65c752 100644 --- a/langfuse/api/datasets/client.py +++ b/langfuse/api/datasets/client.py @@ -2,14 +2,10 @@ import typing -import typing_extensions from ..commons.types.dataset import Dataset -from ..commons.types.dataset_run_with_items import DatasetRunWithItems from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from .raw_client import AsyncRawDatasetsClient, RawDatasetsClient -from .types.delete_dataset_run_response import DeleteDatasetRunResponse -from .types.paginated_dataset_runs import PaginatedDatasetRuns from .types.paginated_datasets import PaginatedDatasets # this is used as the default value for optional parameters @@ -174,157 +170,6 @@ def create( ) return _response.data - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunWithItems: - """ - Get a dataset run and its items - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunWithItems - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.get_run( - dataset_name="datasetName", - run_name="runName", - ) - """ - _response = self._raw_client.get_run( - dataset_name, run_name, request_options=request_options - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def delete_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteDatasetRunResponse: - """ - Delete a dataset run and all its run items. This action is irreversible. - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteDatasetRunResponse - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.delete_run( - dataset_name="datasetName", - run_name="runName", - ) - """ - _response = self._raw_client.delete_run( - dataset_name, run_name, request_options=request_options - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_runs( - self, - dataset_name: str, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRuns: - """ - Get dataset runs - - Parameters - ---------- - dataset_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRuns - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.datasets.get_runs( - dataset_name="datasetName", - ) - """ - _response = self._raw_client.get_runs( - dataset_name, page=page, limit=limit, request_options=request_options - ) - return _response.data - class AsyncDatasetsClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -509,178 +354,3 @@ async def main() -> None: request_options=request_options, ) return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DatasetRunWithItems: - """ - Get a dataset run and its items - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DatasetRunWithItems - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.get_run( - dataset_name="datasetName", - run_name="runName", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_run( - dataset_name, run_name, request_options=request_options - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def delete_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> DeleteDatasetRunResponse: - """ - Delete a dataset run and all its run items. This action is irreversible. - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - DeleteDatasetRunResponse - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.delete_run( - dataset_name="datasetName", - run_name="runName", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.delete_run( - dataset_name, run_name, request_options=request_options - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_runs( - self, - dataset_name: str, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedDatasetRuns: - """ - Get dataset runs - - Parameters - ---------- - dataset_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedDatasetRuns - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.datasets.get_runs( - dataset_name="datasetName", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_runs( - dataset_name, page=page, limit=limit, request_options=request_options - ) - return _response.data diff --git a/langfuse/api/datasets/raw_client.py b/langfuse/api/datasets/raw_client.py index 1b0f49b77..be48ef060 100644 --- a/langfuse/api/datasets/raw_client.py +++ b/langfuse/api/datasets/raw_client.py @@ -1,7 +1,6 @@ # This file was auto-generated by Fern from our API Definition. import typing -import typing_extensions from json.decoder import JSONDecodeError from ..commons.errors.access_denied_error import AccessDeniedError @@ -10,15 +9,12 @@ from ..commons.errors.not_found_error import NotFoundError from ..commons.errors.unauthorized_error import UnauthorizedError from ..commons.types.dataset import Dataset -from ..commons.types.dataset_run_with_items import DatasetRunWithItems from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.http_response import AsyncHttpResponse, HttpResponse from ..core.jsonable_encoder import jsonable_encoder from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions -from .types.delete_dataset_run_response import DeleteDatasetRunResponse -from .types.paginated_dataset_runs import PaginatedDatasetRuns from .types.paginated_datasets import PaginatedDatasets # this is used as the default value for optional parameters @@ -369,618 +365,51 @@ def create( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[DatasetRunWithItems]: - """ - Get a dataset run and its items - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[DatasetRunWithItems] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - DatasetRunWithItems, - parse_obj_as( - type_=DatasetRunWithItems, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def delete_run( - self, - dataset_name: str, - run_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[DeleteDatasetRunResponse]: - """ - Delete a dataset run and all its run items. This action is irreversible. - - Parameters - ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[DeleteDatasetRunResponse] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - DeleteDatasetRunResponse, - parse_obj_as( - type_=DeleteDatasetRunResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_runs( - self, - dataset_name: str, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[PaginatedDatasetRuns]: - """ - Get dataset runs - - Parameters - ---------- - dataset_name : str - - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[PaginatedDatasetRuns] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs", - method="GET", - params={ - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PaginatedDatasetRuns, - parse_obj_as( - type_=PaginatedDatasetRuns, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawDatasetsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[PaginatedDatasets]: - """ - Get all datasets - - Parameters - ---------- - page : typing.Optional[int] - page number, starts at 1 - - limit : typing.Optional[int] - limit of items per page - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[PaginatedDatasets] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/datasets", - method="GET", - params={ - "page": page, - "limit": limit, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PaginatedDatasets, - parse_obj_as( - type_=PaginatedDatasets, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - async def get( - self, - dataset_name: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[Dataset]: - """ - Get a dataset - - Parameters - ---------- - dataset_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[Dataset] - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/v2/datasets/{jsonable_encoder(dataset_name)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - Dataset, - parse_obj_as( - type_=Dataset, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - async def create( +class AsyncRawDatasetsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( self, *, - name: str, - description: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Any] = OMIT, - input_schema: typing.Optional[typing.Any] = OMIT, - expected_output_schema: typing.Optional[typing.Any] = OMIT, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[Dataset]: + ) -> AsyncHttpResponse[PaginatedDatasets]: """ - Create a dataset + Get all datasets Parameters ---------- - name : str - - description : typing.Optional[str] - - metadata : typing.Optional[typing.Any] - - input_schema : typing.Optional[typing.Any] - JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. + page : typing.Optional[int] + page number, starts at 1 - expected_output_schema : typing.Optional[typing.Any] - JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. + limit : typing.Optional[int] + limit of items per page request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[Dataset] + AsyncHttpResponse[PaginatedDatasets] """ _response = await self._client_wrapper.httpx_client.request( "api/public/v2/datasets", - method="POST", - json={ - "name": name, - "description": description, - "metadata": metadata, - "inputSchema": input_schema, - "expectedOutputSchema": expected_output_schema, + method="GET", + params={ + "page": page, + "limit": limit, }, request_options=request_options, - omit=OMIT, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - Dataset, + PaginatedDatasets, parse_obj_as( - type_=Dataset, # type: ignore + type_=PaginatedDatasets, # type: ignore object_=_response.json(), ), ) @@ -1053,44 +482,37 @@ async def create( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_run( + async def get( self, dataset_name: str, - run_name: str, *, request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[DatasetRunWithItems]: + ) -> AsyncHttpResponse[Dataset]: """ - Get a dataset run and its items + Get a dataset Parameters ---------- dataset_name : str - run_name : str - request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[DatasetRunWithItems] + AsyncHttpResponse[Dataset] """ _response = await self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", + f"api/public/v2/datasets/{jsonable_encoder(dataset_name)}", method="GET", request_options=request_options, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - DatasetRunWithItems, + Dataset, parse_obj_as( - type_=DatasetRunWithItems, # type: ignore + type_=Dataset, # type: ignore object_=_response.json(), ), ) @@ -1163,163 +585,59 @@ async def get_run( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def delete_run( + async def create( self, - dataset_name: str, - run_name: str, *, + name: str, + description: typing.Optional[str] = OMIT, + metadata: typing.Optional[typing.Any] = OMIT, + input_schema: typing.Optional[typing.Any] = OMIT, + expected_output_schema: typing.Optional[typing.Any] = OMIT, request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[DeleteDatasetRunResponse]: + ) -> AsyncHttpResponse[Dataset]: """ - Delete a dataset run and all its run items. This action is irreversible. + Create a dataset Parameters ---------- - dataset_name : str - - run_name : str - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[DeleteDatasetRunResponse] - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs/{jsonable_encoder(run_name)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - DeleteDatasetRunResponse, - parse_obj_as( - type_=DeleteDatasetRunResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) + name : str - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, dataset runs are replaced by experiments; use GET /api/public/experiments instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_runs( - self, - dataset_name: str, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[PaginatedDatasetRuns]: - """ - Get dataset runs + description : typing.Optional[str] - Parameters - ---------- - dataset_name : str + metadata : typing.Optional[typing.Any] - page : typing.Optional[int] - page number, starts at 1 + input_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item inputs. When set, all new and existing dataset items will be validated against this schema. - limit : typing.Optional[int] - limit of items per page + expected_output_schema : typing.Optional[typing.Any] + JSON Schema for validating dataset item expected outputs. When set, all new and existing dataset items will be validated against this schema. request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[PaginatedDatasetRuns] + AsyncHttpResponse[Dataset] """ _response = await self._client_wrapper.httpx_client.request( - f"api/public/datasets/{jsonable_encoder(dataset_name)}/runs", - method="GET", - params={ - "page": page, - "limit": limit, + "api/public/v2/datasets", + method="POST", + json={ + "name": name, + "description": description, + "metadata": metadata, + "inputSchema": input_schema, + "expectedOutputSchema": expected_output_schema, }, request_options=request_options, + omit=OMIT, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - PaginatedDatasetRuns, + Dataset, parse_obj_as( - type_=PaginatedDatasetRuns, # type: ignore + type_=Dataset, # type: ignore object_=_response.json(), ), ) diff --git a/langfuse/api/datasets/types/__init__.py b/langfuse/api/datasets/types/__init__.py index 60c58934a..4ca73e0bc 100644 --- a/langfuse/api/datasets/types/__init__.py +++ b/langfuse/api/datasets/types/__init__.py @@ -7,13 +7,9 @@ if typing.TYPE_CHECKING: from .create_dataset_request import CreateDatasetRequest - from .delete_dataset_run_response import DeleteDatasetRunResponse - from .paginated_dataset_runs import PaginatedDatasetRuns from .paginated_datasets import PaginatedDatasets _dynamic_imports: typing.Dict[str, str] = { "CreateDatasetRequest": ".create_dataset_request", - "DeleteDatasetRunResponse": ".delete_dataset_run_response", - "PaginatedDatasetRuns": ".paginated_dataset_runs", "PaginatedDatasets": ".paginated_datasets", } @@ -45,9 +41,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = [ - "CreateDatasetRequest", - "DeleteDatasetRunResponse", - "PaginatedDatasetRuns", - "PaginatedDatasets", -] +__all__ = ["CreateDatasetRequest", "PaginatedDatasets"] diff --git a/langfuse/api/datasets/types/paginated_dataset_runs.py b/langfuse/api/datasets/types/paginated_dataset_runs.py deleted file mode 100644 index d70a9bf35..000000000 --- a/langfuse/api/datasets/types/paginated_dataset_runs.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...commons.types.dataset_run import DatasetRun -from ...commons.types.deprecation import Deprecation -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from ...utils.pagination.types.meta_response import MetaResponse - - -class PaginatedDatasetRuns(UniversalBaseModel): - data: typing.List[DatasetRun] - meta: MetaResponse - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/experiments/types/experiment.py b/langfuse/api/experiments/types/experiment.py index e52022b68..77118b4c3 100644 --- a/langfuse/api/experiments/types/experiment.py +++ b/langfuse/api/experiments/types/experiment.py @@ -7,7 +7,7 @@ import typing_extensions from ...core.pydantic_utilities import UniversalBaseModel from ...core.serialization import FieldMetadata -from ...scores_v3.types.score_v3 import ScoreV3 +from ...scores.types.score_v3 import ScoreV3 class Experiment(UniversalBaseModel): diff --git a/langfuse/api/experiments/types/experiment_item.py b/langfuse/api/experiments/types/experiment_item.py index a294c8eae..3850750e9 100644 --- a/langfuse/api/experiments/types/experiment_item.py +++ b/langfuse/api/experiments/types/experiment_item.py @@ -8,7 +8,7 @@ from ...commons.types.observation_level import ObservationLevel from ...core.pydantic_utilities import UniversalBaseModel from ...core.serialization import FieldMetadata -from ...scores_v3.types.score_v3 import ScoreV3 +from ...scores.types.score_v3 import ScoreV3 class ExperimentItem(UniversalBaseModel): diff --git a/langfuse/api/ingestion/__init__.py b/langfuse/api/ingestion/__init__.py deleted file mode 100644 index 5cd4ba3bd..000000000 --- a/langfuse/api/ingestion/__init__.py +++ /dev/null @@ -1,169 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import ( - BaseEvent, - CreateEventBody, - CreateEventEvent, - CreateGenerationBody, - CreateGenerationEvent, - CreateObservationEvent, - CreateSpanBody, - CreateSpanEvent, - IngestionError, - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, - IngestionResponse, - IngestionSuccess, - IngestionUsage, - ObservationBody, - ObservationType, - OpenAiCompletionUsageSchema, - OpenAiResponseUsageSchema, - OpenAiUsage, - OptionalObservationBody, - ScoreBody, - ScoreEvent, - SdkLogBody, - SdkLogEvent, - TraceBody, - TraceEvent, - UpdateEventBody, - UpdateGenerationBody, - UpdateGenerationEvent, - UpdateObservationEvent, - UpdateSpanBody, - UpdateSpanEvent, - UsageDetails, - ) -_dynamic_imports: typing.Dict[str, str] = { - "BaseEvent": ".types", - "CreateEventBody": ".types", - "CreateEventEvent": ".types", - "CreateGenerationBody": ".types", - "CreateGenerationEvent": ".types", - "CreateObservationEvent": ".types", - "CreateSpanBody": ".types", - "CreateSpanEvent": ".types", - "IngestionError": ".types", - "IngestionEvent": ".types", - "IngestionEvent_EventCreate": ".types", - "IngestionEvent_GenerationCreate": ".types", - "IngestionEvent_GenerationUpdate": ".types", - "IngestionEvent_ObservationCreate": ".types", - "IngestionEvent_ObservationUpdate": ".types", - "IngestionEvent_ScoreCreate": ".types", - "IngestionEvent_SdkLog": ".types", - "IngestionEvent_SpanCreate": ".types", - "IngestionEvent_SpanUpdate": ".types", - "IngestionEvent_TraceCreate": ".types", - "IngestionResponse": ".types", - "IngestionSuccess": ".types", - "IngestionUsage": ".types", - "ObservationBody": ".types", - "ObservationType": ".types", - "OpenAiCompletionUsageSchema": ".types", - "OpenAiResponseUsageSchema": ".types", - "OpenAiUsage": ".types", - "OptionalObservationBody": ".types", - "ScoreBody": ".types", - "ScoreEvent": ".types", - "SdkLogBody": ".types", - "SdkLogEvent": ".types", - "TraceBody": ".types", - "TraceEvent": ".types", - "UpdateEventBody": ".types", - "UpdateGenerationBody": ".types", - "UpdateGenerationEvent": ".types", - "UpdateObservationEvent": ".types", - "UpdateSpanBody": ".types", - "UpdateSpanEvent": ".types", - "UsageDetails": ".types", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "BaseEvent", - "CreateEventBody", - "CreateEventEvent", - "CreateGenerationBody", - "CreateGenerationEvent", - "CreateObservationEvent", - "CreateSpanBody", - "CreateSpanEvent", - "IngestionError", - "IngestionEvent", - "IngestionEvent_EventCreate", - "IngestionEvent_GenerationCreate", - "IngestionEvent_GenerationUpdate", - "IngestionEvent_ObservationCreate", - "IngestionEvent_ObservationUpdate", - "IngestionEvent_ScoreCreate", - "IngestionEvent_SdkLog", - "IngestionEvent_SpanCreate", - "IngestionEvent_SpanUpdate", - "IngestionEvent_TraceCreate", - "IngestionResponse", - "IngestionSuccess", - "IngestionUsage", - "ObservationBody", - "ObservationType", - "OpenAiCompletionUsageSchema", - "OpenAiResponseUsageSchema", - "OpenAiUsage", - "OptionalObservationBody", - "ScoreBody", - "ScoreEvent", - "SdkLogBody", - "SdkLogEvent", - "TraceBody", - "TraceEvent", - "UpdateEventBody", - "UpdateGenerationBody", - "UpdateGenerationEvent", - "UpdateObservationEvent", - "UpdateSpanBody", - "UpdateSpanEvent", - "UsageDetails", -] diff --git a/langfuse/api/ingestion/client.py b/langfuse/api/ingestion/client.py deleted file mode 100644 index c6ea6d805..000000000 --- a/langfuse/api/ingestion/client.py +++ /dev/null @@ -1,230 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import typing_extensions -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from .raw_client import AsyncRawIngestionClient, RawIngestionClient -from .types.ingestion_event import IngestionEvent -from .types.ingestion_response import IngestionResponse - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class IngestionClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawIngestionClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawIngestionClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawIngestionClient - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Send data via the OpenTelemetry endpoint at `POST /api/public/otel/v1/traces` instead, see the [OpenTelemetry integration docs](https://langfuse.com/integrations/native/opentelemetry). Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def batch( - self, - *, - batch: typing.Sequence[IngestionEvent], - metadata: typing.Optional[typing.Any] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> IngestionResponse: - """ - **Legacy endpoint for batch ingestion for Langfuse Observability.** - - -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry - - Within each batch, there can be multiple events. - Each event has a type, an id, a timestamp, metadata and a body. - Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. - We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. - The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. - I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. - - Notes: - - Introduction to data model: https://langfuse.com/docs/observability/data-model - - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. - - Parameters - ---------- - batch : typing.Sequence[IngestionEvent] - Batch of tracing events to be ingested. Discriminated by attribute `type`. - - metadata : typing.Optional[typing.Any] - Optional. Metadata field used by the Langfuse SDKs for debugging. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - IngestionResponse - - Examples - -------- - import datetime - - from langfuse import LangfuseAPI - from langfuse.ingestion import IngestionEvent_TraceCreate, TraceBody - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.ingestion.batch( - batch=[ - IngestionEvent_TraceCreate( - id="abcdef-1234-5678-90ab", - timestamp="2022-01-01T00:00:00.000Z", - body=TraceBody( - id="abcdef-1234-5678-90ab", - timestamp=datetime.datetime.fromisoformat( - "2022-01-01 00:00:00+00:00", - ), - environment="production", - name="My Trace", - user_id="1234-5678-90ab-cdef", - input="My input", - output="My output", - session_id="1234-5678-90ab-cdef", - release="1.0.0", - version="1.0.0", - metadata="My metadata", - tags=["tag1", "tag2"], - public=True, - ), - ) - ], - ) - """ - _response = self._raw_client.batch( - batch=batch, metadata=metadata, request_options=request_options - ) - return _response.data - - -class AsyncIngestionClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawIngestionClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawIngestionClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawIngestionClient - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Send data via the OpenTelemetry endpoint at `POST /api/public/otel/v1/traces` instead, see the [OpenTelemetry integration docs](https://langfuse.com/integrations/native/opentelemetry). Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def batch( - self, - *, - batch: typing.Sequence[IngestionEvent], - metadata: typing.Optional[typing.Any] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> IngestionResponse: - """ - **Legacy endpoint for batch ingestion for Langfuse Observability.** - - -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry - - Within each batch, there can be multiple events. - Each event has a type, an id, a timestamp, metadata and a body. - Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. - We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. - The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. - I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. - - Notes: - - Introduction to data model: https://langfuse.com/docs/observability/data-model - - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. - - Parameters - ---------- - batch : typing.Sequence[IngestionEvent] - Batch of tracing events to be ingested. Discriminated by attribute `type`. - - metadata : typing.Optional[typing.Any] - Optional. Metadata field used by the Langfuse SDKs for debugging. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - IngestionResponse - - Examples - -------- - import asyncio - import datetime - - from langfuse import AsyncLangfuseAPI - from langfuse.ingestion import IngestionEvent_TraceCreate, TraceBody - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.ingestion.batch( - batch=[ - IngestionEvent_TraceCreate( - id="abcdef-1234-5678-90ab", - timestamp="2022-01-01T00:00:00.000Z", - body=TraceBody( - id="abcdef-1234-5678-90ab", - timestamp=datetime.datetime.fromisoformat( - "2022-01-01 00:00:00+00:00", - ), - environment="production", - name="My Trace", - user_id="1234-5678-90ab-cdef", - input="My input", - output="My output", - session_id="1234-5678-90ab-cdef", - release="1.0.0", - version="1.0.0", - metadata="My metadata", - tags=["tag1", "tag2"], - public=True, - ), - ) - ], - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.batch( - batch=batch, metadata=metadata, request_options=request_options - ) - return _response.data diff --git a/langfuse/api/ingestion/raw_client.py b/langfuse/api/ingestion/raw_client.py deleted file mode 100644 index 67d22c556..000000000 --- a/langfuse/api/ingestion/raw_client.py +++ /dev/null @@ -1,302 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -import typing_extensions -from json.decoder import JSONDecodeError - -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..core.serialization import convert_and_respect_annotation_metadata -from .types.ingestion_event import IngestionEvent -from .types.ingestion_response import IngestionResponse - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawIngestionClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Send data via the OpenTelemetry endpoint at `POST /api/public/otel/v1/traces` instead, see the [OpenTelemetry integration docs](https://langfuse.com/integrations/native/opentelemetry). Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def batch( - self, - *, - batch: typing.Sequence[IngestionEvent], - metadata: typing.Optional[typing.Any] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[IngestionResponse]: - """ - **Legacy endpoint for batch ingestion for Langfuse Observability.** - - -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry - - Within each batch, there can be multiple events. - Each event has a type, an id, a timestamp, metadata and a body. - Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. - We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. - The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. - I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. - - Notes: - - Introduction to data model: https://langfuse.com/docs/observability/data-model - - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. - - Parameters - ---------- - batch : typing.Sequence[IngestionEvent] - Batch of tracing events to be ingested. Discriminated by attribute `type`. - - metadata : typing.Optional[typing.Any] - Optional. Metadata field used by the Langfuse SDKs for debugging. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[IngestionResponse] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/ingestion", - method="POST", - json={ - "batch": convert_and_respect_annotation_metadata( - object_=batch, - annotation=typing.Sequence[IngestionEvent], - direction="write", - ), - "metadata": metadata, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - IngestionResponse, - parse_obj_as( - type_=IngestionResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawIngestionClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Send data via the OpenTelemetry endpoint at `POST /api/public/otel/v1/traces` instead, see the [OpenTelemetry integration docs](https://langfuse.com/integrations/native/opentelemetry). Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def batch( - self, - *, - batch: typing.Sequence[IngestionEvent], - metadata: typing.Optional[typing.Any] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[IngestionResponse]: - """ - **Legacy endpoint for batch ingestion for Langfuse Observability.** - - -> Please use the OpenTelemetry endpoint (`/api/public/otel/v1/traces`). Learn more: https://langfuse.com/integrations/native/opentelemetry - - Within each batch, there can be multiple events. - Each event has a type, an id, a timestamp, metadata and a body. - Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. - We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. - The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. - I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. - - Notes: - - Introduction to data model: https://langfuse.com/docs/observability/data-model - - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. - - Parameters - ---------- - batch : typing.Sequence[IngestionEvent] - Batch of tracing events to be ingested. Discriminated by attribute `type`. - - metadata : typing.Optional[typing.Any] - Optional. Metadata field used by the Langfuse SDKs for debugging. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[IngestionResponse] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/ingestion", - method="POST", - json={ - "batch": convert_and_respect_annotation_metadata( - object_=batch, - annotation=typing.Sequence[IngestionEvent], - direction="write", - ), - "metadata": metadata, - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - IngestionResponse, - parse_obj_as( - type_=IngestionResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/ingestion/types/__init__.py b/langfuse/api/ingestion/types/__init__.py deleted file mode 100644 index 4addfd9c7..000000000 --- a/langfuse/api/ingestion/types/__init__.py +++ /dev/null @@ -1,169 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .base_event import BaseEvent - from .create_event_body import CreateEventBody - from .create_event_event import CreateEventEvent - from .create_generation_body import CreateGenerationBody - from .create_generation_event import CreateGenerationEvent - from .create_observation_event import CreateObservationEvent - from .create_span_body import CreateSpanBody - from .create_span_event import CreateSpanEvent - from .ingestion_error import IngestionError - from .ingestion_event import ( - IngestionEvent, - IngestionEvent_EventCreate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - IngestionEvent_ScoreCreate, - IngestionEvent_SdkLog, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_TraceCreate, - ) - from .ingestion_response import IngestionResponse - from .ingestion_success import IngestionSuccess - from .ingestion_usage import IngestionUsage - from .observation_body import ObservationBody - from .observation_type import ObservationType - from .open_ai_completion_usage_schema import OpenAiCompletionUsageSchema - from .open_ai_response_usage_schema import OpenAiResponseUsageSchema - from .open_ai_usage import OpenAiUsage - from .optional_observation_body import OptionalObservationBody - from .score_body import ScoreBody - from .score_event import ScoreEvent - from .sdk_log_body import SdkLogBody - from .sdk_log_event import SdkLogEvent - from .trace_body import TraceBody - from .trace_event import TraceEvent - from .update_event_body import UpdateEventBody - from .update_generation_body import UpdateGenerationBody - from .update_generation_event import UpdateGenerationEvent - from .update_observation_event import UpdateObservationEvent - from .update_span_body import UpdateSpanBody - from .update_span_event import UpdateSpanEvent - from .usage_details import UsageDetails -_dynamic_imports: typing.Dict[str, str] = { - "BaseEvent": ".base_event", - "CreateEventBody": ".create_event_body", - "CreateEventEvent": ".create_event_event", - "CreateGenerationBody": ".create_generation_body", - "CreateGenerationEvent": ".create_generation_event", - "CreateObservationEvent": ".create_observation_event", - "CreateSpanBody": ".create_span_body", - "CreateSpanEvent": ".create_span_event", - "IngestionError": ".ingestion_error", - "IngestionEvent": ".ingestion_event", - "IngestionEvent_EventCreate": ".ingestion_event", - "IngestionEvent_GenerationCreate": ".ingestion_event", - "IngestionEvent_GenerationUpdate": ".ingestion_event", - "IngestionEvent_ObservationCreate": ".ingestion_event", - "IngestionEvent_ObservationUpdate": ".ingestion_event", - "IngestionEvent_ScoreCreate": ".ingestion_event", - "IngestionEvent_SdkLog": ".ingestion_event", - "IngestionEvent_SpanCreate": ".ingestion_event", - "IngestionEvent_SpanUpdate": ".ingestion_event", - "IngestionEvent_TraceCreate": ".ingestion_event", - "IngestionResponse": ".ingestion_response", - "IngestionSuccess": ".ingestion_success", - "IngestionUsage": ".ingestion_usage", - "ObservationBody": ".observation_body", - "ObservationType": ".observation_type", - "OpenAiCompletionUsageSchema": ".open_ai_completion_usage_schema", - "OpenAiResponseUsageSchema": ".open_ai_response_usage_schema", - "OpenAiUsage": ".open_ai_usage", - "OptionalObservationBody": ".optional_observation_body", - "ScoreBody": ".score_body", - "ScoreEvent": ".score_event", - "SdkLogBody": ".sdk_log_body", - "SdkLogEvent": ".sdk_log_event", - "TraceBody": ".trace_body", - "TraceEvent": ".trace_event", - "UpdateEventBody": ".update_event_body", - "UpdateGenerationBody": ".update_generation_body", - "UpdateGenerationEvent": ".update_generation_event", - "UpdateObservationEvent": ".update_observation_event", - "UpdateSpanBody": ".update_span_body", - "UpdateSpanEvent": ".update_span_event", - "UsageDetails": ".usage_details", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "BaseEvent", - "CreateEventBody", - "CreateEventEvent", - "CreateGenerationBody", - "CreateGenerationEvent", - "CreateObservationEvent", - "CreateSpanBody", - "CreateSpanEvent", - "IngestionError", - "IngestionEvent", - "IngestionEvent_EventCreate", - "IngestionEvent_GenerationCreate", - "IngestionEvent_GenerationUpdate", - "IngestionEvent_ObservationCreate", - "IngestionEvent_ObservationUpdate", - "IngestionEvent_ScoreCreate", - "IngestionEvent_SdkLog", - "IngestionEvent_SpanCreate", - "IngestionEvent_SpanUpdate", - "IngestionEvent_TraceCreate", - "IngestionResponse", - "IngestionSuccess", - "IngestionUsage", - "ObservationBody", - "ObservationType", - "OpenAiCompletionUsageSchema", - "OpenAiResponseUsageSchema", - "OpenAiUsage", - "OptionalObservationBody", - "ScoreBody", - "ScoreEvent", - "SdkLogBody", - "SdkLogEvent", - "TraceBody", - "TraceEvent", - "UpdateEventBody", - "UpdateGenerationBody", - "UpdateGenerationEvent", - "UpdateObservationEvent", - "UpdateSpanBody", - "UpdateSpanEvent", - "UsageDetails", -] diff --git a/langfuse/api/ingestion/types/base_event.py b/langfuse/api/ingestion/types/base_event.py deleted file mode 100644 index 70c6bcfa4..000000000 --- a/langfuse/api/ingestion/types/base_event.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import UniversalBaseModel - - -class BaseEvent(UniversalBaseModel): - id: str = pydantic.Field() - """ - UUID v4 that identifies the event - """ - - timestamp: str = pydantic.Field() - """ - Datetime (ISO 8601) of event creation in client. Should be as close to actual event creation in client as possible, this timestamp will be used for ordering of events in future release. Resolution: milliseconds (required), microseconds (optimal). - """ - - metadata: typing.Optional[typing.Any] = pydantic.Field(default=None) - """ - Optional. Metadata field used by the Langfuse SDKs for debugging. - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/create_event_event.py b/langfuse/api/ingestion/types/create_event_event.py deleted file mode 100644 index e0cc820e1..000000000 --- a/langfuse/api/ingestion/types/create_event_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .create_event_body import CreateEventBody - - -class CreateEventEvent(BaseEvent): - body: CreateEventBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/create_generation_body.py b/langfuse/api/ingestion/types/create_generation_body.py deleted file mode 100644 index 72cb57116..000000000 --- a/langfuse/api/ingestion/types/create_generation_body.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...commons.types.map_value import MapValue -from ...core.serialization import FieldMetadata -from .create_span_body import CreateSpanBody -from .ingestion_usage import IngestionUsage -from .usage_details import UsageDetails - - -class CreateGenerationBody(CreateSpanBody): - completion_start_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") - ] = None - model: typing.Optional[str] = None - model_parameters: typing_extensions.Annotated[ - typing.Optional[typing.Dict[str, MapValue]], - FieldMetadata(alias="modelParameters"), - ] = None - usage: typing.Optional[IngestionUsage] = None - usage_details: typing_extensions.Annotated[ - typing.Optional[UsageDetails], FieldMetadata(alias="usageDetails") - ] = None - cost_details: typing_extensions.Annotated[ - typing.Optional[typing.Dict[str, float]], FieldMetadata(alias="costDetails") - ] = None - prompt_name: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="promptName") - ] = None - prompt_version: typing_extensions.Annotated[ - typing.Optional[int], FieldMetadata(alias="promptVersion") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/create_generation_event.py b/langfuse/api/ingestion/types/create_generation_event.py deleted file mode 100644 index d62d6cc41..000000000 --- a/langfuse/api/ingestion/types/create_generation_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .create_generation_body import CreateGenerationBody - - -class CreateGenerationEvent(BaseEvent): - body: CreateGenerationBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/create_observation_event.py b/langfuse/api/ingestion/types/create_observation_event.py deleted file mode 100644 index 06d927f36..000000000 --- a/langfuse/api/ingestion/types/create_observation_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .observation_body import ObservationBody - - -class CreateObservationEvent(BaseEvent): - body: ObservationBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/create_span_body.py b/langfuse/api/ingestion/types/create_span_body.py deleted file mode 100644 index 7a47d9748..000000000 --- a/langfuse/api/ingestion/types/create_span_body.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .create_event_body import CreateEventBody - - -class CreateSpanBody(CreateEventBody): - end_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="endTime") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/create_span_event.py b/langfuse/api/ingestion/types/create_span_event.py deleted file mode 100644 index 6e60cf1fe..000000000 --- a/langfuse/api/ingestion/types/create_span_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .create_span_body import CreateSpanBody - - -class CreateSpanEvent(BaseEvent): - body: CreateSpanBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/ingestion_event.py b/langfuse/api/ingestion/types/ingestion_event.py deleted file mode 100644 index 03202e635..000000000 --- a/langfuse/api/ingestion/types/ingestion_event.py +++ /dev/null @@ -1,155 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from .create_event_body import CreateEventBody -from .create_generation_body import CreateGenerationBody -from .create_span_body import CreateSpanBody -from .observation_body import ObservationBody -from .score_body import ScoreBody -from .sdk_log_body import SdkLogBody -from .trace_body import TraceBody -from .update_generation_body import UpdateGenerationBody -from .update_span_body import UpdateSpanBody - - -class IngestionEvent_TraceCreate(UniversalBaseModel): - type: typing.Literal["trace-create"] = "trace-create" - body: TraceBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_ScoreCreate(UniversalBaseModel): - type: typing.Literal["score-create"] = "score-create" - body: ScoreBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_SpanCreate(UniversalBaseModel): - type: typing.Literal["span-create"] = "span-create" - body: CreateSpanBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_SpanUpdate(UniversalBaseModel): - type: typing.Literal["span-update"] = "span-update" - body: UpdateSpanBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_GenerationCreate(UniversalBaseModel): - type: typing.Literal["generation-create"] = "generation-create" - body: CreateGenerationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_GenerationUpdate(UniversalBaseModel): - type: typing.Literal["generation-update"] = "generation-update" - body: UpdateGenerationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_EventCreate(UniversalBaseModel): - type: typing.Literal["event-create"] = "event-create" - body: CreateEventBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_SdkLog(UniversalBaseModel): - type: typing.Literal["sdk-log"] = "sdk-log" - body: SdkLogBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_ObservationCreate(UniversalBaseModel): - type: typing.Literal["observation-create"] = "observation-create" - body: ObservationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class IngestionEvent_ObservationUpdate(UniversalBaseModel): - type: typing.Literal["observation-update"] = "observation-update" - body: ObservationBody - id: str - timestamp: str - metadata: typing.Optional[typing.Any] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -IngestionEvent = typing_extensions.Annotated[ - typing.Union[ - IngestionEvent_TraceCreate, - IngestionEvent_ScoreCreate, - IngestionEvent_SpanCreate, - IngestionEvent_SpanUpdate, - IngestionEvent_GenerationCreate, - IngestionEvent_GenerationUpdate, - IngestionEvent_EventCreate, - IngestionEvent_SdkLog, - IngestionEvent_ObservationCreate, - IngestionEvent_ObservationUpdate, - ], - pydantic.Field(discriminator="type"), -] diff --git a/langfuse/api/ingestion/types/ingestion_response.py b/langfuse/api/ingestion/types/ingestion_response.py deleted file mode 100644 index b9781fab5..000000000 --- a/langfuse/api/ingestion/types/ingestion_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import UniversalBaseModel -from .ingestion_error import IngestionError -from .ingestion_success import IngestionSuccess - - -class IngestionResponse(UniversalBaseModel): - successes: typing.List[IngestionSuccess] - errors: typing.List[IngestionError] - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/ingestion_success.py b/langfuse/api/ingestion/types/ingestion_success.py deleted file mode 100644 index e7894797a..000000000 --- a/langfuse/api/ingestion/types/ingestion_success.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import UniversalBaseModel - - -class IngestionSuccess(UniversalBaseModel): - id: str - status: int - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/ingestion_usage.py b/langfuse/api/ingestion/types/ingestion_usage.py deleted file mode 100644 index 8b3f67103..000000000 --- a/langfuse/api/ingestion/types/ingestion_usage.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ...commons.types.usage import Usage -from .open_ai_usage import OpenAiUsage - -IngestionUsage = typing.Union[Usage, OpenAiUsage] diff --git a/langfuse/api/ingestion/types/observation_body.py b/langfuse/api/ingestion/types/observation_body.py deleted file mode 100644 index e989a768f..000000000 --- a/langfuse/api/ingestion/types/observation_body.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...commons.types.map_value import MapValue -from ...commons.types.observation_level import ObservationLevel -from ...commons.types.usage import Usage -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from .observation_type import ObservationType - - -class ObservationBody(UniversalBaseModel): - id: typing.Optional[str] = None - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - type: ObservationType - name: typing.Optional[str] = None - start_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="startTime") - ] = None - end_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="endTime") - ] = None - completion_start_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") - ] = None - model: typing.Optional[str] = None - model_parameters: typing_extensions.Annotated[ - typing.Optional[typing.Dict[str, MapValue]], - FieldMetadata(alias="modelParameters"), - ] = None - input: typing.Optional[typing.Any] = None - version: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - output: typing.Optional[typing.Any] = None - usage: typing.Optional[Usage] = None - level: typing.Optional[ObservationLevel] = None - status_message: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="statusMessage") - ] = None - parent_observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="parentObservationId") - ] = None - environment: typing.Optional[str] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/observation_type.py b/langfuse/api/ingestion/types/observation_type.py deleted file mode 100644 index f769e34cf..000000000 --- a/langfuse/api/ingestion/types/observation_type.py +++ /dev/null @@ -1,54 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ...core import enum - -T_Result = typing.TypeVar("T_Result") - - -class ObservationType(enum.StrEnum): - SPAN = "SPAN" - GENERATION = "GENERATION" - EVENT = "EVENT" - AGENT = "AGENT" - TOOL = "TOOL" - CHAIN = "CHAIN" - RETRIEVER = "RETRIEVER" - EVALUATOR = "EVALUATOR" - EMBEDDING = "EMBEDDING" - GUARDRAIL = "GUARDRAIL" - - def visit( - self, - span: typing.Callable[[], T_Result], - generation: typing.Callable[[], T_Result], - event: typing.Callable[[], T_Result], - agent: typing.Callable[[], T_Result], - tool: typing.Callable[[], T_Result], - chain: typing.Callable[[], T_Result], - retriever: typing.Callable[[], T_Result], - evaluator: typing.Callable[[], T_Result], - embedding: typing.Callable[[], T_Result], - guardrail: typing.Callable[[], T_Result], - ) -> T_Result: - if self is ObservationType.SPAN: - return span() - if self is ObservationType.GENERATION: - return generation() - if self is ObservationType.EVENT: - return event() - if self is ObservationType.AGENT: - return agent() - if self is ObservationType.TOOL: - return tool() - if self is ObservationType.CHAIN: - return chain() - if self is ObservationType.RETRIEVER: - return retriever() - if self is ObservationType.EVALUATOR: - return evaluator() - if self is ObservationType.EMBEDDING: - return embedding() - if self is ObservationType.GUARDRAIL: - return guardrail() diff --git a/langfuse/api/ingestion/types/open_ai_completion_usage_schema.py b/langfuse/api/ingestion/types/open_ai_completion_usage_schema.py deleted file mode 100644 index 292c51e79..000000000 --- a/langfuse/api/ingestion/types/open_ai_completion_usage_schema.py +++ /dev/null @@ -1,26 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import UniversalBaseModel - - -class OpenAiCompletionUsageSchema(UniversalBaseModel): - """ - OpenAI Usage schema from (Chat-)Completion APIs - """ - - prompt_tokens: int - completion_tokens: int - total_tokens: int - prompt_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = ( - None - ) - completion_tokens_details: typing.Optional[ - typing.Dict[str, typing.Optional[int]] - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/open_ai_response_usage_schema.py b/langfuse/api/ingestion/types/open_ai_response_usage_schema.py deleted file mode 100644 index 93cbc7dba..000000000 --- a/langfuse/api/ingestion/types/open_ai_response_usage_schema.py +++ /dev/null @@ -1,24 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import UniversalBaseModel - - -class OpenAiResponseUsageSchema(UniversalBaseModel): - """ - OpenAI Usage schema from Response API - """ - - input_tokens: int - output_tokens: int - total_tokens: int - input_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = None - output_tokens_details: typing.Optional[typing.Dict[str, typing.Optional[int]]] = ( - None - ) - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/optional_observation_body.py b/langfuse/api/ingestion/types/optional_observation_body.py deleted file mode 100644 index f2aaf9b6d..000000000 --- a/langfuse/api/ingestion/types/optional_observation_body.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...commons.types.observation_level import ObservationLevel -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class OptionalObservationBody(UniversalBaseModel): - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - name: typing.Optional[str] = None - start_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="startTime") - ] = None - metadata: typing.Optional[typing.Any] = None - input: typing.Optional[typing.Any] = None - output: typing.Optional[typing.Any] = None - level: typing.Optional[ObservationLevel] = None - status_message: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="statusMessage") - ] = None - parent_observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="parentObservationId") - ] = None - version: typing.Optional[str] = None - environment: typing.Optional[str] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/sdk_log_event.py b/langfuse/api/ingestion/types/sdk_log_event.py deleted file mode 100644 index ca303af55..000000000 --- a/langfuse/api/ingestion/types/sdk_log_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .sdk_log_body import SdkLogBody - - -class SdkLogEvent(BaseEvent): - body: SdkLogBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/trace_body.py b/langfuse/api/ingestion/types/trace_body.py deleted file mode 100644 index 7fb2842a0..000000000 --- a/langfuse/api/ingestion/types/trace_body.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class TraceBody(UniversalBaseModel): - id: typing.Optional[str] = None - timestamp: typing.Optional[dt.datetime] = None - name: typing.Optional[str] = None - user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="userId") - ] = None - input: typing.Optional[typing.Any] = None - output: typing.Optional[typing.Any] = None - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = None - release: typing.Optional[str] = None - version: typing.Optional[str] = None - metadata: typing.Optional[typing.Any] = None - tags: typing.Optional[typing.List[str]] = None - environment: typing.Optional[str] = None - public: typing.Optional[bool] = pydantic.Field(default=None) - """ - Make trace publicly accessible via url - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/trace_event.py b/langfuse/api/ingestion/types/trace_event.py deleted file mode 100644 index 54127597a..000000000 --- a/langfuse/api/ingestion/types/trace_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .trace_body import TraceBody - - -class TraceEvent(BaseEvent): - body: TraceBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/update_event_body.py b/langfuse/api/ingestion/types/update_event_body.py deleted file mode 100644 index 055f66f08..000000000 --- a/langfuse/api/ingestion/types/update_event_body.py +++ /dev/null @@ -1,14 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .optional_observation_body import OptionalObservationBody - - -class UpdateEventBody(OptionalObservationBody): - id: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/update_generation_body.py b/langfuse/api/ingestion/types/update_generation_body.py deleted file mode 100644 index 1d453e759..000000000 --- a/langfuse/api/ingestion/types/update_generation_body.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...commons.types.map_value import MapValue -from ...core.serialization import FieldMetadata -from .ingestion_usage import IngestionUsage -from .update_span_body import UpdateSpanBody -from .usage_details import UsageDetails - - -class UpdateGenerationBody(UpdateSpanBody): - completion_start_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="completionStartTime") - ] = None - model: typing.Optional[str] = None - model_parameters: typing_extensions.Annotated[ - typing.Optional[typing.Dict[str, MapValue]], - FieldMetadata(alias="modelParameters"), - ] = None - usage: typing.Optional[IngestionUsage] = None - prompt_name: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="promptName") - ] = None - usage_details: typing_extensions.Annotated[ - typing.Optional[UsageDetails], FieldMetadata(alias="usageDetails") - ] = None - cost_details: typing_extensions.Annotated[ - typing.Optional[typing.Dict[str, float]], FieldMetadata(alias="costDetails") - ] = None - prompt_version: typing_extensions.Annotated[ - typing.Optional[int], FieldMetadata(alias="promptVersion") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/update_generation_event.py b/langfuse/api/ingestion/types/update_generation_event.py deleted file mode 100644 index e2c7fe284..000000000 --- a/langfuse/api/ingestion/types/update_generation_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .update_generation_body import UpdateGenerationBody - - -class UpdateGenerationEvent(BaseEvent): - body: UpdateGenerationBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/update_observation_event.py b/langfuse/api/ingestion/types/update_observation_event.py deleted file mode 100644 index 5c33e7591..000000000 --- a/langfuse/api/ingestion/types/update_observation_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .observation_body import ObservationBody - - -class UpdateObservationEvent(BaseEvent): - body: ObservationBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/update_span_body.py b/langfuse/api/ingestion/types/update_span_body.py deleted file mode 100644 index f094b7cdb..000000000 --- a/langfuse/api/ingestion/types/update_span_body.py +++ /dev/null @@ -1,19 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...core.serialization import FieldMetadata -from .update_event_body import UpdateEventBody - - -class UpdateSpanBody(UpdateEventBody): - end_time: typing_extensions.Annotated[ - typing.Optional[dt.datetime], FieldMetadata(alias="endTime") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/update_span_event.py b/langfuse/api/ingestion/types/update_span_event.py deleted file mode 100644 index 20214ac9d..000000000 --- a/langfuse/api/ingestion/types/update_span_event.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from .base_event import BaseEvent -from .update_span_body import UpdateSpanBody - - -class UpdateSpanEvent(BaseEvent): - body: UpdateSpanBody - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/ingestion/types/usage_details.py b/langfuse/api/ingestion/types/usage_details.py deleted file mode 100644 index beb98cbd0..000000000 --- a/langfuse/api/ingestion/types/usage_details.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from .open_ai_completion_usage_schema import OpenAiCompletionUsageSchema -from .open_ai_response_usage_schema import OpenAiResponseUsageSchema - -UsageDetails = typing.Union[ - typing.Dict[str, int], OpenAiCompletionUsageSchema, OpenAiResponseUsageSchema -] diff --git a/langfuse/api/legacy/__init__.py b/langfuse/api/legacy/__init__.py deleted file mode 100644 index e76c96247..000000000 --- a/langfuse/api/legacy/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from . import metrics_v1, observations_v1, score_v1 - from .metrics_v1 import MetricsResponse - from .observations_v1 import Observations, ObservationsViews -_dynamic_imports: typing.Dict[str, str] = { - "MetricsResponse": ".metrics_v1", - "Observations": ".observations_v1", - "ObservationsViews": ".observations_v1", - "metrics_v1": ".metrics_v1", - "observations_v1": ".observations_v1", - "score_v1": ".score_v1", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "MetricsResponse", - "Observations", - "ObservationsViews", - "metrics_v1", - "observations_v1", - "score_v1", -] - -# Score-create compatibility aliases (LFE-10397). -from .score_v1 import CreateScoreRequest -from .score_v1 import CreateScoreResponse -from .score_v1 import CreateScoreSource - -__all__ = [*__all__, "CreateScoreRequest", "CreateScoreResponse", "CreateScoreSource"] diff --git a/langfuse/api/legacy/client.py b/langfuse/api/legacy/client.py deleted file mode 100644 index 3886b9eac..000000000 --- a/langfuse/api/legacy/client.py +++ /dev/null @@ -1,105 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from .raw_client import AsyncRawLegacyClient, RawLegacyClient - -if typing.TYPE_CHECKING: - from .metrics_v1.client import AsyncMetricsV1Client, MetricsV1Client - from .observations_v1.client import AsyncObservationsV1Client, ObservationsV1Client - from .score_v1.client import AsyncScoreV1Client, ScoreV1Client - - -class LegacyClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawLegacyClient(client_wrapper=client_wrapper) - self._client_wrapper = client_wrapper - self._metrics_v1: typing.Optional[MetricsV1Client] = None - self._observations_v1: typing.Optional[ObservationsV1Client] = None - self._score_v1: typing.Optional[ScoreV1Client] = None - - @property - def with_raw_response(self) -> RawLegacyClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawLegacyClient - """ - return self._raw_client - - @property - def metrics_v1(self): - if self._metrics_v1 is None: - from .metrics_v1.client import MetricsV1Client # noqa: E402 - - self._metrics_v1 = MetricsV1Client(client_wrapper=self._client_wrapper) - return self._metrics_v1 - - @property - def observations_v1(self): - if self._observations_v1 is None: - from .observations_v1.client import ObservationsV1Client # noqa: E402 - - self._observations_v1 = ObservationsV1Client( - client_wrapper=self._client_wrapper - ) - return self._observations_v1 - - @property - def score_v1(self): - if self._score_v1 is None: - from .score_v1.client import ScoreV1Client # noqa: E402 - - self._score_v1 = ScoreV1Client(client_wrapper=self._client_wrapper) - return self._score_v1 - - -class AsyncLegacyClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawLegacyClient(client_wrapper=client_wrapper) - self._client_wrapper = client_wrapper - self._metrics_v1: typing.Optional[AsyncMetricsV1Client] = None - self._observations_v1: typing.Optional[AsyncObservationsV1Client] = None - self._score_v1: typing.Optional[AsyncScoreV1Client] = None - - @property - def with_raw_response(self) -> AsyncRawLegacyClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawLegacyClient - """ - return self._raw_client - - @property - def metrics_v1(self): - if self._metrics_v1 is None: - from .metrics_v1.client import AsyncMetricsV1Client # noqa: E402 - - self._metrics_v1 = AsyncMetricsV1Client(client_wrapper=self._client_wrapper) - return self._metrics_v1 - - @property - def observations_v1(self): - if self._observations_v1 is None: - from .observations_v1.client import AsyncObservationsV1Client # noqa: E402 - - self._observations_v1 = AsyncObservationsV1Client( - client_wrapper=self._client_wrapper - ) - return self._observations_v1 - - @property - def score_v1(self): - if self._score_v1 is None: - from .score_v1.client import AsyncScoreV1Client # noqa: E402 - - self._score_v1 = AsyncScoreV1Client(client_wrapper=self._client_wrapper) - return self._score_v1 diff --git a/langfuse/api/legacy/metrics_v1/__init__.py b/langfuse/api/legacy/metrics_v1/__init__.py deleted file mode 100644 index fb47bc976..000000000 --- a/langfuse/api/legacy/metrics_v1/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import MetricsResponse -_dynamic_imports: typing.Dict[str, str] = {"MetricsResponse": ".types"} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["MetricsResponse"] diff --git a/langfuse/api/legacy/metrics_v1/client.py b/langfuse/api/legacy/metrics_v1/client.py deleted file mode 100644 index 696afc985..000000000 --- a/langfuse/api/legacy/metrics_v1/client.py +++ /dev/null @@ -1,223 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import typing_extensions -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.request_options import RequestOptions -from .raw_client import AsyncRawMetricsV1Client, RawMetricsV1Client -from .types.metrics_response import MetricsResponse - - -class MetricsV1Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawMetricsV1Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawMetricsV1Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawMetricsV1Client - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/metrics?query=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def metrics( - self, *, query: str, request_options: typing.Optional[RequestOptions] = None - ) -> MetricsResponse: - """ - Get metrics from the Langfuse project using a query object. - - Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. - - For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). - - Parameters - ---------- - query : str - JSON string containing the query parameters with the following structure: - ```json - { - "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-boolean", "scores-categorical" - "dimensions": [ // Optional. Default: [] - { - "field": string // Field to group by, e.g. "name", "userId", "sessionId" - } - ], - "metrics": [ // Required. At least one metric must be provided - { - "measure": string, // What to measure, e.g. "count", "latency", "value" - "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" - } - ], - "filters": [ // Optional. Default: [] - { - "column": string, // Column to filter on - "operator": string, // Operator, e.g. "=", ">", "<", "contains" - "value": any, // Value to compare against - "type": string, // Data type, e.g. "string", "number", "stringObject" - "key": string // Required only when filtering on metadata - } - ], - "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time - "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" - }, - "fromTimestamp": string, // Required. ISO datetime string for start of time range - "toTimestamp": string, // Required. ISO datetime string for end of time range - "orderBy": [ // Optional. Default: null - { - "field": string, // Field to order by - "direction": string // "asc" or "desc" - } - ], - "config": { // Optional. Query-specific configuration - "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 - "row_limit": number // Optional. Row limit for results (1-1000) - } - } - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MetricsResponse - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.legacy.metrics_v1.metrics( - query="query", - ) - """ - _response = self._raw_client.metrics( - query=query, request_options=request_options - ) - return _response.data - - -class AsyncMetricsV1Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawMetricsV1Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawMetricsV1Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawMetricsV1Client - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/metrics?query=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def metrics( - self, *, query: str, request_options: typing.Optional[RequestOptions] = None - ) -> MetricsResponse: - """ - Get metrics from the Langfuse project using a query object. - - Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. - - For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). - - Parameters - ---------- - query : str - JSON string containing the query parameters with the following structure: - ```json - { - "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-boolean", "scores-categorical" - "dimensions": [ // Optional. Default: [] - { - "field": string // Field to group by, e.g. "name", "userId", "sessionId" - } - ], - "metrics": [ // Required. At least one metric must be provided - { - "measure": string, // What to measure, e.g. "count", "latency", "value" - "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" - } - ], - "filters": [ // Optional. Default: [] - { - "column": string, // Column to filter on - "operator": string, // Operator, e.g. "=", ">", "<", "contains" - "value": any, // Value to compare against - "type": string, // Data type, e.g. "string", "number", "stringObject" - "key": string // Required only when filtering on metadata - } - ], - "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time - "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" - }, - "fromTimestamp": string, // Required. ISO datetime string for start of time range - "toTimestamp": string, // Required. ISO datetime string for end of time range - "orderBy": [ // Optional. Default: null - { - "field": string, // Field to order by - "direction": string // "asc" or "desc" - } - ], - "config": { // Optional. Query-specific configuration - "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 - "row_limit": number // Optional. Row limit for results (1-1000) - } - } - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MetricsResponse - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.legacy.metrics_v1.metrics( - query="query", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.metrics( - query=query, request_options=request_options - ) - return _response.data diff --git a/langfuse/api/legacy/metrics_v1/raw_client.py b/langfuse/api/legacy/metrics_v1/raw_client.py deleted file mode 100644 index 3d396981e..000000000 --- a/langfuse/api/legacy/metrics_v1/raw_client.py +++ /dev/null @@ -1,331 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -import typing_extensions -from json.decoder import JSONDecodeError - -from ...commons.errors.access_denied_error import AccessDeniedError -from ...commons.errors.error import Error -from ...commons.errors.method_not_allowed_error import MethodNotAllowedError -from ...commons.errors.not_found_error import NotFoundError -from ...commons.errors.unauthorized_error import UnauthorizedError -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.http_response import AsyncHttpResponse, HttpResponse -from ...core.pydantic_utilities import parse_obj_as -from ...core.request_options import RequestOptions -from .types.metrics_response import MetricsResponse - - -class RawMetricsV1Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/metrics?query=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def metrics( - self, *, query: str, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[MetricsResponse]: - """ - Get metrics from the Langfuse project using a query object. - - Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. - - For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). - - Parameters - ---------- - query : str - JSON string containing the query parameters with the following structure: - ```json - { - "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-boolean", "scores-categorical" - "dimensions": [ // Optional. Default: [] - { - "field": string // Field to group by, e.g. "name", "userId", "sessionId" - } - ], - "metrics": [ // Required. At least one metric must be provided - { - "measure": string, // What to measure, e.g. "count", "latency", "value" - "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" - } - ], - "filters": [ // Optional. Default: [] - { - "column": string, // Column to filter on - "operator": string, // Operator, e.g. "=", ">", "<", "contains" - "value": any, // Value to compare against - "type": string, // Data type, e.g. "string", "number", "stringObject" - "key": string // Required only when filtering on metadata - } - ], - "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time - "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" - }, - "fromTimestamp": string, // Required. ISO datetime string for start of time range - "toTimestamp": string, // Required. ISO datetime string for end of time range - "orderBy": [ // Optional. Default: null - { - "field": string, // Field to order by - "direction": string // "asc" or "desc" - } - ], - "config": { // Optional. Query-specific configuration - "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 - "row_limit": number // Optional. Row limit for results (1-1000) - } - } - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[MetricsResponse] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/metrics", - method="GET", - params={ - "query": query, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MetricsResponse, - parse_obj_as( - type_=MetricsResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawMetricsV1Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/metrics?query=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def metrics( - self, *, query: str, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[MetricsResponse]: - """ - Get metrics from the Langfuse project using a query object. - - Consider using the [v2 metrics endpoint](/api-reference#tag/metricsv2/GET/api/public/v2/metrics) for better performance. - - For more details, see the [Metrics API documentation](https://langfuse.com/docs/metrics/features/metrics-api). - - Parameters - ---------- - query : str - JSON string containing the query parameters with the following structure: - ```json - { - "view": string, // Required. One of "traces", "observations", "scores-numeric", "scores-boolean", "scores-categorical" - "dimensions": [ // Optional. Default: [] - { - "field": string // Field to group by, e.g. "name", "userId", "sessionId" - } - ], - "metrics": [ // Required. At least one metric must be provided - { - "measure": string, // What to measure, e.g. "count", "latency", "value" - "aggregation": string // How to aggregate, e.g. "count", "sum", "avg", "p95", "histogram" - } - ], - "filters": [ // Optional. Default: [] - { - "column": string, // Column to filter on - "operator": string, // Operator, e.g. "=", ">", "<", "contains" - "value": any, // Value to compare against - "type": string, // Data type, e.g. "string", "number", "stringObject" - "key": string // Required only when filtering on metadata - } - ], - "timeDimension": { // Optional. Default: null. If provided, results will be grouped by time - "granularity": string // One of "minute", "hour", "day", "week", "month", "auto" - }, - "fromTimestamp": string, // Required. ISO datetime string for start of time range - "toTimestamp": string, // Required. ISO datetime string for end of time range - "orderBy": [ // Optional. Default: null - { - "field": string, // Field to order by - "direction": string // "asc" or "desc" - } - ], - "config": { // Optional. Query-specific configuration - "bins": number, // Optional. Number of bins for histogram (1-100), default: 10 - "row_limit": number // Optional. Row limit for results (1-1000) - } - } - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[MetricsResponse] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/metrics", - method="GET", - params={ - "query": query, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MetricsResponse, - parse_obj_as( - type_=MetricsResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/legacy/metrics_v1/types/__init__.py b/langfuse/api/legacy/metrics_v1/types/__init__.py deleted file mode 100644 index 308847504..000000000 --- a/langfuse/api/legacy/metrics_v1/types/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .metrics_response import MetricsResponse -_dynamic_imports: typing.Dict[str, str] = {"MetricsResponse": ".metrics_response"} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["MetricsResponse"] diff --git a/langfuse/api/legacy/metrics_v1/types/metrics_response.py b/langfuse/api/legacy/metrics_v1/types/metrics_response.py deleted file mode 100644 index 546f19fd6..000000000 --- a/langfuse/api/legacy/metrics_v1/types/metrics_response.py +++ /dev/null @@ -1,26 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ....commons.types.deprecation import Deprecation -from ....core.pydantic_utilities import UniversalBaseModel -from ....core.serialization import FieldMetadata - - -class MetricsResponse(UniversalBaseModel): - data: typing.List[typing.Dict[str, typing.Any]] = pydantic.Field() - """ - The metrics data. Each item in the list contains the metric values and dimensions requested in the query. - Format varies based on the query parameters. - Histograms will return an array with [lower, upper, height] tuples. - """ - - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/legacy/observations_v1/__init__.py b/langfuse/api/legacy/observations_v1/__init__.py deleted file mode 100644 index 22b445984..000000000 --- a/langfuse/api/legacy/observations_v1/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import Observations, ObservationsViews -_dynamic_imports: typing.Dict[str, str] = { - "Observations": ".types", - "ObservationsViews": ".types", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["Observations", "ObservationsViews"] diff --git a/langfuse/api/legacy/observations_v1/client.py b/langfuse/api/legacy/observations_v1/client.py deleted file mode 100644 index 46ee7772a..000000000 --- a/langfuse/api/legacy/observations_v1/client.py +++ /dev/null @@ -1,540 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import typing_extensions -from ...commons.types.observation_level import ObservationLevel -from ...commons.types.observations_view_single import ObservationsViewSingle -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.request_options import RequestOptions -from .raw_client import AsyncRawObservationsV1Client, RawObservationsV1Client -from .types.observations_views import ObservationsViews - - -class ObservationsV1Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawObservationsV1Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawObservationsV1Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawObservationsV1Client - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get( - self, - observation_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsViewSingle: - """ - Get a observation - - Parameters - ---------- - observation_id : str - The unique langfuse identifier of an observation, can be an event, span or generation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsViewSingle - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.legacy.observations_v1.get( - observation_id="observationId", - ) - """ - _response = self._raw_client.get( - observation_id, request_options=request_options - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - name: typing.Optional[str] = None, - user_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - level: typing.Optional[ObservationLevel] = None, - parent_observation_id: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - from_start_time: typing.Optional[dt.datetime] = None, - to_start_time: typing.Optional[dt.datetime] = None, - version: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsViews: - """ - Get a list of observations. - - Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - name : typing.Optional[str] - - user_id : typing.Optional[str] - - type : typing.Optional[str] - - trace_id : typing.Optional[str] - - level : typing.Optional[ObservationLevel] - Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). - - parent_observation_id : typing.Optional[str] - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for observations where the environment is one of the provided values. - - from_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time on or after this datetime (ISO 8601). - - to_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time before this datetime (ISO 8601). - - version : typing.Optional[str] - Optional filter to only include observations with a certain version. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata - } - ] - ``` - - ## Available Columns - - ### Core Observation Fields - - `id` (string) - Observation ID - - `type` (string) - Observation type (SPAN, GENERATION, EVENT) - - `name` (string) - Observation name - - `traceId` (string) - Associated trace ID - - `startTime` (datetime) - Observation start time - - `endTime` (datetime) - Observation end time - - `environment` (string) - Environment tag - - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) - - `statusMessage` (string) - Status message - - `version` (string) - Version tag - - ### Performance Metrics - - `latency` (number) - Latency in seconds (calculated: end_time - start_time) - - `timeToFirstToken` (number) - Time to first token in seconds - - `tokensPerSecond` (number) - Output tokens per second - - ### Token Usage - - `inputTokens` (number) - Number of input tokens - - `outputTokens` (number) - Number of output tokens - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - ### Cost Metrics - - `inputCost` (number) - Input cost in USD - - `outputCost` (number) - Output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Model Information - - `model` (string) - Provided model name - - `promptName` (string) - Associated prompt name - - `promptVersion` (number) - Associated prompt version - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Associated Trace Fields (requires join with traces table) - - `userId` (string) - User ID from associated trace - - `traceName` (string) - Name from associated trace - - `traceEnvironment` (string) - Environment from associated trace - - `traceTags` (arrayOptions) - Tags from associated trace - - ## Filter Examples - ```json - [ - { - "type": "string", - "column": "type", - "operator": "=", - "value": "GENERATION" - }, - { - "type": "number", - "column": "latency", - "operator": ">=", - "value": 2.5 - }, - { - "type": "stringObject", - "column": "metadata", - "key": "environment", - "operator": "=", - "value": "production" - } - ] - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsViews - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.legacy.observations_v1.get_many() - """ - _response = self._raw_client.get_many( - page=page, - limit=limit, - name=name, - user_id=user_id, - type=type, - trace_id=trace_id, - level=level, - parent_observation_id=parent_observation_id, - environment=environment, - from_start_time=from_start_time, - to_start_time=to_start_time, - version=version, - filter=filter, - request_options=request_options, - ) - return _response.data - - -class AsyncObservationsV1Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawObservationsV1Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawObservationsV1Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawObservationsV1Client - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get( - self, - observation_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsViewSingle: - """ - Get a observation - - Parameters - ---------- - observation_id : str - The unique langfuse identifier of an observation, can be an event, span or generation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsViewSingle - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.legacy.observations_v1.get( - observation_id="observationId", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get( - observation_id, request_options=request_options - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - name: typing.Optional[str] = None, - user_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - level: typing.Optional[ObservationLevel] = None, - parent_observation_id: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - from_start_time: typing.Optional[dt.datetime] = None, - to_start_time: typing.Optional[dt.datetime] = None, - version: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> ObservationsViews: - """ - Get a list of observations. - - Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - name : typing.Optional[str] - - user_id : typing.Optional[str] - - type : typing.Optional[str] - - trace_id : typing.Optional[str] - - level : typing.Optional[ObservationLevel] - Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). - - parent_observation_id : typing.Optional[str] - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for observations where the environment is one of the provided values. - - from_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time on or after this datetime (ISO 8601). - - to_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time before this datetime (ISO 8601). - - version : typing.Optional[str] - Optional filter to only include observations with a certain version. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata - } - ] - ``` - - ## Available Columns - - ### Core Observation Fields - - `id` (string) - Observation ID - - `type` (string) - Observation type (SPAN, GENERATION, EVENT) - - `name` (string) - Observation name - - `traceId` (string) - Associated trace ID - - `startTime` (datetime) - Observation start time - - `endTime` (datetime) - Observation end time - - `environment` (string) - Environment tag - - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) - - `statusMessage` (string) - Status message - - `version` (string) - Version tag - - ### Performance Metrics - - `latency` (number) - Latency in seconds (calculated: end_time - start_time) - - `timeToFirstToken` (number) - Time to first token in seconds - - `tokensPerSecond` (number) - Output tokens per second - - ### Token Usage - - `inputTokens` (number) - Number of input tokens - - `outputTokens` (number) - Number of output tokens - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - ### Cost Metrics - - `inputCost` (number) - Input cost in USD - - `outputCost` (number) - Output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Model Information - - `model` (string) - Provided model name - - `promptName` (string) - Associated prompt name - - `promptVersion` (number) - Associated prompt version - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Associated Trace Fields (requires join with traces table) - - `userId` (string) - User ID from associated trace - - `traceName` (string) - Name from associated trace - - `traceEnvironment` (string) - Environment from associated trace - - `traceTags` (arrayOptions) - Tags from associated trace - - ## Filter Examples - ```json - [ - { - "type": "string", - "column": "type", - "operator": "=", - "value": "GENERATION" - }, - { - "type": "number", - "column": "latency", - "operator": ">=", - "value": 2.5 - }, - { - "type": "stringObject", - "column": "metadata", - "key": "environment", - "operator": "=", - "value": "production" - } - ] - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ObservationsViews - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.legacy.observations_v1.get_many() - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_many( - page=page, - limit=limit, - name=name, - user_id=user_id, - type=type, - trace_id=trace_id, - level=level, - parent_observation_id=parent_observation_id, - environment=environment, - from_start_time=from_start_time, - to_start_time=to_start_time, - version=version, - filter=filter, - request_options=request_options, - ) - return _response.data diff --git a/langfuse/api/legacy/observations_v1/raw_client.py b/langfuse/api/legacy/observations_v1/raw_client.py deleted file mode 100644 index ee1792052..000000000 --- a/langfuse/api/legacy/observations_v1/raw_client.py +++ /dev/null @@ -1,776 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -import typing_extensions -from json.decoder import JSONDecodeError - -from ...commons.errors.access_denied_error import AccessDeniedError -from ...commons.errors.error import Error -from ...commons.errors.method_not_allowed_error import MethodNotAllowedError -from ...commons.errors.not_found_error import NotFoundError -from ...commons.errors.unauthorized_error import UnauthorizedError -from ...commons.types.observation_level import ObservationLevel -from ...commons.types.observations_view_single import ObservationsViewSingle -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.datetime_utils import serialize_datetime -from ...core.http_response import AsyncHttpResponse, HttpResponse -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import parse_obj_as -from ...core.request_options import RequestOptions -from .types.observations_views import ObservationsViews - - -class RawObservationsV1Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get( - self, - observation_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ObservationsViewSingle]: - """ - Get a observation - - Parameters - ---------- - observation_id : str - The unique langfuse identifier of an observation, can be an event, span or generation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ObservationsViewSingle] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/observations/{jsonable_encoder(observation_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ObservationsViewSingle, - parse_obj_as( - type_=ObservationsViewSingle, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - name: typing.Optional[str] = None, - user_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - level: typing.Optional[ObservationLevel] = None, - parent_observation_id: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - from_start_time: typing.Optional[dt.datetime] = None, - to_start_time: typing.Optional[dt.datetime] = None, - version: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ObservationsViews]: - """ - Get a list of observations. - - Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - name : typing.Optional[str] - - user_id : typing.Optional[str] - - type : typing.Optional[str] - - trace_id : typing.Optional[str] - - level : typing.Optional[ObservationLevel] - Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). - - parent_observation_id : typing.Optional[str] - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for observations where the environment is one of the provided values. - - from_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time on or after this datetime (ISO 8601). - - to_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time before this datetime (ISO 8601). - - version : typing.Optional[str] - Optional filter to only include observations with a certain version. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata - } - ] - ``` - - ## Available Columns - - ### Core Observation Fields - - `id` (string) - Observation ID - - `type` (string) - Observation type (SPAN, GENERATION, EVENT) - - `name` (string) - Observation name - - `traceId` (string) - Associated trace ID - - `startTime` (datetime) - Observation start time - - `endTime` (datetime) - Observation end time - - `environment` (string) - Environment tag - - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) - - `statusMessage` (string) - Status message - - `version` (string) - Version tag - - ### Performance Metrics - - `latency` (number) - Latency in seconds (calculated: end_time - start_time) - - `timeToFirstToken` (number) - Time to first token in seconds - - `tokensPerSecond` (number) - Output tokens per second - - ### Token Usage - - `inputTokens` (number) - Number of input tokens - - `outputTokens` (number) - Number of output tokens - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - ### Cost Metrics - - `inputCost` (number) - Input cost in USD - - `outputCost` (number) - Output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Model Information - - `model` (string) - Provided model name - - `promptName` (string) - Associated prompt name - - `promptVersion` (number) - Associated prompt version - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Associated Trace Fields (requires join with traces table) - - `userId` (string) - User ID from associated trace - - `traceName` (string) - Name from associated trace - - `traceEnvironment` (string) - Environment from associated trace - - `traceTags` (arrayOptions) - Tags from associated trace - - ## Filter Examples - ```json - [ - { - "type": "string", - "column": "type", - "operator": "=", - "value": "GENERATION" - }, - { - "type": "number", - "column": "latency", - "operator": ">=", - "value": 2.5 - }, - { - "type": "stringObject", - "column": "metadata", - "key": "environment", - "operator": "=", - "value": "production" - } - ] - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ObservationsViews] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/observations", - method="GET", - params={ - "page": page, - "limit": limit, - "name": name, - "userId": user_id, - "type": type, - "traceId": trace_id, - "level": level, - "parentObservationId": parent_observation_id, - "environment": environment, - "fromStartTime": serialize_datetime(from_start_time) - if from_start_time is not None - else None, - "toStartTime": serialize_datetime(to_start_time) - if to_start_time is not None - else None, - "version": version, - "filter": filter, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ObservationsViews, - parse_obj_as( - type_=ObservationsViews, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawObservationsV1Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get( - self, - observation_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ObservationsViewSingle]: - """ - Get a observation - - Parameters - ---------- - observation_id : str - The unique langfuse identifier of an observation, can be an event, span or generation - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ObservationsViewSingle] - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/observations/{jsonable_encoder(observation_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ObservationsViewSingle, - parse_obj_as( - type_=ObservationsViewSingle, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v2/observations?fromStartTime=&toStartTime=` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - name: typing.Optional[str] = None, - user_id: typing.Optional[str] = None, - type: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - level: typing.Optional[ObservationLevel] = None, - parent_observation_id: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - from_start_time: typing.Optional[dt.datetime] = None, - to_start_time: typing.Optional[dt.datetime] = None, - version: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ObservationsViews]: - """ - Get a list of observations. - - Consider using the [v2 observations endpoint](/api-reference#tag/observationsv2/GET/api/public/v2/observations) for cursor-based pagination and field selection. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - name : typing.Optional[str] - - user_id : typing.Optional[str] - - type : typing.Optional[str] - - trace_id : typing.Optional[str] - - level : typing.Optional[ObservationLevel] - Optional filter for observations with a specific level (e.g. "DEBUG", "DEFAULT", "WARNING", "ERROR"). - - parent_observation_id : typing.Optional[str] - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for observations where the environment is one of the provided values. - - from_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time on or after this datetime (ISO 8601). - - to_start_time : typing.Optional[dt.datetime] - Retrieve only observations with a start_time before this datetime (ISO 8601). - - version : typing.Optional[str] - Optional filter to only include observations with a certain version. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, type, level, environment, fromStartTime, ...). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, and categoryOptions types when filtering on nested fields like metadata - } - ] - ``` - - ## Available Columns - - ### Core Observation Fields - - `id` (string) - Observation ID - - `type` (string) - Observation type (SPAN, GENERATION, EVENT) - - `name` (string) - Observation name - - `traceId` (string) - Associated trace ID - - `startTime` (datetime) - Observation start time - - `endTime` (datetime) - Observation end time - - `environment` (string) - Environment tag - - `level` (string) - Log level (DEBUG, DEFAULT, WARNING, ERROR) - - `statusMessage` (string) - Status message - - `version` (string) - Version tag - - ### Performance Metrics - - `latency` (number) - Latency in seconds (calculated: end_time - start_time) - - `timeToFirstToken` (number) - Time to first token in seconds - - `tokensPerSecond` (number) - Output tokens per second - - ### Token Usage - - `inputTokens` (number) - Number of input tokens - - `outputTokens` (number) - Number of output tokens - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - ### Cost Metrics - - `inputCost` (number) - Input cost in USD - - `outputCost` (number) - Output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Model Information - - `model` (string) - Provided model name - - `promptName` (string) - Associated prompt name - - `promptVersion` (number) - Associated prompt version - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Associated Trace Fields (requires join with traces table) - - `userId` (string) - User ID from associated trace - - `traceName` (string) - Name from associated trace - - `traceEnvironment` (string) - Environment from associated trace - - `traceTags` (arrayOptions) - Tags from associated trace - - ## Filter Examples - ```json - [ - { - "type": "string", - "column": "type", - "operator": "=", - "value": "GENERATION" - }, - { - "type": "number", - "column": "latency", - "operator": ">=", - "value": 2.5 - }, - { - "type": "stringObject", - "column": "metadata", - "key": "environment", - "operator": "=", - "value": "production" - } - ] - ``` - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ObservationsViews] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/observations", - method="GET", - params={ - "page": page, - "limit": limit, - "name": name, - "userId": user_id, - "type": type, - "traceId": trace_id, - "level": level, - "parentObservationId": parent_observation_id, - "environment": environment, - "fromStartTime": serialize_datetime(from_start_time) - if from_start_time is not None - else None, - "toStartTime": serialize_datetime(to_start_time) - if to_start_time is not None - else None, - "version": version, - "filter": filter, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ObservationsViews, - parse_obj_as( - type_=ObservationsViews, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/legacy/observations_v1/types/__init__.py b/langfuse/api/legacy/observations_v1/types/__init__.py deleted file mode 100644 index 247b674a1..000000000 --- a/langfuse/api/legacy/observations_v1/types/__init__.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .observations import Observations - from .observations_views import ObservationsViews -_dynamic_imports: typing.Dict[str, str] = { - "Observations": ".observations", - "ObservationsViews": ".observations_views", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["Observations", "ObservationsViews"] diff --git a/langfuse/api/legacy/observations_v1/types/observations_views.py b/langfuse/api/legacy/observations_v1/types/observations_views.py deleted file mode 100644 index bb1da2541..000000000 --- a/langfuse/api/legacy/observations_v1/types/observations_views.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ....commons.types.deprecation import Deprecation -from ....commons.types.observations_view import ObservationsView -from ....core.pydantic_utilities import UniversalBaseModel -from ....core.serialization import FieldMetadata -from ....utils.pagination.types.meta_response import MetaResponse - - -class ObservationsViews(UniversalBaseModel): - data: typing.List[ObservationsView] - meta: MetaResponse - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/legacy/raw_client.py b/langfuse/api/legacy/raw_client.py deleted file mode 100644 index 0672ed01d..000000000 --- a/langfuse/api/legacy/raw_client.py +++ /dev/null @@ -1,13 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper - - -class RawLegacyClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - -class AsyncRawLegacyClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper diff --git a/langfuse/api/legacy/score_v1/__init__.py b/langfuse/api/legacy/score_v1/__init__.py deleted file mode 100644 index 8fdfbc4a0..000000000 --- a/langfuse/api/legacy/score_v1/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -# Score-create compatibility aliases (LFE-10397). -from .types import CreateScoreRequest -from .types import CreateScoreResponse -from .types import CreateScoreSource - -__all__ = ["CreateScoreRequest", "CreateScoreResponse", "CreateScoreSource"] diff --git a/langfuse/api/legacy/score_v1/client.py b/langfuse/api/legacy/score_v1/client.py deleted file mode 100644 index 66176746b..000000000 --- a/langfuse/api/legacy/score_v1/client.py +++ /dev/null @@ -1,212 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...commons.types.create_score_value import CreateScoreValue -from ...commons.types.score_data_type import ScoreDataType -from ...scores.client import ( - AsyncScoresClient as CanonicalAsyncScoresClient, - ScoresClient as CanonicalScoresClient, - OMIT, -) -from ...scores.types.create_score_response import CreateScoreResponse -from ...scores.types.create_score_source import CreateScoreSource -from ...core.request_options import RequestOptions -from .raw_client import AsyncRawScoreV1Client, RawScoreV1Client - - -class ScoreV1Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawScoreV1Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawScoreV1Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawScoreV1Client - """ - return self._raw_client - - def create( - self, - *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: - """**Deprecated compatibility alias.** Use ``client.scores.create``.""" - return CanonicalScoresClient( - client_wrapper=self._raw_client._client_wrapper - ).create( - name=name, - value=value, - id=id, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - dataset_run_id=dataset_run_id, - comment=comment, - metadata=metadata, - environment=environment, - queue_id=queue_id, - data_type=data_type, - config_id=config_id, - source=source, - request_options=request_options, - ) - - def delete( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Delete a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.legacy.score_v1.delete( - score_id="scoreId", - ) - """ - _response = self._raw_client.delete(score_id, request_options=request_options) - return _response.data - - -class AsyncScoreV1Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawScoreV1Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawScoreV1Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawScoreV1Client - """ - return self._raw_client - - async def create( - self, - *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: - """**Deprecated compatibility alias.** Use ``client.scores.create``.""" - return await CanonicalAsyncScoresClient( - client_wrapper=self._raw_client._client_wrapper - ).create( - name=name, - value=value, - id=id, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - dataset_run_id=dataset_run_id, - comment=comment, - metadata=metadata, - environment=environment, - queue_id=queue_id, - data_type=data_type, - config_id=config_id, - source=source, - request_options=request_options, - ) - - async def delete( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> None: - """ - Delete a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.legacy.score_v1.delete( - score_id="scoreId", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.delete( - score_id, request_options=request_options - ) - return _response.data diff --git a/langfuse/api/legacy/score_v1/raw_client.py b/langfuse/api/legacy/score_v1/raw_client.py deleted file mode 100644 index 6abeb5245..000000000 --- a/langfuse/api/legacy/score_v1/raw_client.py +++ /dev/null @@ -1,214 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ...commons.errors.access_denied_error import AccessDeniedError -from ...commons.errors.error import Error -from ...commons.errors.method_not_allowed_error import MethodNotAllowedError -from ...commons.errors.not_found_error import NotFoundError -from ...commons.errors.unauthorized_error import UnauthorizedError -from ...core.api_error import ApiError -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ...core.http_response import AsyncHttpResponse, HttpResponse -from ...core.jsonable_encoder import jsonable_encoder -from ...core.pydantic_utilities import parse_obj_as -from ...core.request_options import RequestOptions - - -class RawScoreV1Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def delete( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[None]: - """ - Delete a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[None] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/scores/{jsonable_encoder(score_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return HttpResponse(response=_response, data=None) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawScoreV1Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def delete( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[None]: - """ - Delete a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[None] - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/scores/{jsonable_encoder(score_id)}", - method="DELETE", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - return AsyncHttpResponse(response=_response, data=None) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/legacy/score_v1/types/__init__.py b/langfuse/api/legacy/score_v1/types/__init__.py deleted file mode 100644 index 75b37a8ed..000000000 --- a/langfuse/api/legacy/score_v1/types/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# This package is generated by Fern and extended for compatibility. - -__all__: list[str] = [] - -# Score-create compatibility aliases (LFE-10397). -from .create_score_request import CreateScoreRequest -from .create_score_response import CreateScoreResponse -from .create_score_source import CreateScoreSource - -__all__ = [*__all__, "CreateScoreRequest", "CreateScoreResponse", "CreateScoreSource"] diff --git a/langfuse/api/legacy/score_v1/types/create_score_request.py b/langfuse/api/legacy/score_v1/types/create_score_request.py deleted file mode 100644 index 1f1007fc6..000000000 --- a/langfuse/api/legacy/score_v1/types/create_score_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This compatibility alias is applied after Fern generation. - -from ....scores.types.create_score_request import CreateScoreRequest - -__all__ = ["CreateScoreRequest"] diff --git a/langfuse/api/legacy/score_v1/types/create_score_response.py b/langfuse/api/legacy/score_v1/types/create_score_response.py deleted file mode 100644 index 876c50f0b..000000000 --- a/langfuse/api/legacy/score_v1/types/create_score_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This compatibility alias is applied after Fern generation. - -from ....scores.types.create_score_response import CreateScoreResponse - -__all__ = ["CreateScoreResponse"] diff --git a/langfuse/api/legacy/score_v1/types/create_score_source.py b/langfuse/api/legacy/score_v1/types/create_score_source.py deleted file mode 100644 index 341481d77..000000000 --- a/langfuse/api/legacy/score_v1/types/create_score_source.py +++ /dev/null @@ -1,5 +0,0 @@ -# This compatibility alias is applied after Fern generation. - -from ....scores.types.create_score_source import CreateScoreSource - -__all__ = ["CreateScoreSource"] diff --git a/langfuse/api/metrics/client.py b/langfuse/api/metrics/client.py index f2272409b..707112cc2 100644 --- a/langfuse/api/metrics/client.py +++ b/langfuse/api/metrics/client.py @@ -29,6 +29,8 @@ def metrics( """ Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + Together with OpenTelemetry ingestion and Observations API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## V2 Differences - Supports `observations`, `scores-numeric`, `scores-boolean`, and `scores-categorical` views only (traces view not supported) - Direct access to tags and release fields on observations @@ -255,6 +257,8 @@ async def metrics( """ Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + Together with OpenTelemetry ingestion and Observations API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## V2 Differences - Supports `observations`, `scores-numeric`, `scores-boolean`, and `scores-categorical` views only (traces view not supported) - Direct access to tags and release fields on observations diff --git a/langfuse/api/metrics/raw_client.py b/langfuse/api/metrics/raw_client.py index ce52e03c2..dbb81c772 100644 --- a/langfuse/api/metrics/raw_client.py +++ b/langfuse/api/metrics/raw_client.py @@ -26,6 +26,8 @@ def metrics( """ Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + Together with OpenTelemetry ingestion and Observations API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## V2 Differences - Supports `observations`, `scores-numeric`, `scores-boolean`, and `scores-categorical` views only (traces view not supported) - Direct access to tags and release fields on observations @@ -306,6 +308,8 @@ async def metrics( """ Get metrics from the Langfuse project using a query object. V2 endpoint with optimized performance. + Together with OpenTelemetry ingestion and Observations API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## V2 Differences - Supports `observations`, `scores-numeric`, `scores-boolean`, and `scores-categorical` views only (traces view not supported) - Direct access to tags and release fields on observations diff --git a/langfuse/api/observations/client.py b/langfuse/api/observations/client.py index a44bd0707..5e717adb6 100644 --- a/langfuse/api/observations/client.py +++ b/langfuse/api/observations/client.py @@ -51,6 +51,8 @@ def get_many( """ Get a list of observations with cursor-based pagination and flexible field selection. + Together with OpenTelemetry ingestion and Metrics API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## Cursor-based Pagination This endpoint uses cursor-based pagination for efficient traversal of large datasets. The cursor is returned in the response metadata and should be passed in subsequent requests @@ -335,6 +337,8 @@ async def get_many( """ Get a list of observations with cursor-based pagination and flexible field selection. + Together with OpenTelemetry ingestion and Metrics API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## Cursor-based Pagination This endpoint uses cursor-based pagination for efficient traversal of large datasets. The cursor is returned in the response metadata and should be passed in subsequent requests diff --git a/langfuse/api/observations/raw_client.py b/langfuse/api/observations/raw_client.py index 24a78cc02..84dba4891 100644 --- a/langfuse/api/observations/raw_client.py +++ b/langfuse/api/observations/raw_client.py @@ -49,6 +49,8 @@ def get_many( """ Get a list of observations with cursor-based pagination and flexible field selection. + Together with OpenTelemetry ingestion and Metrics API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## Cursor-based Pagination This endpoint uses cursor-based pagination for efficient traversal of large datasets. The cursor is returned in the response metadata and should be passed in subsequent requests @@ -392,6 +394,8 @@ async def get_many( """ Get a list of observations with cursor-based pagination and flexible field selection. + Together with OpenTelemetry ingestion and Metrics API v2, this is the only real-time read path. Other public API endpoints can delay data by about 10 minutes. + ## Cursor-based Pagination This endpoint uses cursor-based pagination for efficient traversal of large datasets. The cursor is returned in the response metadata and should be passed in subsequent requests diff --git a/langfuse/api/opentelemetry/client.py b/langfuse/api/opentelemetry/client.py index 13177e5e6..31af9bc42 100644 --- a/langfuse/api/opentelemetry/client.py +++ b/langfuse/api/opentelemetry/client.py @@ -38,6 +38,8 @@ def export_traces( This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + Together with Observations API v2 and Metrics API v2, this is the only real-time write path. Other public API endpoints can delay data by about 10 minutes. Direct exporters must send `x-langfuse-ingestion-version: 4`; current Python and JS SDKs already do. + **Supported Formats:** - Binary Protobuf: `Content-Type: application/x-protobuf` - JSON Protobuf: `Content-Type: application/json` @@ -166,6 +168,8 @@ async def export_traces( This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + Together with Observations API v2 and Metrics API v2, this is the only real-time write path. Other public API endpoints can delay data by about 10 minutes. Direct exporters must send `x-langfuse-ingestion-version: 4`; current Python and JS SDKs already do. + **Supported Formats:** - Binary Protobuf: `Content-Type: application/x-protobuf` - JSON Protobuf: `Content-Type: application/json` diff --git a/langfuse/api/opentelemetry/raw_client.py b/langfuse/api/opentelemetry/raw_client.py index 6b68f909b..14c74c589 100644 --- a/langfuse/api/opentelemetry/raw_client.py +++ b/langfuse/api/opentelemetry/raw_client.py @@ -36,6 +36,8 @@ def export_traces( This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + Together with Observations API v2 and Metrics API v2, this is the only real-time write path. Other public API endpoints can delay data by about 10 minutes. Direct exporters must send `x-langfuse-ingestion-version: 4`; current Python and JS SDKs already do. + **Supported Formats:** - Binary Protobuf: `Content-Type: application/x-protobuf` - JSON Protobuf: `Content-Type: application/json` @@ -171,6 +173,8 @@ async def export_traces( This endpoint implements the OTLP/HTTP specification for trace ingestion, providing native OpenTelemetry integration for Langfuse Observability. + Together with Observations API v2 and Metrics API v2, this is the only real-time write path. Other public API endpoints can delay data by about 10 minutes. Direct exporters must send `x-langfuse-ingestion-version: 4`; current Python and JS SDKs already do. + **Supported Formats:** - Binary Protobuf: `Content-Type: application/x-protobuf` - JSON Protobuf: `Content-Type: application/json` diff --git a/langfuse/api/organizations/types/organization_api_key.py b/langfuse/api/organizations/types/organization_api_key.py index 572015ab3..5454ac2d8 100644 --- a/langfuse/api/organizations/types/organization_api_key.py +++ b/langfuse/api/organizations/types/organization_api_key.py @@ -16,11 +16,24 @@ class OrganizationApiKey(UniversalBaseModel): ] expires_at: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="expiresAt") - ] = None + ] = pydantic.Field(default=None) + """ + Expiration timestamp. Null if the key does not expire. + """ + last_used_at: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="lastUsedAt") ] = None - note: typing.Optional[str] = None + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the API key. Contains the same value as note; null if no name was provided. + """ + + note: typing.Optional[str] = pydantic.Field(default=None) + """ + Deprecated alias for name. Contains the same value as name. + """ + public_key: typing_extensions.Annotated[str, FieldMetadata(alias="publicKey")] display_secret_key: typing_extensions.Annotated[ str, FieldMetadata(alias="displaySecretKey") diff --git a/langfuse/api/projects/client.py b/langfuse/api/projects/client.py index 52350f3b6..35d9683b1 100644 --- a/langfuse/api/projects/client.py +++ b/langfuse/api/projects/client.py @@ -1,5 +1,6 @@ # This file was auto-generated by Fern from our API Definition. +import datetime as dt import typing from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper @@ -260,7 +261,9 @@ def create_api_key( self, project_id: str, *, + name: typing.Optional[str] = OMIT, note: typing.Optional[str] = OMIT, + expires_at: typing.Optional[dt.datetime] = OMIT, public_key: typing.Optional[str] = OMIT, secret_key: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, @@ -272,8 +275,14 @@ def create_api_key( ---------- project_id : str + name : typing.Optional[str] + Optional name for the API key. Cannot be provided together with note, even if either value is an empty string. + note : typing.Optional[str] - Optional note for the API key + Deprecated alias for name. Cannot be provided together with name, even if either value is an empty string. + + expires_at : typing.Optional[dt.datetime] + Optional expiration timestamp in ISO 8601 format. Must be in the future. Omit or set to null for a key that does not expire. public_key : typing.Optional[str] Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. @@ -306,7 +315,9 @@ def create_api_key( """ _response = self._raw_client.create_api_key( project_id, + name=name, note=note, + expires_at=expires_at, public_key=public_key, secret_key=secret_key, request_options=request_options, @@ -645,7 +656,9 @@ async def create_api_key( self, project_id: str, *, + name: typing.Optional[str] = OMIT, note: typing.Optional[str] = OMIT, + expires_at: typing.Optional[dt.datetime] = OMIT, public_key: typing.Optional[str] = OMIT, secret_key: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, @@ -657,8 +670,14 @@ async def create_api_key( ---------- project_id : str + name : typing.Optional[str] + Optional name for the API key. Cannot be provided together with note, even if either value is an empty string. + note : typing.Optional[str] - Optional note for the API key + Deprecated alias for name. Cannot be provided together with name, even if either value is an empty string. + + expires_at : typing.Optional[dt.datetime] + Optional expiration timestamp in ISO 8601 format. Must be in the future. Omit or set to null for a key that does not expire. public_key : typing.Optional[str] Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. @@ -699,7 +718,9 @@ async def main() -> None: """ _response = await self._raw_client.create_api_key( project_id, + name=name, note=note, + expires_at=expires_at, public_key=public_key, secret_key=secret_key, request_options=request_options, diff --git a/langfuse/api/projects/raw_client.py b/langfuse/api/projects/raw_client.py index 524f37d3f..71457a2ac 100644 --- a/langfuse/api/projects/raw_client.py +++ b/langfuse/api/projects/raw_client.py @@ -1,5 +1,6 @@ # This file was auto-generated by Fern from our API Definition. +import datetime as dt import typing from json.decoder import JSONDecodeError @@ -577,7 +578,9 @@ def create_api_key( self, project_id: str, *, + name: typing.Optional[str] = OMIT, note: typing.Optional[str] = OMIT, + expires_at: typing.Optional[dt.datetime] = OMIT, public_key: typing.Optional[str] = OMIT, secret_key: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, @@ -589,8 +592,14 @@ def create_api_key( ---------- project_id : str + name : typing.Optional[str] + Optional name for the API key. Cannot be provided together with note, even if either value is an empty string. + note : typing.Optional[str] - Optional note for the API key + Deprecated alias for name. Cannot be provided together with name, even if either value is an empty string. + + expires_at : typing.Optional[dt.datetime] + Optional expiration timestamp in ISO 8601 format. Must be in the future. Omit or set to null for a key that does not expire. public_key : typing.Optional[str] Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. @@ -609,7 +618,9 @@ def create_api_key( f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", method="POST", json={ + "name": name, "note": note, + "expiresAt": expires_at, "publicKey": public_key, "secretKey": secret_key, }, @@ -1353,7 +1364,9 @@ async def create_api_key( self, project_id: str, *, + name: typing.Optional[str] = OMIT, note: typing.Optional[str] = OMIT, + expires_at: typing.Optional[dt.datetime] = OMIT, public_key: typing.Optional[str] = OMIT, secret_key: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None, @@ -1365,8 +1378,14 @@ async def create_api_key( ---------- project_id : str + name : typing.Optional[str] + Optional name for the API key. Cannot be provided together with note, even if either value is an empty string. + note : typing.Optional[str] - Optional note for the API key + Deprecated alias for name. Cannot be provided together with name, even if either value is an empty string. + + expires_at : typing.Optional[dt.datetime] + Optional expiration timestamp in ISO 8601 format. Must be in the future. Omit or set to null for a key that does not expire. public_key : typing.Optional[str] Optional predefined public key. Must start with 'pk-lf-'. If provided, secretKey must also be provided. @@ -1385,7 +1404,9 @@ async def create_api_key( f"api/public/projects/{jsonable_encoder(project_id)}/apiKeys", method="POST", json={ + "name": name, "note": note, + "expiresAt": expires_at, "publicKey": public_key, "secretKey": secret_key, }, diff --git a/langfuse/api/projects/types/api_key_response.py b/langfuse/api/projects/types/api_key_response.py index 06ad54610..e3a67cab4 100644 --- a/langfuse/api/projects/types/api_key_response.py +++ b/langfuse/api/projects/types/api_key_response.py @@ -18,12 +18,27 @@ class ApiKeyResponse(UniversalBaseModel): created_at: typing_extensions.Annotated[ dt.datetime, FieldMetadata(alias="createdAt") ] + expires_at: typing_extensions.Annotated[ + typing.Optional[dt.datetime], FieldMetadata(alias="expiresAt") + ] = pydantic.Field(default=None) + """ + Expiration timestamp. Null if the key does not expire. + """ + public_key: typing_extensions.Annotated[str, FieldMetadata(alias="publicKey")] secret_key: typing_extensions.Annotated[str, FieldMetadata(alias="secretKey")] display_secret_key: typing_extensions.Annotated[ str, FieldMetadata(alias="displaySecretKey") ] - note: typing.Optional[str] = None + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the API key. Contains the same value as note; null if no name was provided. + """ + + note: typing.Optional[str] = pydantic.Field(default=None) + """ + Deprecated alias for name. Contains the same value as name. + """ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/projects/types/api_key_summary.py b/langfuse/api/projects/types/api_key_summary.py index 68d21421b..d64cd1045 100644 --- a/langfuse/api/projects/types/api_key_summary.py +++ b/langfuse/api/projects/types/api_key_summary.py @@ -20,11 +20,24 @@ class ApiKeySummary(UniversalBaseModel): ] expires_at: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="expiresAt") - ] = None + ] = pydantic.Field(default=None) + """ + Expiration timestamp. Null if the key does not expire. + """ + last_used_at: typing_extensions.Annotated[ typing.Optional[dt.datetime], FieldMetadata(alias="lastUsedAt") ] = None - note: typing.Optional[str] = None + name: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the API key. Contains the same value as note; null if no name was provided. + """ + + note: typing.Optional[str] = pydantic.Field(default=None) + """ + Deprecated alias for name. Contains the same value as name. + """ + public_key: typing_extensions.Annotated[str, FieldMetadata(alias="publicKey")] display_secret_key: typing_extensions.Annotated[ str, FieldMetadata(alias="displaySecretKey") diff --git a/langfuse/api/prompts/client.py b/langfuse/api/prompts/client.py index eff5e572f..b4a978c44 100644 --- a/langfuse/api/prompts/client.py +++ b/langfuse/api/prompts/client.py @@ -98,6 +98,7 @@ def list( limit: typing.Optional[int] = None, from_updated_at: typing.Optional[dt.datetime] = None, to_updated_at: typing.Optional[dt.datetime] = None, + filter: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> PromptMetaListResponse: """ @@ -123,6 +124,32 @@ def list( to_updated_at : typing.Optional[dt.datetime] Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + filter : typing.Optional[str] + JSON string containing an array of filter conditions, combined with AND. + When provided, this takes precedence over name, version, + label, tag, fromUpdatedAt, and toUpdatedAt. An empty array applies no + filters. Project scoping and pagination always apply. + + Each condition requires `type`, `column`, `operator`, and `value`. + Supported types and columns: + - `string`: `id`, `name`, `type`. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is not empty`. + - `stringOptions`: `id`, `name`, `type`. Operators: `any of`, `none of`. Value: non-empty string array. + - `number`: `version`. Operators: `=`, `>`, `<`, `>=`, `<=`. Value: number. + - `datetime`: `createdAt`, `updatedAt`. Operators: `>`, `<`, `>=`, `<=`. Value: ISO 8601 timestamp. + - `arrayOptions`: `labels`, `tags`. Operators: `any of`, `all of`, `none of`. Value: non-empty string array. + - `stringObject`: `config`. Requires an additional `key` field. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is set`, `is not set`. Value: string (use an empty string for key-presence operators). + + String equality is case-sensitive. String search operators use + case-insensitive SQL pattern matching: `%` matches any sequence and + `_` matches a single character. A backslash escapes these characters; + JSON-escape the backslash in the filter value. Asterisks are literal. + For config, an empty search value is treated as `is set`. + + Filters apply to prompt versions before aggregation: returned versions, + labels, and lastConfig reflect only matching versions. + + Example: [{"type":"string","column":"name","operator":"starts with","value":"tools/"},{"type":"string","column":"name","operator":"ends with","value":"/description"}] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -152,6 +179,7 @@ def list( limit=limit, from_updated_at=from_updated_at, to_updated_at=to_updated_at, + filter=filter, request_options=request_options, ) return _response.data @@ -358,6 +386,7 @@ async def list( limit: typing.Optional[int] = None, from_updated_at: typing.Optional[dt.datetime] = None, to_updated_at: typing.Optional[dt.datetime] = None, + filter: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> PromptMetaListResponse: """ @@ -383,6 +412,32 @@ async def list( to_updated_at : typing.Optional[dt.datetime] Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + filter : typing.Optional[str] + JSON string containing an array of filter conditions, combined with AND. + When provided, this takes precedence over name, version, + label, tag, fromUpdatedAt, and toUpdatedAt. An empty array applies no + filters. Project scoping and pagination always apply. + + Each condition requires `type`, `column`, `operator`, and `value`. + Supported types and columns: + - `string`: `id`, `name`, `type`. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is not empty`. + - `stringOptions`: `id`, `name`, `type`. Operators: `any of`, `none of`. Value: non-empty string array. + - `number`: `version`. Operators: `=`, `>`, `<`, `>=`, `<=`. Value: number. + - `datetime`: `createdAt`, `updatedAt`. Operators: `>`, `<`, `>=`, `<=`. Value: ISO 8601 timestamp. + - `arrayOptions`: `labels`, `tags`. Operators: `any of`, `all of`, `none of`. Value: non-empty string array. + - `stringObject`: `config`. Requires an additional `key` field. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is set`, `is not set`. Value: string (use an empty string for key-presence operators). + + String equality is case-sensitive. String search operators use + case-insensitive SQL pattern matching: `%` matches any sequence and + `_` matches a single character. A backslash escapes these characters; + JSON-escape the backslash in the filter value. Asterisks are literal. + For config, an empty search value is treated as `is set`. + + Filters apply to prompt versions before aggregation: returned versions, + labels, and lastConfig reflect only matching versions. + + Example: [{"type":"string","column":"name","operator":"starts with","value":"tools/"},{"type":"string","column":"name","operator":"ends with","value":"/description"}] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -420,6 +475,7 @@ async def main() -> None: limit=limit, from_updated_at=from_updated_at, to_updated_at=to_updated_at, + filter=filter, request_options=request_options, ) return _response.data diff --git a/langfuse/api/prompts/raw_client.py b/langfuse/api/prompts/raw_client.py index a2d81a9d1..c4816cbe0 100644 --- a/langfuse/api/prompts/raw_client.py +++ b/langfuse/api/prompts/raw_client.py @@ -161,6 +161,7 @@ def list( limit: typing.Optional[int] = None, from_updated_at: typing.Optional[dt.datetime] = None, to_updated_at: typing.Optional[dt.datetime] = None, + filter: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> HttpResponse[PromptMetaListResponse]: """ @@ -186,6 +187,32 @@ def list( to_updated_at : typing.Optional[dt.datetime] Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + filter : typing.Optional[str] + JSON string containing an array of filter conditions, combined with AND. + When provided, this takes precedence over name, version, + label, tag, fromUpdatedAt, and toUpdatedAt. An empty array applies no + filters. Project scoping and pagination always apply. + + Each condition requires `type`, `column`, `operator`, and `value`. + Supported types and columns: + - `string`: `id`, `name`, `type`. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is not empty`. + - `stringOptions`: `id`, `name`, `type`. Operators: `any of`, `none of`. Value: non-empty string array. + - `number`: `version`. Operators: `=`, `>`, `<`, `>=`, `<=`. Value: number. + - `datetime`: `createdAt`, `updatedAt`. Operators: `>`, `<`, `>=`, `<=`. Value: ISO 8601 timestamp. + - `arrayOptions`: `labels`, `tags`. Operators: `any of`, `all of`, `none of`. Value: non-empty string array. + - `stringObject`: `config`. Requires an additional `key` field. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is set`, `is not set`. Value: string (use an empty string for key-presence operators). + + String equality is case-sensitive. String search operators use + case-insensitive SQL pattern matching: `%` matches any sequence and + `_` matches a single character. A backslash escapes these characters; + JSON-escape the backslash in the filter value. Asterisks are literal. + For config, an empty search value is treated as `is set`. + + Filters apply to prompt versions before aggregation: returned versions, + labels, and lastConfig reflect only matching versions. + + Example: [{"type":"string","column":"name","operator":"starts with","value":"tools/"},{"type":"string","column":"name","operator":"ends with","value":"/description"}] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -208,6 +235,7 @@ def list( "toUpdatedAt": serialize_datetime(to_updated_at) if to_updated_at is not None else None, + "filter": filter, }, request_options=request_options, ) @@ -642,6 +670,7 @@ async def list( limit: typing.Optional[int] = None, from_updated_at: typing.Optional[dt.datetime] = None, to_updated_at: typing.Optional[dt.datetime] = None, + filter: typing.Optional[str] = None, request_options: typing.Optional[RequestOptions] = None, ) -> AsyncHttpResponse[PromptMetaListResponse]: """ @@ -667,6 +696,32 @@ async def list( to_updated_at : typing.Optional[dt.datetime] Optional filter to only include prompt versions created/updated before a certain datetime (ISO 8601) + filter : typing.Optional[str] + JSON string containing an array of filter conditions, combined with AND. + When provided, this takes precedence over name, version, + label, tag, fromUpdatedAt, and toUpdatedAt. An empty array applies no + filters. Project scoping and pagination always apply. + + Each condition requires `type`, `column`, `operator`, and `value`. + Supported types and columns: + - `string`: `id`, `name`, `type`. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is not empty`. + - `stringOptions`: `id`, `name`, `type`. Operators: `any of`, `none of`. Value: non-empty string array. + - `number`: `version`. Operators: `=`, `>`, `<`, `>=`, `<=`. Value: number. + - `datetime`: `createdAt`, `updatedAt`. Operators: `>`, `<`, `>=`, `<=`. Value: ISO 8601 timestamp. + - `arrayOptions`: `labels`, `tags`. Operators: `any of`, `all of`, `none of`. Value: non-empty string array. + - `stringObject`: `config`. Requires an additional `key` field. Operators: `=`, `contains`, `does not contain`, `starts with`, `ends with`, `is set`, `is not set`. Value: string (use an empty string for key-presence operators). + + String equality is case-sensitive. String search operators use + case-insensitive SQL pattern matching: `%` matches any sequence and + `_` matches a single character. A backslash escapes these characters; + JSON-escape the backslash in the filter value. Asterisks are literal. + For config, an empty search value is treated as `is set`. + + Filters apply to prompt versions before aggregation: returned versions, + labels, and lastConfig reflect only matching versions. + + Example: [{"type":"string","column":"name","operator":"starts with","value":"tools/"},{"type":"string","column":"name","operator":"ends with","value":"/description"}] + request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -689,6 +744,7 @@ async def list( "toUpdatedAt": serialize_datetime(to_updated_at) if to_updated_at is not None else None, + "filter": filter, }, request_options=request_options, ) diff --git a/langfuse/api/prompts/types/create_chat_prompt_request.py b/langfuse/api/prompts/types/create_chat_prompt_request.py index 0fe1de0c1..36712f52e 100644 --- a/langfuse/api/prompts/types/create_chat_prompt_request.py +++ b/langfuse/api/prompts/types/create_chat_prompt_request.py @@ -11,7 +11,11 @@ class CreateChatPromptRequest(UniversalBaseModel): - name: str + name: str = pydantic.Field() + """ + The full prompt name, including any folder path, with at most 255 characters. + """ + prompt: typing.List[ChatMessageWithPlaceholders] config: typing.Optional[typing.Any] = None type: CreateChatPromptType diff --git a/langfuse/api/prompts/types/create_text_prompt_request.py b/langfuse/api/prompts/types/create_text_prompt_request.py index be87a7dde..8728da486 100644 --- a/langfuse/api/prompts/types/create_text_prompt_request.py +++ b/langfuse/api/prompts/types/create_text_prompt_request.py @@ -10,7 +10,11 @@ class CreateTextPromptRequest(UniversalBaseModel): - name: str + name: str = pydantic.Field() + """ + The full prompt name, including any folder path, with at most 255 characters. + """ + prompt: str config: typing.Optional[typing.Any] = None type: typing.Optional[CreateTextPromptType] = None diff --git a/langfuse/api/scores/__init__.py b/langfuse/api/scores/__init__.py index 9b006eddd..0fe558ea3 100644 --- a/langfuse/api/scores/__init__.py +++ b/langfuse/api/scores/__init__.py @@ -7,40 +7,70 @@ if typing.TYPE_CHECKING: from .types import ( + BaseScoreV3, + BooleanScoreV3, + CategoricalScoreV3, + CorrectionScoreV3, + CreateScoreBatchError, + CreateScoreBatchResponse, + CreateScoreBatchResults, CreateScoreRequest, - CreateScoreResponse, CreateScoreSource, - GetScoresResponse, - GetScoresResponseData, - GetScoresResponseDataBoolean, - GetScoresResponseDataCategorical, - GetScoresResponseDataCorrection, - GetScoresResponseDataNumeric, - GetScoresResponseDataText, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Correction, - GetScoresResponseData_Numeric, - GetScoresResponseData_Text, - GetScoresResponseTraceData, + CreateScoresRequest, + CreateScoresResponse, + GetScoresV3Meta, + GetScoresV3Response, + NumericScoreV3, + ScoreBody, + ScoreSubjectExperimentV3, + ScoreSubjectObservationV3, + ScoreSubjectSessionV3, + ScoreSubjectTraceV3, + ScoreSubjectV3, + ScoreSubjectV3_Experiment, + ScoreSubjectV3_Observation, + ScoreSubjectV3_Session, + ScoreSubjectV3_Trace, + ScoreV3, + ScoreV3_Boolean, + ScoreV3_Categorical, + ScoreV3_Correction, + ScoreV3_Numeric, + ScoreV3_Text, + TextScoreV3, ) _dynamic_imports: typing.Dict[str, str] = { + "BaseScoreV3": ".types", + "BooleanScoreV3": ".types", + "CategoricalScoreV3": ".types", + "CorrectionScoreV3": ".types", + "CreateScoreBatchError": ".types", + "CreateScoreBatchResponse": ".types", + "CreateScoreBatchResults": ".types", "CreateScoreRequest": ".types", - "CreateScoreResponse": ".types", "CreateScoreSource": ".types", - "GetScoresResponse": ".types", - "GetScoresResponseData": ".types", - "GetScoresResponseDataBoolean": ".types", - "GetScoresResponseDataCategorical": ".types", - "GetScoresResponseDataCorrection": ".types", - "GetScoresResponseDataNumeric": ".types", - "GetScoresResponseDataText": ".types", - "GetScoresResponseData_Boolean": ".types", - "GetScoresResponseData_Categorical": ".types", - "GetScoresResponseData_Correction": ".types", - "GetScoresResponseData_Numeric": ".types", - "GetScoresResponseData_Text": ".types", - "GetScoresResponseTraceData": ".types", + "CreateScoresRequest": ".types", + "CreateScoresResponse": ".types", + "GetScoresV3Meta": ".types", + "GetScoresV3Response": ".types", + "NumericScoreV3": ".types", + "ScoreBody": ".types", + "ScoreSubjectExperimentV3": ".types", + "ScoreSubjectObservationV3": ".types", + "ScoreSubjectSessionV3": ".types", + "ScoreSubjectTraceV3": ".types", + "ScoreSubjectV3": ".types", + "ScoreSubjectV3_Experiment": ".types", + "ScoreSubjectV3_Observation": ".types", + "ScoreSubjectV3_Session": ".types", + "ScoreSubjectV3_Trace": ".types", + "ScoreV3": ".types", + "ScoreV3_Boolean": ".types", + "ScoreV3_Categorical": ".types", + "ScoreV3_Correction": ".types", + "ScoreV3_Numeric": ".types", + "ScoreV3_Text": ".types", + "TextScoreV3": ".types", } @@ -72,20 +102,35 @@ def __dir__(): __all__ = [ + "BaseScoreV3", + "BooleanScoreV3", + "CategoricalScoreV3", + "CorrectionScoreV3", + "CreateScoreBatchError", + "CreateScoreBatchResponse", + "CreateScoreBatchResults", "CreateScoreRequest", - "CreateScoreResponse", "CreateScoreSource", - "GetScoresResponse", - "GetScoresResponseData", - "GetScoresResponseDataBoolean", - "GetScoresResponseDataCategorical", - "GetScoresResponseDataCorrection", - "GetScoresResponseDataNumeric", - "GetScoresResponseDataText", - "GetScoresResponseData_Boolean", - "GetScoresResponseData_Categorical", - "GetScoresResponseData_Correction", - "GetScoresResponseData_Numeric", - "GetScoresResponseData_Text", - "GetScoresResponseTraceData", + "CreateScoresRequest", + "CreateScoresResponse", + "GetScoresV3Meta", + "GetScoresV3Response", + "NumericScoreV3", + "ScoreBody", + "ScoreSubjectExperimentV3", + "ScoreSubjectObservationV3", + "ScoreSubjectSessionV3", + "ScoreSubjectTraceV3", + "ScoreSubjectV3", + "ScoreSubjectV3_Experiment", + "ScoreSubjectV3_Observation", + "ScoreSubjectV3_Session", + "ScoreSubjectV3_Trace", + "ScoreV3", + "ScoreV3_Boolean", + "ScoreV3_Categorical", + "ScoreV3_Correction", + "ScoreV3_Numeric", + "ScoreV3_Text", + "TextScoreV3", ] diff --git a/langfuse/api/scores/client.py b/langfuse/api/scores/client.py index 95f94389a..c5582cfc5 100644 --- a/langfuse/api/scores/client.py +++ b/langfuse/api/scores/client.py @@ -3,17 +3,12 @@ import datetime as dt import typing -import typing_extensions -from ..commons.types.create_score_value import CreateScoreValue -from ..commons.types.score import Score -from ..commons.types.score_data_type import ScoreDataType -from ..commons.types.score_source import ScoreSource from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from .raw_client import AsyncRawScoresClient, RawScoresClient -from .types.create_score_response import CreateScoreResponse -from .types.create_score_source import CreateScoreSource -from .types.get_scores_response import GetScoresResponse +from .types.create_scores_request import CreateScoresRequest +from .types.create_scores_response import CreateScoresResponse +from .types.get_scores_v3response import GetScoresV3Response # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -37,67 +32,71 @@ def with_raw_response(self) -> RawScoresClient: def create( self, *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, + request: CreateScoresRequest, request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: + ) -> CreateScoresResponse: """ - Create a score (supports trace, observation, session, and dataset run scores) + Create scores asynchronously from a list of scores. Returns 202 if all scores are accepted, or 207 with accepted/rejected counts and error messages. Do not automatically retry a 207 batch. The generated reference shows only 200; the endpoint returns 202 or 207. Parameters ---------- - name : str + request : CreateScoresRequest - value : CreateScoreValue - The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. - - id : typing.Optional[str] - - trace_id : typing.Optional[str] - - session_id : typing.Optional[str] - - observation_id : typing.Optional[str] - - dataset_run_id : typing.Optional[str] - - comment : typing.Optional[str] - - metadata : typing.Optional[typing.Dict[str, typing.Any]] + request_options : typing.Optional[RequestOptions] + Request-specific configuration. - environment : typing.Optional[str] - The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + Returns + ------- + CreateScoresResponse - queue_id : typing.Optional[str] - The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.scores import CreateScoreRequest - data_type : typing.Optional[ScoreDataType] - The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.scores.create( + request=[ + CreateScoreRequest( + name="name", + value=1.1, + ), + CreateScoreRequest( + name="name", + value=1.1, + ), + ], + ) + """ + _response = self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data - config_id : typing.Optional[str] - Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete a score (supports both trace and session scores) - source : typing.Optional[CreateScoreSource] - The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - CreateScoreResponse + None Examples -------- @@ -111,134 +110,126 @@ def create( password="YOUR_PASSWORD", base_url="https://yourhost.com/path/to/api", ) - client.scores.create( - name="name", - value=1.1, + client.scores.delete( + score_id="scoreId", ) """ - _response = self._raw_client.create( - name=name, - value=value, - id=id, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - dataset_run_id=dataset_run_id, - comment=comment, - metadata=metadata, - environment=environment, - queue_id=queue_id, - data_type=data_type, - config_id=config_id, - source=source, - request_options=request_options, - ) + _response = self._raw_client.delete(score_id, request_options=request_options) return _response.data - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) def get_many( self, *, - page: typing.Optional[int] = None, limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, name: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - source: typing.Optional[ScoreSource] = None, - operator: typing.Optional[str] = None, - value: typing.Optional[float] = None, - score_ids: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, config_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - dataset_run_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, observation_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - data_type: typing.Optional[ScoreDataType] = None, - trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, request_options: typing.Optional[RequestOptions] = None, - ) -> GetScoresResponse: + ) -> GetScoresV3Response: """ - Get a list of scores (supports both trace and session scores) + Get a list of scores with a polymorphic `value` field (v3). + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + The response always includes the core fields: id, projectId, name, + value, dataType, source, timestamp, environment, createdAt, updatedAt. + + Additional field groups can be requested via the `fields` parameter: + - `details` — adds comment, configId, metadata + - `subject` — adds the subject object describing the entity the score + is attached to: kind (trace, observation, session, or experiment), + id, and traceId for observation-level scores + - `annotation` — adds authorUserId, queueId + + Unknown group names return HTTP 400. Parameters ---------- - page : typing.Optional[int] - Page number, starts at 1. - limit : typing.Optional[int] - Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. - user_id : typing.Optional[str] - Retrieve only scores with this userId associated to the trace. + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. - name : typing.Optional[str] - Retrieve only scores with this name. + fields : typing.Optional[str] + Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created on or after a certain datetime (ISO 8601) + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created before a certain datetime (ISO 8601) + name : typing.Optional[str] + Comma-separated list of score names to filter by. - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for scores where the environment is one of the provided values. + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. - source : typing.Optional[ScoreSource] - Retrieve only scores from a specific source. + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. - operator : typing.Optional[str] - Retrieve only scores with value. + environment : typing.Optional[str] + Comma-separated list of environments to filter by. - value : typing.Optional[float] - Retrieve only scores with value. + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. - score_ids : typing.Optional[str] - Comma-separated list of score IDs to limit the results to. + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. - config_id : typing.Optional[str] - Retrieve only scores with a specific configId. + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. - session_id : typing.Optional[str] - Retrieve only scores with a specific sessionId. + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. - dataset_run_id : typing.Optional[str] - Retrieve only scores with a specific datasetRunId. + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - trace_id : typing.Optional[str] - Retrieve only scores with a specific traceId. + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter scores by. + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. - queue_id : typing.Optional[str] - Retrieve only scores with a specific annotation queueId. + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. - data_type : typing.Optional[ScoreDataType] - Retrieve only scores with a specific dataType. + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. - trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only scores linked to traces that include all of these tags will be returned. + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. - fields : typing.Optional[str] - Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. - filter : typing.Optional[str] - A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with, is set, is not set. Use is set / is not set to filter on metadata key presence. An empty value for contains, starts with, or ends with is treated as is set (key presence). + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - GetScoresResponse + GetScoresV3Response Examples -------- @@ -255,74 +246,30 @@ def get_many( client.scores.get_many() """ _response = self._raw_client.get_many( - page=page, limit=limit, - user_id=user_id, + cursor=cursor, + fields=fields, + id=id, name=name, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - environment=environment, source=source, - operator=operator, - value=value, - score_ids=score_ids, + data_type=data_type, + environment=environment, config_id=config_id, - session_id=session_id, - dataset_run_id=dataset_run_id, + queue_id=queue_id, + author_user_id=author_user_id, + value=value, + value_min=value_min, + value_max=value_max, trace_id=trace_id, + session_id=session_id, observation_id=observation_id, - queue_id=queue_id, - data_type=data_type, - trace_tags=trace_tags, - fields=fields, - filter=filter, + experiment_id=experiment_id, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, request_options=request_options, ) return _response.data - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` with the `id` filter instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_by_id( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> Score: - """ - Get a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Score - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scores.get_by_id( - score_id="scoreId", - ) - """ - _response = self._raw_client.get_by_id( - score_id, request_options=request_options - ) - return _response.data - class AsyncScoresClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -342,67 +289,79 @@ def with_raw_response(self) -> AsyncRawScoresClient: async def create( self, *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, + request: CreateScoresRequest, request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: + ) -> CreateScoresResponse: """ - Create a score (supports trace, observation, session, and dataset run scores) + Create scores asynchronously from a list of scores. Returns 202 if all scores are accepted, or 207 with accepted/rejected counts and error messages. Do not automatically retry a 207 batch. The generated reference shows only 200; the endpoint returns 202 or 207. Parameters ---------- - name : str - - value : CreateScoreValue - The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. - - id : typing.Optional[str] + request : CreateScoresRequest - trace_id : typing.Optional[str] + request_options : typing.Optional[RequestOptions] + Request-specific configuration. - session_id : typing.Optional[str] + Returns + ------- + CreateScoresResponse - observation_id : typing.Optional[str] + Examples + -------- + import asyncio - dataset_run_id : typing.Optional[str] + from langfuse import AsyncLangfuseAPI + from langfuse.scores import CreateScoreRequest - comment : typing.Optional[str] + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) - metadata : typing.Optional[typing.Dict[str, typing.Any]] - environment : typing.Optional[str] - The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. + async def main() -> None: + await client.scores.create( + request=[ + CreateScoreRequest( + name="name", + value=1.1, + ), + CreateScoreRequest( + name="name", + value=1.1, + ), + ], + ) - queue_id : typing.Optional[str] - The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. - data_type : typing.Optional[ScoreDataType] - The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. + asyncio.run(main()) + """ + _response = await self._raw_client.create( + request=request, request_options=request_options + ) + return _response.data - config_id : typing.Optional[str] - Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. + async def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> None: + """ + Delete a score (supports both trace and session scores) - source : typing.Optional[CreateScoreSource] - The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + Parameters + ---------- + score_id : str + The unique langfuse identifier of a score request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - CreateScoreResponse + None Examples -------- @@ -421,137 +380,131 @@ async def create( async def main() -> None: - await client.scores.create( - name="name", - value=1.1, + await client.scores.delete( + score_id="scoreId", ) asyncio.run(main()) """ - _response = await self._raw_client.create( - name=name, - value=value, - id=id, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - dataset_run_id=dataset_run_id, - comment=comment, - metadata=metadata, - environment=environment, - queue_id=queue_id, - data_type=data_type, - config_id=config_id, - source=source, - request_options=request_options, + _response = await self._raw_client.delete( + score_id, request_options=request_options ) return _response.data - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) async def get_many( self, *, - page: typing.Optional[int] = None, limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, name: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - source: typing.Optional[ScoreSource] = None, - operator: typing.Optional[str] = None, - value: typing.Optional[float] = None, - score_ids: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, config_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - dataset_run_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, observation_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - data_type: typing.Optional[ScoreDataType] = None, - trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, request_options: typing.Optional[RequestOptions] = None, - ) -> GetScoresResponse: + ) -> GetScoresV3Response: """ - Get a list of scores (supports both trace and session scores) + Get a list of scores with a polymorphic `value` field (v3). + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + The response always includes the core fields: id, projectId, name, + value, dataType, source, timestamp, environment, createdAt, updatedAt. + + Additional field groups can be requested via the `fields` parameter: + - `details` — adds comment, configId, metadata + - `subject` — adds the subject object describing the entity the score + is attached to: kind (trace, observation, session, or experiment), + id, and traceId for observation-level scores + - `annotation` — adds authorUserId, queueId + + Unknown group names return HTTP 400. Parameters ---------- - page : typing.Optional[int] - Page number, starts at 1. - limit : typing.Optional[int] - Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. - user_id : typing.Optional[str] - Retrieve only scores with this userId associated to the trace. + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. - name : typing.Optional[str] - Retrieve only scores with this name. + fields : typing.Optional[str] + Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created on or after a certain datetime (ISO 8601) + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created before a certain datetime (ISO 8601) + name : typing.Optional[str] + Comma-separated list of score names to filter by. - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for scores where the environment is one of the provided values. + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. - source : typing.Optional[ScoreSource] - Retrieve only scores from a specific source. + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. - operator : typing.Optional[str] - Retrieve only scores with value. + environment : typing.Optional[str] + Comma-separated list of environments to filter by. - value : typing.Optional[float] - Retrieve only scores with value. + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. - score_ids : typing.Optional[str] - Comma-separated list of score IDs to limit the results to. + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. - config_id : typing.Optional[str] - Retrieve only scores with a specific configId. + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. - session_id : typing.Optional[str] - Retrieve only scores with a specific sessionId. + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. - dataset_run_id : typing.Optional[str] - Retrieve only scores with a specific datasetRunId. + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - trace_id : typing.Optional[str] - Retrieve only scores with a specific traceId. + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter scores by. + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. - queue_id : typing.Optional[str] - Retrieve only scores with a specific annotation queueId. + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. - data_type : typing.Optional[ScoreDataType] - Retrieve only scores with a specific dataType. + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. - trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only scores linked to traces that include all of these tags will be returned. + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. - fields : typing.Optional[str] - Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. - filter : typing.Optional[str] - A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with, is set, is not set. Use is set / is not set to filter on metadata key presence. An empty value for contains, starts with, or ends with is treated as is set (key presence). + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - GetScoresResponse + GetScoresV3Response Examples -------- @@ -576,78 +529,26 @@ async def main() -> None: asyncio.run(main()) """ _response = await self._raw_client.get_many( - page=page, limit=limit, - user_id=user_id, + cursor=cursor, + fields=fields, + id=id, name=name, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - environment=environment, source=source, - operator=operator, - value=value, - score_ids=score_ids, + data_type=data_type, + environment=environment, config_id=config_id, - session_id=session_id, - dataset_run_id=dataset_run_id, + queue_id=queue_id, + author_user_id=author_user_id, + value=value, + value_min=value_min, + value_max=value_max, trace_id=trace_id, + session_id=session_id, observation_id=observation_id, - queue_id=queue_id, - data_type=data_type, - trace_tags=trace_tags, - fields=fields, - filter=filter, + experiment_id=experiment_id, + from_timestamp=from_timestamp, + to_timestamp=to_timestamp, request_options=request_options, ) return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` with the `id` filter instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_by_id( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> Score: - """ - Get a score (supports both trace and session scores) - - Parameters - ---------- - score_id : str - The unique langfuse identifier of a score - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Score - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scores.get_by_id( - score_id="scoreId", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_by_id( - score_id, request_options=request_options - ) - return _response.data diff --git a/langfuse/api/scores/raw_client.py b/langfuse/api/scores/raw_client.py index e4295f233..fcc599f7f 100644 --- a/langfuse/api/scores/raw_client.py +++ b/langfuse/api/scores/raw_client.py @@ -2,7 +2,6 @@ import datetime as dt import typing -import typing_extensions from json.decoder import JSONDecodeError from ..commons.errors.access_denied_error import AccessDeniedError @@ -10,10 +9,6 @@ from ..commons.errors.method_not_allowed_error import MethodNotAllowedError from ..commons.errors.not_found_error import NotFoundError from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.create_score_value import CreateScoreValue -from ..commons.types.score import Score -from ..commons.types.score_data_type import ScoreDataType -from ..commons.types.score_source import ScoreSource from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.datetime_utils import serialize_datetime @@ -22,9 +17,9 @@ from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from ..core.serialization import convert_and_respect_annotation_metadata -from .types.create_score_response import CreateScoreResponse -from .types.create_score_source import CreateScoreSource -from .types.get_scores_response import GetScoresResponse +from .types.create_scores_request import CreateScoresRequest +from .types.create_scores_response import CreateScoresResponse +from .types.get_scores_v3response import GetScoresV3Response # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -37,98 +32,38 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): def create( self, *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, + request: CreateScoresRequest, request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[CreateScoreResponse]: + ) -> HttpResponse[CreateScoresResponse]: """ - Create a score (supports trace, observation, session, and dataset run scores) + Create scores asynchronously from a list of scores. Returns 202 if all scores are accepted, or 207 with accepted/rejected counts and error messages. Do not automatically retry a 207 batch. The generated reference shows only 200; the endpoint returns 202 or 207. Parameters ---------- - name : str - - value : CreateScoreValue - The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. - - id : typing.Optional[str] - - trace_id : typing.Optional[str] - - session_id : typing.Optional[str] - - observation_id : typing.Optional[str] - - dataset_run_id : typing.Optional[str] - - comment : typing.Optional[str] - - metadata : typing.Optional[typing.Dict[str, typing.Any]] - - environment : typing.Optional[str] - The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - - queue_id : typing.Optional[str] - The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. - - data_type : typing.Optional[ScoreDataType] - The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. - - config_id : typing.Optional[str] - Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. - - source : typing.Optional[CreateScoreSource] - The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + request : CreateScoresRequest request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - HttpResponse[CreateScoreResponse] + HttpResponse[CreateScoresResponse] """ _response = self._client_wrapper.httpx_client.request( "api/public/scores", method="POST", - json={ - "id": id, - "traceId": trace_id, - "sessionId": session_id, - "observationId": observation_id, - "datasetRunId": dataset_run_id, - "name": name, - "value": convert_and_respect_annotation_metadata( - object_=value, annotation=CreateScoreValue, direction="write" - ), - "comment": comment, - "metadata": metadata, - "environment": environment, - "queueId": queue_id, - "dataType": data_type, - "configId": config_id, - "source": source, - }, + json=convert_and_respect_annotation_metadata( + object_=request, annotation=CreateScoresRequest, direction="write" + ), request_options=request_options, omit=OMIT, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - CreateScoreResponse, + CreateScoresResponse, parse_obj_as( - type_=CreateScoreResponse, # type: ignore + type_=CreateScoresResponse, # type: ignore object_=_response.json(), ), ) @@ -201,153 +136,32 @@ def create( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - source: typing.Optional[ScoreSource] = None, - operator: typing.Optional[str] = None, - value: typing.Optional[float] = None, - score_ids: typing.Optional[str] = None, - config_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - dataset_run_id: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - observation_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - data_type: typing.Optional[ScoreDataType] = None, - trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[GetScoresResponse]: + def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: """ - Get a list of scores (supports both trace and session scores) + Delete a score (supports both trace and session scores) Parameters ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - Retrieve only scores with this userId associated to the trace. - - name : typing.Optional[str] - Retrieve only scores with this name. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for scores where the environment is one of the provided values. - - source : typing.Optional[ScoreSource] - Retrieve only scores from a specific source. - - operator : typing.Optional[str] - Retrieve only scores with value. - - value : typing.Optional[float] - Retrieve only scores with value. - - score_ids : typing.Optional[str] - Comma-separated list of score IDs to limit the results to. - - config_id : typing.Optional[str] - Retrieve only scores with a specific configId. - - session_id : typing.Optional[str] - Retrieve only scores with a specific sessionId. - - dataset_run_id : typing.Optional[str] - Retrieve only scores with a specific datasetRunId. - - trace_id : typing.Optional[str] - Retrieve only scores with a specific traceId. - - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter scores by. - - queue_id : typing.Optional[str] - Retrieve only scores with a specific annotation queueId. - - data_type : typing.Optional[ScoreDataType] - Retrieve only scores with a specific dataType. - - trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only scores linked to traces that include all of these tags will be returned. - - fields : typing.Optional[str] - Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. - - filter : typing.Optional[str] - A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with, is set, is not set. Use is set / is not set to filter on metadata key presence. An empty value for contains, starts with, or ends with is treated as is set (key presence). + score_id : str + The unique langfuse identifier of a score request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - HttpResponse[GetScoresResponse] + HttpResponse[None] """ _response = self._client_wrapper.httpx_client.request( - "api/public/v2/scores", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - "source": source, - "operator": operator, - "value": value, - "scoreIds": score_ids, - "configId": config_id, - "sessionId": session_id, - "datasetRunId": dataset_run_id, - "traceId": trace_id, - "observationId": observation_id, - "queueId": queue_id, - "dataType": data_type, - "traceTags": trace_tags, - "fields": fields, - "filter": filter, - }, + f"api/public/scores/{jsonable_encoder(score_id)}", + method="DELETE", request_options=request_options, ) try: if 200 <= _response.status_code < 300: - _data = typing.cast( - GetScoresResponse, - parse_obj_as( - type_=GetScoresResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) + return HttpResponse(response=_response, data=None) if _response.status_code == 400: raise Error( headers=dict(_response.headers), @@ -416,39 +230,157 @@ def get_many( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` with the `id` filter instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get_by_id( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[Score]: + def get_many( + self, + *, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, + trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[GetScoresV3Response]: """ - Get a score (supports both trace and session scores) + Get a list of scores with a polymorphic `value` field (v3). + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + The response always includes the core fields: id, projectId, name, + value, dataType, source, timestamp, environment, createdAt, updatedAt. + + Additional field groups can be requested via the `fields` parameter: + - `details` — adds comment, configId, metadata + - `subject` — adds the subject object describing the entity the score + is attached to: kind (trace, observation, session, or experiment), + id, and traceId for observation-level scores + - `annotation` — adds authorUserId, queueId + + Unknown group names return HTTP 400. Parameters ---------- - score_id : str - The unique langfuse identifier of a score + limit : typing.Optional[int] + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. + + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. + + fields : typing.Optional[str] + Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. + + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). + + name : typing.Optional[str] + Comma-separated list of score names to filter by. + + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. + + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. + + environment : typing.Optional[str] + Comma-separated list of environments to filter by. + + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. + + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. + + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. + + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. + + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. + + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. + + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. + + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. + + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - HttpResponse[Score] + HttpResponse[GetScoresV3Response] """ _response = self._client_wrapper.httpx_client.request( - f"api/public/v2/scores/{jsonable_encoder(score_id)}", + "api/public/v3/scores", method="GET", + params={ + "limit": limit, + "cursor": cursor, + "fields": fields, + "id": id, + "name": name, + "source": source, + "dataType": data_type, + "environment": environment, + "configId": config_id, + "queueId": queue_id, + "authorUserId": author_user_id, + "value": value, + "valueMin": value_min, + "valueMax": value_max, + "traceId": trace_id, + "sessionId": session_id, + "observationId": observation_id, + "experimentId": experiment_id, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + }, request_options=request_options, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - Score, + GetScoresV3Response, parse_obj_as( - type_=Score, # type: ignore + type_=GetScoresV3Response, # type: ignore object_=_response.json(), ), ) @@ -529,98 +461,38 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): async def create( self, *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, + request: CreateScoresRequest, request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[CreateScoreResponse]: + ) -> AsyncHttpResponse[CreateScoresResponse]: """ - Create a score (supports trace, observation, session, and dataset run scores) + Create scores asynchronously from a list of scores. Returns 202 if all scores are accepted, or 207 with accepted/rejected counts and error messages. Do not automatically retry a 207 batch. The generated reference shows only 200; the endpoint returns 202 or 207. Parameters ---------- - name : str - - value : CreateScoreValue - The value of the score. Must be passed as string for categorical and text scores, and numeric for boolean and numeric scores. Boolean score values must equal either 1 or 0 (true or false). Text score values must be between 1 and 500 characters. - - id : typing.Optional[str] - - trace_id : typing.Optional[str] - - session_id : typing.Optional[str] - - observation_id : typing.Optional[str] - - dataset_run_id : typing.Optional[str] - - comment : typing.Optional[str] - - metadata : typing.Optional[typing.Dict[str, typing.Any]] - - environment : typing.Optional[str] - The environment of the score. Can be any lowercase alphanumeric string with hyphens and underscores that does not start with 'langfuse'. - - queue_id : typing.Optional[str] - The annotation queue referenced by the score. Indicates if score was initially created while processing annotation queue. - - data_type : typing.Optional[ScoreDataType] - The data type of the score. When passing a configId this field is inferred. Otherwise, this field must be passed or will default to numeric. - - config_id : typing.Optional[str] - Reference a score config on a score. The unique langfuse identifier of a score config. When passing this field, the dataType and stringValue fields are automatically populated. - - source : typing.Optional[CreateScoreSource] - The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. + request : CreateScoresRequest request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[CreateScoreResponse] + AsyncHttpResponse[CreateScoresResponse] """ _response = await self._client_wrapper.httpx_client.request( "api/public/scores", method="POST", - json={ - "id": id, - "traceId": trace_id, - "sessionId": session_id, - "observationId": observation_id, - "datasetRunId": dataset_run_id, - "name": name, - "value": convert_and_respect_annotation_metadata( - object_=value, annotation=CreateScoreValue, direction="write" - ), - "comment": comment, - "metadata": metadata, - "environment": environment, - "queueId": queue_id, - "dataType": data_type, - "configId": config_id, - "source": source, - }, + json=convert_and_respect_annotation_metadata( + object_=request, annotation=CreateScoresRequest, direction="write" + ), request_options=request_options, omit=OMIT, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - CreateScoreResponse, + CreateScoresResponse, parse_obj_as( - type_=CreateScoreResponse, # type: ignore + type_=CreateScoresResponse, # type: ignore object_=_response.json(), ), ) @@ -693,153 +565,32 @@ async def create( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_many( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - source: typing.Optional[ScoreSource] = None, - operator: typing.Optional[str] = None, - value: typing.Optional[float] = None, - score_ids: typing.Optional[str] = None, - config_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - dataset_run_id: typing.Optional[str] = None, - trace_id: typing.Optional[str] = None, - observation_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - data_type: typing.Optional[ScoreDataType] = None, - trace_tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[GetScoresResponse]: + async def delete( + self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: """ - Get a list of scores (supports both trace and session scores) + Delete a score (supports both trace and session scores) Parameters ---------- - page : typing.Optional[int] - Page number, starts at 1. - - limit : typing.Optional[int] - Limit of items per page. Maximum 100. Defaults to 50. Requests with a limit greater than 100 return HTTP 400. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - Retrieve only scores with this userId associated to the trace. - - name : typing.Optional[str] - Retrieve only scores with this name. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include scores created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for scores where the environment is one of the provided values. - - source : typing.Optional[ScoreSource] - Retrieve only scores from a specific source. - - operator : typing.Optional[str] - Retrieve only scores with value. - - value : typing.Optional[float] - Retrieve only scores with value. - - score_ids : typing.Optional[str] - Comma-separated list of score IDs to limit the results to. - - config_id : typing.Optional[str] - Retrieve only scores with a specific configId. - - session_id : typing.Optional[str] - Retrieve only scores with a specific sessionId. - - dataset_run_id : typing.Optional[str] - Retrieve only scores with a specific datasetRunId. - - trace_id : typing.Optional[str] - Retrieve only scores with a specific traceId. - - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter scores by. - - queue_id : typing.Optional[str] - Retrieve only scores with a specific annotation queueId. - - data_type : typing.Optional[ScoreDataType] - Retrieve only scores with a specific dataType. - - trace_tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only scores linked to traces that include all of these tags will be returned. - - fields : typing.Optional[str] - Comma-separated list of field groups to include in the response. Available field groups: 'score' (core score fields), 'trace' (trace properties: userId, tags, environment, sessionId). If not specified, both 'score' and 'trace' are returned by default. Example: 'score' to exclude trace data, 'score,trace' to include both. Note: When filtering by trace properties (using userId or traceTags parameters), the 'trace' field group must be included, otherwise a 400 error will be returned. - - filter : typing.Optional[str] - A JSON stringified array of filter objects. Each object requires type, column, operator, and value. Supports filtering by score metadata using the stringObject type. Example: [{"type":"stringObject","column":"metadata","key":"user_id","operator":"=","value":"abc123"}]. Supported types: stringObject (metadata key-value filtering), string, number, datetime, stringOptions, arrayOptions. Supported operators for stringObject: =, contains, does not contain, starts with, ends with, is set, is not set. Use is set / is not set to filter on metadata key presence. An empty value for contains, starts with, or ends with is treated as is set (key presence). + score_id : str + The unique langfuse identifier of a score request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[GetScoresResponse] + AsyncHttpResponse[None] """ _response = await self._client_wrapper.httpx_client.request( - "api/public/v2/scores", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - "source": source, - "operator": operator, - "value": value, - "scoreIds": score_ids, - "configId": config_id, - "sessionId": session_id, - "datasetRunId": dataset_run_id, - "traceId": trace_id, - "observationId": observation_id, - "queueId": queue_id, - "dataType": data_type, - "traceTags": trace_tags, - "fields": fields, - "filter": filter, - }, + f"api/public/scores/{jsonable_encoder(score_id)}", + method="DELETE", request_options=request_options, ) try: if 200 <= _response.status_code < 300: - _data = typing.cast( - GetScoresResponse, - parse_obj_as( - type_=GetScoresResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) + return AsyncHttpResponse(response=_response, data=None) if _response.status_code == 400: raise Error( headers=dict(_response.headers), @@ -908,39 +659,157 @@ async def get_many( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. Use `GET /api/public/v3/scores` with the `id` filter instead. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get_by_id( - self, score_id: str, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[Score]: + async def get_many( + self, + *, + limit: typing.Optional[int] = None, + cursor: typing.Optional[str] = None, + fields: typing.Optional[str] = None, + id: typing.Optional[str] = None, + name: typing.Optional[str] = None, + source: typing.Optional[str] = None, + data_type: typing.Optional[str] = None, + environment: typing.Optional[str] = None, + config_id: typing.Optional[str] = None, + queue_id: typing.Optional[str] = None, + author_user_id: typing.Optional[str] = None, + value: typing.Optional[str] = None, + value_min: typing.Optional[float] = None, + value_max: typing.Optional[float] = None, + trace_id: typing.Optional[str] = None, + session_id: typing.Optional[str] = None, + observation_id: typing.Optional[str] = None, + experiment_id: typing.Optional[str] = None, + from_timestamp: typing.Optional[dt.datetime] = None, + to_timestamp: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[GetScoresV3Response]: """ - Get a score (supports both trace and session scores) + Get a list of scores with a polymorphic `value` field (v3). + + The `value` field type depends on `dataType`: + - `NUMERIC` → number + - `BOOLEAN` → boolean + - `CATEGORICAL`, `TEXT`, `CORRECTION` → string + + The response always includes the core fields: id, projectId, name, + value, dataType, source, timestamp, environment, createdAt, updatedAt. + + Additional field groups can be requested via the `fields` parameter: + - `details` — adds comment, configId, metadata + - `subject` — adds the subject object describing the entity the score + is attached to: kind (trace, observation, session, or experiment), + id, and traceId for observation-level scores + - `annotation` — adds authorUserId, queueId + + Unknown group names return HTTP 400. Parameters ---------- - score_id : str - The unique langfuse identifier of a score + limit : typing.Optional[int] + Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. + + cursor : typing.Optional[str] + URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. + + fields : typing.Optional[str] + Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. + + id : typing.Optional[str] + Comma-separated list of score IDs to filter by (OR within, AND across filters). + + name : typing.Optional[str] + Comma-separated list of score names to filter by. + + source : typing.Optional[str] + Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. + + data_type : typing.Optional[str] + Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. + + environment : typing.Optional[str] + Comma-separated list of environments to filter by. + + config_id : typing.Optional[str] + Comma-separated list of score config IDs to filter by. + + queue_id : typing.Optional[str] + Comma-separated list of annotation queue IDs to filter by. + + author_user_id : typing.Optional[str] + Comma-separated list of author user IDs to filter by. + + value : typing.Optional[str] + Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. + + value_min : typing.Optional[float] + Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + value_max : typing.Optional[float] + Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. + + trace_id : typing.Optional[str] + Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. + + session_id : typing.Optional[str] + Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. + + observation_id : typing.Optional[str] + Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. + + experiment_id : typing.Optional[str] + Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. + + from_timestamp : typing.Optional[dt.datetime] + Inclusive lower bound on the score timestamp. + + to_timestamp : typing.Optional[dt.datetime] + Exclusive upper bound on the score timestamp. request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[Score] + AsyncHttpResponse[GetScoresV3Response] """ _response = await self._client_wrapper.httpx_client.request( - f"api/public/v2/scores/{jsonable_encoder(score_id)}", + "api/public/v3/scores", method="GET", + params={ + "limit": limit, + "cursor": cursor, + "fields": fields, + "id": id, + "name": name, + "source": source, + "dataType": data_type, + "environment": environment, + "configId": config_id, + "queueId": queue_id, + "authorUserId": author_user_id, + "value": value, + "valueMin": value_min, + "valueMax": value_max, + "traceId": trace_id, + "sessionId": session_id, + "observationId": observation_id, + "experimentId": experiment_id, + "fromTimestamp": serialize_datetime(from_timestamp) + if from_timestamp is not None + else None, + "toTimestamp": serialize_datetime(to_timestamp) + if to_timestamp is not None + else None, + }, request_options=request_options, ) try: if 200 <= _response.status_code < 300: _data = typing.cast( - Score, + GetScoresV3Response, parse_obj_as( - type_=Score, # type: ignore + type_=GetScoresV3Response, # type: ignore object_=_response.json(), ), ) diff --git a/langfuse/api/scores/types/__init__.py b/langfuse/api/scores/types/__init__.py index 754c497f4..9a2be8f5c 100644 --- a/langfuse/api/scores/types/__init__.py +++ b/langfuse/api/scores/types/__init__.py @@ -6,41 +6,73 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .base_score_v3 import BaseScoreV3 + from .boolean_score_v3 import BooleanScoreV3 + from .categorical_score_v3 import CategoricalScoreV3 + from .correction_score_v3 import CorrectionScoreV3 + from .create_score_batch_error import CreateScoreBatchError + from .create_score_batch_response import CreateScoreBatchResponse + from .create_score_batch_results import CreateScoreBatchResults from .create_score_request import CreateScoreRequest - from .create_score_response import CreateScoreResponse from .create_score_source import CreateScoreSource - from .get_scores_response import GetScoresResponse - from .get_scores_response_data import ( - GetScoresResponseData, - GetScoresResponseData_Boolean, - GetScoresResponseData_Categorical, - GetScoresResponseData_Correction, - GetScoresResponseData_Numeric, - GetScoresResponseData_Text, + from .create_scores_request import CreateScoresRequest + from .create_scores_response import CreateScoresResponse + from .get_scores_v3meta import GetScoresV3Meta + from .get_scores_v3response import GetScoresV3Response + from .numeric_score_v3 import NumericScoreV3 + from .score_body import ScoreBody + from .score_subject_experiment_v3 import ScoreSubjectExperimentV3 + from .score_subject_observation_v3 import ScoreSubjectObservationV3 + from .score_subject_session_v3 import ScoreSubjectSessionV3 + from .score_subject_trace_v3 import ScoreSubjectTraceV3 + from .score_subject_v3 import ( + ScoreSubjectV3, + ScoreSubjectV3_Experiment, + ScoreSubjectV3_Observation, + ScoreSubjectV3_Session, + ScoreSubjectV3_Trace, ) - from .get_scores_response_data_boolean import GetScoresResponseDataBoolean - from .get_scores_response_data_categorical import GetScoresResponseDataCategorical - from .get_scores_response_data_correction import GetScoresResponseDataCorrection - from .get_scores_response_data_numeric import GetScoresResponseDataNumeric - from .get_scores_response_data_text import GetScoresResponseDataText - from .get_scores_response_trace_data import GetScoresResponseTraceData + from .score_v3 import ( + ScoreV3, + ScoreV3_Boolean, + ScoreV3_Categorical, + ScoreV3_Correction, + ScoreV3_Numeric, + ScoreV3_Text, + ) + from .text_score_v3 import TextScoreV3 _dynamic_imports: typing.Dict[str, str] = { + "BaseScoreV3": ".base_score_v3", + "BooleanScoreV3": ".boolean_score_v3", + "CategoricalScoreV3": ".categorical_score_v3", + "CorrectionScoreV3": ".correction_score_v3", + "CreateScoreBatchError": ".create_score_batch_error", + "CreateScoreBatchResponse": ".create_score_batch_response", + "CreateScoreBatchResults": ".create_score_batch_results", "CreateScoreRequest": ".create_score_request", - "CreateScoreResponse": ".create_score_response", "CreateScoreSource": ".create_score_source", - "GetScoresResponse": ".get_scores_response", - "GetScoresResponseData": ".get_scores_response_data", - "GetScoresResponseDataBoolean": ".get_scores_response_data_boolean", - "GetScoresResponseDataCategorical": ".get_scores_response_data_categorical", - "GetScoresResponseDataCorrection": ".get_scores_response_data_correction", - "GetScoresResponseDataNumeric": ".get_scores_response_data_numeric", - "GetScoresResponseDataText": ".get_scores_response_data_text", - "GetScoresResponseData_Boolean": ".get_scores_response_data", - "GetScoresResponseData_Categorical": ".get_scores_response_data", - "GetScoresResponseData_Correction": ".get_scores_response_data", - "GetScoresResponseData_Numeric": ".get_scores_response_data", - "GetScoresResponseData_Text": ".get_scores_response_data", - "GetScoresResponseTraceData": ".get_scores_response_trace_data", + "CreateScoresRequest": ".create_scores_request", + "CreateScoresResponse": ".create_scores_response", + "GetScoresV3Meta": ".get_scores_v3meta", + "GetScoresV3Response": ".get_scores_v3response", + "NumericScoreV3": ".numeric_score_v3", + "ScoreBody": ".score_body", + "ScoreSubjectExperimentV3": ".score_subject_experiment_v3", + "ScoreSubjectObservationV3": ".score_subject_observation_v3", + "ScoreSubjectSessionV3": ".score_subject_session_v3", + "ScoreSubjectTraceV3": ".score_subject_trace_v3", + "ScoreSubjectV3": ".score_subject_v3", + "ScoreSubjectV3_Experiment": ".score_subject_v3", + "ScoreSubjectV3_Observation": ".score_subject_v3", + "ScoreSubjectV3_Session": ".score_subject_v3", + "ScoreSubjectV3_Trace": ".score_subject_v3", + "ScoreV3": ".score_v3", + "ScoreV3_Boolean": ".score_v3", + "ScoreV3_Categorical": ".score_v3", + "ScoreV3_Correction": ".score_v3", + "ScoreV3_Numeric": ".score_v3", + "ScoreV3_Text": ".score_v3", + "TextScoreV3": ".text_score_v3", } @@ -72,20 +104,35 @@ def __dir__(): __all__ = [ + "BaseScoreV3", + "BooleanScoreV3", + "CategoricalScoreV3", + "CorrectionScoreV3", + "CreateScoreBatchError", + "CreateScoreBatchResponse", + "CreateScoreBatchResults", "CreateScoreRequest", - "CreateScoreResponse", "CreateScoreSource", - "GetScoresResponse", - "GetScoresResponseData", - "GetScoresResponseDataBoolean", - "GetScoresResponseDataCategorical", - "GetScoresResponseDataCorrection", - "GetScoresResponseDataNumeric", - "GetScoresResponseDataText", - "GetScoresResponseData_Boolean", - "GetScoresResponseData_Categorical", - "GetScoresResponseData_Correction", - "GetScoresResponseData_Numeric", - "GetScoresResponseData_Text", - "GetScoresResponseTraceData", + "CreateScoresRequest", + "CreateScoresResponse", + "GetScoresV3Meta", + "GetScoresV3Response", + "NumericScoreV3", + "ScoreBody", + "ScoreSubjectExperimentV3", + "ScoreSubjectObservationV3", + "ScoreSubjectSessionV3", + "ScoreSubjectTraceV3", + "ScoreSubjectV3", + "ScoreSubjectV3_Experiment", + "ScoreSubjectV3_Observation", + "ScoreSubjectV3_Session", + "ScoreSubjectV3_Trace", + "ScoreV3", + "ScoreV3_Boolean", + "ScoreV3_Categorical", + "ScoreV3_Correction", + "ScoreV3_Numeric", + "ScoreV3_Text", + "TextScoreV3", ] diff --git a/langfuse/api/scores_v3/types/base_score_v3.py b/langfuse/api/scores/types/base_score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/base_score_v3.py rename to langfuse/api/scores/types/base_score_v3.py diff --git a/langfuse/api/scores_v3/types/boolean_score_v3.py b/langfuse/api/scores/types/boolean_score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/boolean_score_v3.py rename to langfuse/api/scores/types/boolean_score_v3.py diff --git a/langfuse/api/scores_v3/types/categorical_score_v3.py b/langfuse/api/scores/types/categorical_score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/categorical_score_v3.py rename to langfuse/api/scores/types/categorical_score_v3.py diff --git a/langfuse/api/scores_v3/types/correction_score_v3.py b/langfuse/api/scores/types/correction_score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/correction_score_v3.py rename to langfuse/api/scores/types/correction_score_v3.py diff --git a/langfuse/api/datasets/types/delete_dataset_run_response.py b/langfuse/api/scores/types/create_score_batch_error.py similarity index 84% rename from langfuse/api/datasets/types/delete_dataset_run_response.py rename to langfuse/api/scores/types/create_score_batch_error.py index 024433ef6..a34b11a24 100644 --- a/langfuse/api/datasets/types/delete_dataset_run_response.py +++ b/langfuse/api/scores/types/create_score_batch_error.py @@ -6,7 +6,7 @@ from ...core.pydantic_utilities import UniversalBaseModel -class DeleteDatasetRunResponse(UniversalBaseModel): +class CreateScoreBatchError(UniversalBaseModel): message: str model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( diff --git a/langfuse/api/scores/types/create_score_response.py b/langfuse/api/scores/types/create_score_batch_response.py similarity index 68% rename from langfuse/api/scores/types/create_score_response.py rename to langfuse/api/scores/types/create_score_batch_response.py index 1c20c0f3a..b68766fa8 100644 --- a/langfuse/api/scores/types/create_score_response.py +++ b/langfuse/api/scores/types/create_score_batch_response.py @@ -6,12 +6,13 @@ from ...core.pydantic_utilities import UniversalBaseModel -class CreateScoreResponse(UniversalBaseModel): - id: str = pydantic.Field() +class CreateScoreBatchResponse(UniversalBaseModel): """ - The id of the created object in Langfuse + Batch accepted (HTTP 202). """ + message: typing.Literal["Accepted"] = "Accepted" + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True ) diff --git a/langfuse/api/ingestion/types/ingestion_error.py b/langfuse/api/scores/types/create_score_batch_results.py similarity index 51% rename from langfuse/api/ingestion/types/ingestion_error.py rename to langfuse/api/scores/types/create_score_batch_results.py index 2391d95eb..063291b9e 100644 --- a/langfuse/api/ingestion/types/ingestion_error.py +++ b/langfuse/api/scores/types/create_score_batch_results.py @@ -4,13 +4,17 @@ import pydantic from ...core.pydantic_utilities import UniversalBaseModel +from .create_score_batch_error import CreateScoreBatchError -class IngestionError(UniversalBaseModel): - id: str - status: int - message: typing.Optional[str] = None - error: typing.Optional[typing.Any] = None +class CreateScoreBatchResults(UniversalBaseModel): + """ + Acceptance counts and errors (HTTP 207). + """ + + accepted: int + rejected: int + errors: typing.List[CreateScoreBatchError] model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/scores/types/create_score_request.py b/langfuse/api/scores/types/create_score_request.py index 71c4d0ba3..a9453c452 100644 --- a/langfuse/api/scores/types/create_score_request.py +++ b/langfuse/api/scores/types/create_score_request.py @@ -1,5 +1,6 @@ # This file was auto-generated by Fern from our API Definition. +import datetime as dt import typing import pydantic @@ -76,6 +77,11 @@ class CreateScoreRequest(UniversalBaseModel): The source of the score. Defaults to API. Set to ANNOTATION to prefill scores (e.g. from an LLM) for a human reviewer to verify in an annotation queue. When source is ANNOTATION, a configId is required unless dataType is CORRECTION. EVAL is reserved for internal evaluator outputs and is not accepted on this endpoint. """ + timestamp: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + The timestamp of the score, as an ISO 8601 datetime with a timezone offset. Defaults to the time the request is received. To update an existing score, send it again with the same id and timestamp. + """ + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True ) diff --git a/langfuse/api/scores/types/create_scores_request.py b/langfuse/api/scores/types/create_scores_request.py new file mode 100644 index 000000000..3c11bdfc9 --- /dev/null +++ b/langfuse/api/scores/types/create_scores_request.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .create_score_request import CreateScoreRequest + +""" +from langfuse.commons import ScoreDataType +from langfuse.scores import CreateScoreRequest + +[ + CreateScoreRequest( + name="accuracy", + value=0.9, + trace_id="cdef-1234-5678-90ab", + ), + CreateScoreRequest( + name="helpfulness", + value=1.0, + data_type=ScoreDataType.BOOLEAN, + trace_id="cdef-1234-5678-90ab", + ), +] +""" +CreateScoresRequest = typing.List[CreateScoreRequest] diff --git a/langfuse/api/scores/types/create_scores_response.py b/langfuse/api/scores/types/create_scores_response.py new file mode 100644 index 000000000..7570cf33a --- /dev/null +++ b/langfuse/api/scores/types/create_scores_response.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .create_score_batch_response import CreateScoreBatchResponse +from .create_score_batch_results import CreateScoreBatchResults + +CreateScoresResponse = typing.Union[CreateScoreBatchResults, CreateScoreBatchResponse] diff --git a/langfuse/api/scores/types/get_scores_response.py b/langfuse/api/scores/types/get_scores_response.py deleted file mode 100644 index 70f9c7a51..000000000 --- a/langfuse/api/scores/types/get_scores_response.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...commons.types.deprecation import Deprecation -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from ...utils.pagination.types.meta_response import MetaResponse -from .get_scores_response_data import GetScoresResponseData - - -class GetScoresResponse(UniversalBaseModel): - data: typing.List[GetScoresResponseData] - meta: MetaResponse - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores/types/get_scores_response_data.py b/langfuse/api/scores/types/get_scores_response_data.py deleted file mode 100644 index 84b8abcc4..000000000 --- a/langfuse/api/scores/types/get_scores_response_data.py +++ /dev/null @@ -1,258 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import datetime as dt -import typing - -import pydantic -import typing_extensions -from ...commons.types.score_source import ScoreSource -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseData_Numeric(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["NUMERIC"], FieldMetadata(alias="dataType") - ] = "NUMERIC" - trace: typing.Optional[GetScoresResponseTraceData] = None - value: float - id: str - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = None - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - dataset_run_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="datasetRunId") - ] = None - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class GetScoresResponseData_Categorical(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["CATEGORICAL"], FieldMetadata(alias="dataType") - ] = "CATEGORICAL" - trace: typing.Optional[GetScoresResponseTraceData] = None - value: float - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = None - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - dataset_run_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="datasetRunId") - ] = None - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class GetScoresResponseData_Boolean(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["BOOLEAN"], FieldMetadata(alias="dataType") - ] = "BOOLEAN" - trace: typing.Optional[GetScoresResponseTraceData] = None - value: float - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = None - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - dataset_run_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="datasetRunId") - ] = None - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class GetScoresResponseData_Correction(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["CORRECTION"], FieldMetadata(alias="dataType") - ] = "CORRECTION" - trace: typing.Optional[GetScoresResponseTraceData] = None - value: float - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = None - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - dataset_run_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="datasetRunId") - ] = None - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -class GetScoresResponseData_Text(UniversalBaseModel): - data_type: typing_extensions.Annotated[ - typing.Literal["TEXT"], FieldMetadata(alias="dataType") - ] = "TEXT" - trace: typing.Optional[GetScoresResponseTraceData] = None - string_value: typing_extensions.Annotated[str, FieldMetadata(alias="stringValue")] - id: str - trace_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="traceId") - ] = None - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = None - observation_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="observationId") - ] = None - dataset_run_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="datasetRunId") - ] = None - name: str - source: ScoreSource - timestamp: dt.datetime - created_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="createdAt") - ] - updated_at: typing_extensions.Annotated[ - dt.datetime, FieldMetadata(alias="updatedAt") - ] - author_user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="authorUserId") - ] = None - comment: typing.Optional[str] = None - metadata: typing.Optional[typing.Dict[str, typing.Any]] = None - config_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="configId") - ] = None - queue_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="queueId") - ] = None - environment: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) - - -GetScoresResponseData = typing_extensions.Annotated[ - typing.Union[ - GetScoresResponseData_Numeric, - GetScoresResponseData_Categorical, - GetScoresResponseData_Boolean, - GetScoresResponseData_Correction, - GetScoresResponseData_Text, - ], - pydantic.Field(discriminator="data_type"), -] diff --git a/langfuse/api/scores/types/get_scores_response_data_boolean.py b/langfuse/api/scores/types/get_scores_response_data_boolean.py deleted file mode 100644 index eee645cd3..000000000 --- a/langfuse/api/scores/types/get_scores_response_data_boolean.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...commons.types.boolean_score import BooleanScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataBoolean(BooleanScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores/types/get_scores_response_data_categorical.py b/langfuse/api/scores/types/get_scores_response_data_categorical.py deleted file mode 100644 index edd4e46fa..000000000 --- a/langfuse/api/scores/types/get_scores_response_data_categorical.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...commons.types.categorical_score import CategoricalScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataCategorical(CategoricalScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores/types/get_scores_response_data_correction.py b/langfuse/api/scores/types/get_scores_response_data_correction.py deleted file mode 100644 index df39c65ed..000000000 --- a/langfuse/api/scores/types/get_scores_response_data_correction.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...commons.types.correction_score import CorrectionScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataCorrection(CorrectionScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores/types/get_scores_response_data_numeric.py b/langfuse/api/scores/types/get_scores_response_data_numeric.py deleted file mode 100644 index 350be0e0c..000000000 --- a/langfuse/api/scores/types/get_scores_response_data_numeric.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...commons.types.numeric_score import NumericScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataNumeric(NumericScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores/types/get_scores_response_data_text.py b/langfuse/api/scores/types/get_scores_response_data_text.py deleted file mode 100644 index 4949afe74..000000000 --- a/langfuse/api/scores/types/get_scores_response_data_text.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...commons.types.text_score import TextScore -from .get_scores_response_trace_data import GetScoresResponseTraceData - - -class GetScoresResponseDataText(TextScore): - trace: typing.Optional[GetScoresResponseTraceData] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores/types/get_scores_response_trace_data.py b/langfuse/api/scores/types/get_scores_response_trace_data.py deleted file mode 100644 index 306aaaf78..000000000 --- a/langfuse/api/scores/types/get_scores_response_trace_data.py +++ /dev/null @@ -1,38 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata - - -class GetScoresResponseTraceData(UniversalBaseModel): - user_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="userId") - ] = pydantic.Field(default=None) - """ - The user ID associated with the trace referenced by score - """ - - tags: typing.Optional[typing.List[str]] = pydantic.Field(default=None) - """ - A list of tags associated with the trace referenced by score - """ - - environment: typing.Optional[str] = pydantic.Field(default=None) - """ - The environment of the trace referenced by score - """ - - session_id: typing_extensions.Annotated[ - typing.Optional[str], FieldMetadata(alias="sessionId") - ] = pydantic.Field(default=None) - """ - The session ID associated with the trace referenced by score - """ - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/scores_v3/types/get_scores_v3meta.py b/langfuse/api/scores/types/get_scores_v3meta.py similarity index 100% rename from langfuse/api/scores_v3/types/get_scores_v3meta.py rename to langfuse/api/scores/types/get_scores_v3meta.py diff --git a/langfuse/api/scores_v3/types/get_scores_v3response.py b/langfuse/api/scores/types/get_scores_v3response.py similarity index 100% rename from langfuse/api/scores_v3/types/get_scores_v3response.py rename to langfuse/api/scores/types/get_scores_v3response.py diff --git a/langfuse/api/scores_v3/types/numeric_score_v3.py b/langfuse/api/scores/types/numeric_score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/numeric_score_v3.py rename to langfuse/api/scores/types/numeric_score_v3.py diff --git a/langfuse/api/ingestion/types/score_body.py b/langfuse/api/scores/types/score_body.py similarity index 98% rename from langfuse/api/ingestion/types/score_body.py rename to langfuse/api/scores/types/score_body.py index d9a32b6a7..ec10ba7cf 100644 --- a/langfuse/api/ingestion/types/score_body.py +++ b/langfuse/api/scores/types/score_body.py @@ -14,7 +14,7 @@ class ScoreBody(UniversalBaseModel): """ Examples -------- - from langfuse.ingestion import ScoreBody + from langfuse.scores import ScoreBody ScoreBody( name="novelty", diff --git a/langfuse/api/scores_v3/types/score_subject_experiment_v3.py b/langfuse/api/scores/types/score_subject_experiment_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/score_subject_experiment_v3.py rename to langfuse/api/scores/types/score_subject_experiment_v3.py diff --git a/langfuse/api/scores_v3/types/score_subject_observation_v3.py b/langfuse/api/scores/types/score_subject_observation_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/score_subject_observation_v3.py rename to langfuse/api/scores/types/score_subject_observation_v3.py diff --git a/langfuse/api/scores_v3/types/score_subject_session_v3.py b/langfuse/api/scores/types/score_subject_session_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/score_subject_session_v3.py rename to langfuse/api/scores/types/score_subject_session_v3.py diff --git a/langfuse/api/scores_v3/types/score_subject_trace_v3.py b/langfuse/api/scores/types/score_subject_trace_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/score_subject_trace_v3.py rename to langfuse/api/scores/types/score_subject_trace_v3.py diff --git a/langfuse/api/scores_v3/types/score_subject_v3.py b/langfuse/api/scores/types/score_subject_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/score_subject_v3.py rename to langfuse/api/scores/types/score_subject_v3.py diff --git a/langfuse/api/scores_v3/types/score_v3.py b/langfuse/api/scores/types/score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/score_v3.py rename to langfuse/api/scores/types/score_v3.py diff --git a/langfuse/api/scores_v3/types/text_score_v3.py b/langfuse/api/scores/types/text_score_v3.py similarity index 100% rename from langfuse/api/scores_v3/types/text_score_v3.py rename to langfuse/api/scores/types/text_score_v3.py diff --git a/langfuse/api/scores_v3/__init__.py b/langfuse/api/scores_v3/__init__.py deleted file mode 100644 index 855868335..000000000 --- a/langfuse/api/scores_v3/__init__.py +++ /dev/null @@ -1,112 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import ( - BaseScoreV3, - BooleanScoreV3, - CategoricalScoreV3, - CorrectionScoreV3, - GetScoresV3Meta, - GetScoresV3Response, - NumericScoreV3, - ScoreSubjectExperimentV3, - ScoreSubjectObservationV3, - ScoreSubjectSessionV3, - ScoreSubjectTraceV3, - ScoreSubjectV3, - ScoreSubjectV3_Experiment, - ScoreSubjectV3_Observation, - ScoreSubjectV3_Session, - ScoreSubjectV3_Trace, - ScoreV3, - ScoreV3_Boolean, - ScoreV3_Categorical, - ScoreV3_Correction, - ScoreV3_Numeric, - ScoreV3_Text, - TextScoreV3, - ) -_dynamic_imports: typing.Dict[str, str] = { - "BaseScoreV3": ".types", - "BooleanScoreV3": ".types", - "CategoricalScoreV3": ".types", - "CorrectionScoreV3": ".types", - "GetScoresV3Meta": ".types", - "GetScoresV3Response": ".types", - "NumericScoreV3": ".types", - "ScoreSubjectExperimentV3": ".types", - "ScoreSubjectObservationV3": ".types", - "ScoreSubjectSessionV3": ".types", - "ScoreSubjectTraceV3": ".types", - "ScoreSubjectV3": ".types", - "ScoreSubjectV3_Experiment": ".types", - "ScoreSubjectV3_Observation": ".types", - "ScoreSubjectV3_Session": ".types", - "ScoreSubjectV3_Trace": ".types", - "ScoreV3": ".types", - "ScoreV3_Boolean": ".types", - "ScoreV3_Categorical": ".types", - "ScoreV3_Correction": ".types", - "ScoreV3_Numeric": ".types", - "ScoreV3_Text": ".types", - "TextScoreV3": ".types", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "BaseScoreV3", - "BooleanScoreV3", - "CategoricalScoreV3", - "CorrectionScoreV3", - "GetScoresV3Meta", - "GetScoresV3Response", - "NumericScoreV3", - "ScoreSubjectExperimentV3", - "ScoreSubjectObservationV3", - "ScoreSubjectSessionV3", - "ScoreSubjectTraceV3", - "ScoreSubjectV3", - "ScoreSubjectV3_Experiment", - "ScoreSubjectV3_Observation", - "ScoreSubjectV3_Session", - "ScoreSubjectV3_Trace", - "ScoreV3", - "ScoreV3_Boolean", - "ScoreV3_Categorical", - "ScoreV3_Correction", - "ScoreV3_Numeric", - "ScoreV3_Text", - "TextScoreV3", -] diff --git a/langfuse/api/scores_v3/client.py b/langfuse/api/scores_v3/client.py deleted file mode 100644 index d5afda7dd..000000000 --- a/langfuse/api/scores_v3/client.py +++ /dev/null @@ -1,355 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from .raw_client import AsyncRawScoresV3Client, RawScoresV3Client -from .types.get_scores_v3response import GetScoresV3Response - - -class ScoresV3Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawScoresV3Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawScoresV3Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawScoresV3Client - """ - return self._raw_client - - def get_many_v3( - self, - *, - limit: typing.Optional[int] = None, - cursor: typing.Optional[str] = None, - fields: typing.Optional[str] = None, - id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - source: typing.Optional[str] = None, - data_type: typing.Optional[str] = None, - environment: typing.Optional[str] = None, - config_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - author_user_id: typing.Optional[str] = None, - value: typing.Optional[str] = None, - value_min: typing.Optional[float] = None, - value_max: typing.Optional[float] = None, - trace_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - observation_id: typing.Optional[str] = None, - experiment_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetScoresV3Response: - """ - Get a list of scores with a polymorphic `value` field (v3). - - The `value` field type depends on `dataType`: - - `NUMERIC` → number - - `BOOLEAN` → boolean - - `CATEGORICAL`, `TEXT`, `CORRECTION` → string - - The response always includes the core fields: id, projectId, name, - value, dataType, source, timestamp, environment, createdAt, updatedAt. - - Additional field groups can be requested via the `fields` parameter: - - `details` — adds comment, configId, metadata - - `subject` — adds the subject object describing the entity the score - is attached to: kind (trace, observation, session, or experiment), - id, and traceId for observation-level scores - - `annotation` — adds authorUserId, queueId - - Unknown group names return HTTP 400. - - Parameters - ---------- - limit : typing.Optional[int] - Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. - - cursor : typing.Optional[str] - URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. - - fields : typing.Optional[str] - Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. - - id : typing.Optional[str] - Comma-separated list of score IDs to filter by (OR within, AND across filters). - - name : typing.Optional[str] - Comma-separated list of score names to filter by. - - source : typing.Optional[str] - Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. - - data_type : typing.Optional[str] - Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. - - environment : typing.Optional[str] - Comma-separated list of environments to filter by. - - config_id : typing.Optional[str] - Comma-separated list of score config IDs to filter by. - - queue_id : typing.Optional[str] - Comma-separated list of annotation queue IDs to filter by. - - author_user_id : typing.Optional[str] - Comma-separated list of author user IDs to filter by. - - value : typing.Optional[str] - Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. - - value_min : typing.Optional[float] - Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - value_max : typing.Optional[float] - Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - trace_id : typing.Optional[str] - Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. - - session_id : typing.Optional[str] - Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. - - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. - - experiment_id : typing.Optional[str] - Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. - - from_timestamp : typing.Optional[dt.datetime] - Inclusive lower bound on the score timestamp. - - to_timestamp : typing.Optional[dt.datetime] - Exclusive upper bound on the score timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetScoresV3Response - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.scores_v3.get_many_v3() - """ - _response = self._raw_client.get_many_v3( - limit=limit, - cursor=cursor, - fields=fields, - id=id, - name=name, - source=source, - data_type=data_type, - environment=environment, - config_id=config_id, - queue_id=queue_id, - author_user_id=author_user_id, - value=value, - value_min=value_min, - value_max=value_max, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - experiment_id=experiment_id, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - request_options=request_options, - ) - return _response.data - - -class AsyncScoresV3Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawScoresV3Client(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawScoresV3Client: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawScoresV3Client - """ - return self._raw_client - - async def get_many_v3( - self, - *, - limit: typing.Optional[int] = None, - cursor: typing.Optional[str] = None, - fields: typing.Optional[str] = None, - id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - source: typing.Optional[str] = None, - data_type: typing.Optional[str] = None, - environment: typing.Optional[str] = None, - config_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - author_user_id: typing.Optional[str] = None, - value: typing.Optional[str] = None, - value_min: typing.Optional[float] = None, - value_max: typing.Optional[float] = None, - trace_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - observation_id: typing.Optional[str] = None, - experiment_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> GetScoresV3Response: - """ - Get a list of scores with a polymorphic `value` field (v3). - - The `value` field type depends on `dataType`: - - `NUMERIC` → number - - `BOOLEAN` → boolean - - `CATEGORICAL`, `TEXT`, `CORRECTION` → string - - The response always includes the core fields: id, projectId, name, - value, dataType, source, timestamp, environment, createdAt, updatedAt. - - Additional field groups can be requested via the `fields` parameter: - - `details` — adds comment, configId, metadata - - `subject` — adds the subject object describing the entity the score - is attached to: kind (trace, observation, session, or experiment), - id, and traceId for observation-level scores - - `annotation` — adds authorUserId, queueId - - Unknown group names return HTTP 400. - - Parameters - ---------- - limit : typing.Optional[int] - Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. - - cursor : typing.Optional[str] - URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. - - fields : typing.Optional[str] - Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. - - id : typing.Optional[str] - Comma-separated list of score IDs to filter by (OR within, AND across filters). - - name : typing.Optional[str] - Comma-separated list of score names to filter by. - - source : typing.Optional[str] - Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. - - data_type : typing.Optional[str] - Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. - - environment : typing.Optional[str] - Comma-separated list of environments to filter by. - - config_id : typing.Optional[str] - Comma-separated list of score config IDs to filter by. - - queue_id : typing.Optional[str] - Comma-separated list of annotation queue IDs to filter by. - - author_user_id : typing.Optional[str] - Comma-separated list of author user IDs to filter by. - - value : typing.Optional[str] - Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. - - value_min : typing.Optional[float] - Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - value_max : typing.Optional[float] - Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - trace_id : typing.Optional[str] - Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. - - session_id : typing.Optional[str] - Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. - - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. - - experiment_id : typing.Optional[str] - Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. - - from_timestamp : typing.Optional[dt.datetime] - Inclusive lower bound on the score timestamp. - - to_timestamp : typing.Optional[dt.datetime] - Exclusive upper bound on the score timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GetScoresV3Response - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.scores_v3.get_many_v3() - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_many_v3( - limit=limit, - cursor=cursor, - fields=fields, - id=id, - name=name, - source=source, - data_type=data_type, - environment=environment, - config_id=config_id, - queue_id=queue_id, - author_user_id=author_user_id, - value=value, - value_min=value_min, - value_max=value_max, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - experiment_id=experiment_id, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - request_options=request_options, - ) - return _response.data diff --git a/langfuse/api/scores_v3/raw_client.py b/langfuse/api/scores_v3/raw_client.py deleted file mode 100644 index cc0793172..000000000 --- a/langfuse/api/scores_v3/raw_client.py +++ /dev/null @@ -1,474 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.datetime_utils import serialize_datetime -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from .types.get_scores_v3response import GetScoresV3Response - - -class RawScoresV3Client: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get_many_v3( - self, - *, - limit: typing.Optional[int] = None, - cursor: typing.Optional[str] = None, - fields: typing.Optional[str] = None, - id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - source: typing.Optional[str] = None, - data_type: typing.Optional[str] = None, - environment: typing.Optional[str] = None, - config_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - author_user_id: typing.Optional[str] = None, - value: typing.Optional[str] = None, - value_min: typing.Optional[float] = None, - value_max: typing.Optional[float] = None, - trace_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - observation_id: typing.Optional[str] = None, - experiment_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[GetScoresV3Response]: - """ - Get a list of scores with a polymorphic `value` field (v3). - - The `value` field type depends on `dataType`: - - `NUMERIC` → number - - `BOOLEAN` → boolean - - `CATEGORICAL`, `TEXT`, `CORRECTION` → string - - The response always includes the core fields: id, projectId, name, - value, dataType, source, timestamp, environment, createdAt, updatedAt. - - Additional field groups can be requested via the `fields` parameter: - - `details` — adds comment, configId, metadata - - `subject` — adds the subject object describing the entity the score - is attached to: kind (trace, observation, session, or experiment), - id, and traceId for observation-level scores - - `annotation` — adds authorUserId, queueId - - Unknown group names return HTTP 400. - - Parameters - ---------- - limit : typing.Optional[int] - Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. - - cursor : typing.Optional[str] - URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. - - fields : typing.Optional[str] - Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. - - id : typing.Optional[str] - Comma-separated list of score IDs to filter by (OR within, AND across filters). - - name : typing.Optional[str] - Comma-separated list of score names to filter by. - - source : typing.Optional[str] - Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. - - data_type : typing.Optional[str] - Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. - - environment : typing.Optional[str] - Comma-separated list of environments to filter by. - - config_id : typing.Optional[str] - Comma-separated list of score config IDs to filter by. - - queue_id : typing.Optional[str] - Comma-separated list of annotation queue IDs to filter by. - - author_user_id : typing.Optional[str] - Comma-separated list of author user IDs to filter by. - - value : typing.Optional[str] - Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. - - value_min : typing.Optional[float] - Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - value_max : typing.Optional[float] - Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - trace_id : typing.Optional[str] - Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. - - session_id : typing.Optional[str] - Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. - - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. - - experiment_id : typing.Optional[str] - Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. - - from_timestamp : typing.Optional[dt.datetime] - Inclusive lower bound on the score timestamp. - - to_timestamp : typing.Optional[dt.datetime] - Exclusive upper bound on the score timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GetScoresV3Response] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/v3/scores", - method="GET", - params={ - "limit": limit, - "cursor": cursor, - "fields": fields, - "id": id, - "name": name, - "source": source, - "dataType": data_type, - "environment": environment, - "configId": config_id, - "queueId": queue_id, - "authorUserId": author_user_id, - "value": value, - "valueMin": value_min, - "valueMax": value_max, - "traceId": trace_id, - "sessionId": session_id, - "observationId": observation_id, - "experimentId": experiment_id, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GetScoresV3Response, - parse_obj_as( - type_=GetScoresV3Response, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawScoresV3Client: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get_many_v3( - self, - *, - limit: typing.Optional[int] = None, - cursor: typing.Optional[str] = None, - fields: typing.Optional[str] = None, - id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - source: typing.Optional[str] = None, - data_type: typing.Optional[str] = None, - environment: typing.Optional[str] = None, - config_id: typing.Optional[str] = None, - queue_id: typing.Optional[str] = None, - author_user_id: typing.Optional[str] = None, - value: typing.Optional[str] = None, - value_min: typing.Optional[float] = None, - value_max: typing.Optional[float] = None, - trace_id: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - observation_id: typing.Optional[str] = None, - experiment_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[GetScoresV3Response]: - """ - Get a list of scores with a polymorphic `value` field (v3). - - The `value` field type depends on `dataType`: - - `NUMERIC` → number - - `BOOLEAN` → boolean - - `CATEGORICAL`, `TEXT`, `CORRECTION` → string - - The response always includes the core fields: id, projectId, name, - value, dataType, source, timestamp, environment, createdAt, updatedAt. - - Additional field groups can be requested via the `fields` parameter: - - `details` — adds comment, configId, metadata - - `subject` — adds the subject object describing the entity the score - is attached to: kind (trace, observation, session, or experiment), - id, and traceId for observation-level scores - - `annotation` — adds authorUserId, queueId - - Unknown group names return HTTP 400. - - Parameters - ---------- - limit : typing.Optional[int] - Number of items per page. Maximum 100, default 50. Requests with a limit greater than 100 return HTTP 400. - - cursor : typing.Optional[str] - URL-safe base64 (base64url) cursor for pagination. Use the cursor from the previous response to get the next page. Absent on the final page. - - fields : typing.Optional[str] - Comma-separated field groups to include in addition to the always-returned core fields. Allowed: details, subject, annotation — see the endpoint description for the fields each group adds. Unknown names return HTTP 400. - - id : typing.Optional[str] - Comma-separated list of score IDs to filter by (OR within, AND across filters). - - name : typing.Optional[str] - Comma-separated list of score names to filter by. - - source : typing.Optional[str] - Comma-separated list of score sources to filter by (e.g. API, ANNOTATION, EVAL). Case-insensitive — `api` and `API` are equivalent. - - data_type : typing.Optional[str] - Comma-separated list of data types to filter by (NUMERIC, BOOLEAN, CATEGORICAL, TEXT, CORRECTION). Case-insensitive — `numeric` and `NUMERIC` are equivalent. Must be a single value when used with value, valueMin, or valueMax; otherwise the request returns HTTP 400. Must be NUMERIC when used with valueMin or valueMax. - - environment : typing.Optional[str] - Comma-separated list of environments to filter by. - - config_id : typing.Optional[str] - Comma-separated list of score config IDs to filter by. - - queue_id : typing.Optional[str] - Comma-separated list of annotation queue IDs to filter by. - - author_user_id : typing.Optional[str] - Comma-separated list of author user IDs to filter by. - - value : typing.Optional[str] - Comma-separated list of exact values to filter by. Requires a single dataType from NUMERIC, BOOLEAN, or CATEGORICAL; any other dataType, multiple dataTypes, or omitting dataType returns HTTP 400. For BOOLEAN, each value must be "true" or "false"; for NUMERIC, each value must be a finite number. Otherwise the request returns HTTP 400. - - value_min : typing.Optional[float] - Inclusive lower bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - value_max : typing.Optional[float] - Inclusive upper bound on the numeric value. Requires dataType=NUMERIC as a single value; otherwise the request returns HTTP 400. - - trace_id : typing.Optional[str] - Comma-separated list of trace IDs to filter by. Mutually exclusive with sessionId, experimentId. May be combined with observationId to scope the observation lookup to a specific trace. - - session_id : typing.Optional[str] - Comma-separated list of session IDs to filter by. Mutually exclusive with traceId, observationId, experimentId. - - observation_id : typing.Optional[str] - Comma-separated list of observation IDs to filter by. Requires traceId to be specified, because observation IDs are scoped to a trace. Mutually exclusive with sessionId, experimentId. Returns HTTP 400 when used without traceId. - - experiment_id : typing.Optional[str] - Comma-separated list of dataset run IDs (experiment IDs) to filter by. Mutually exclusive with traceId, sessionId, observationId. - - from_timestamp : typing.Optional[dt.datetime] - Inclusive lower bound on the score timestamp. - - to_timestamp : typing.Optional[dt.datetime] - Exclusive upper bound on the score timestamp. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GetScoresV3Response] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/v3/scores", - method="GET", - params={ - "limit": limit, - "cursor": cursor, - "fields": fields, - "id": id, - "name": name, - "source": source, - "dataType": data_type, - "environment": environment, - "configId": config_id, - "queueId": queue_id, - "authorUserId": author_user_id, - "value": value, - "valueMin": value_min, - "valueMax": value_max, - "traceId": trace_id, - "sessionId": session_id, - "observationId": observation_id, - "experimentId": experiment_id, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GetScoresV3Response, - parse_obj_as( - type_=GetScoresV3Response, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/scores_v3/types/__init__.py b/langfuse/api/scores_v3/types/__init__.py deleted file mode 100644 index 14da0ca73..000000000 --- a/langfuse/api/scores_v3/types/__init__.py +++ /dev/null @@ -1,114 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .base_score_v3 import BaseScoreV3 - from .boolean_score_v3 import BooleanScoreV3 - from .categorical_score_v3 import CategoricalScoreV3 - from .correction_score_v3 import CorrectionScoreV3 - from .get_scores_v3meta import GetScoresV3Meta - from .get_scores_v3response import GetScoresV3Response - from .numeric_score_v3 import NumericScoreV3 - from .score_subject_experiment_v3 import ScoreSubjectExperimentV3 - from .score_subject_observation_v3 import ScoreSubjectObservationV3 - from .score_subject_session_v3 import ScoreSubjectSessionV3 - from .score_subject_trace_v3 import ScoreSubjectTraceV3 - from .score_subject_v3 import ( - ScoreSubjectV3, - ScoreSubjectV3_Experiment, - ScoreSubjectV3_Observation, - ScoreSubjectV3_Session, - ScoreSubjectV3_Trace, - ) - from .score_v3 import ( - ScoreV3, - ScoreV3_Boolean, - ScoreV3_Categorical, - ScoreV3_Correction, - ScoreV3_Numeric, - ScoreV3_Text, - ) - from .text_score_v3 import TextScoreV3 -_dynamic_imports: typing.Dict[str, str] = { - "BaseScoreV3": ".base_score_v3", - "BooleanScoreV3": ".boolean_score_v3", - "CategoricalScoreV3": ".categorical_score_v3", - "CorrectionScoreV3": ".correction_score_v3", - "GetScoresV3Meta": ".get_scores_v3meta", - "GetScoresV3Response": ".get_scores_v3response", - "NumericScoreV3": ".numeric_score_v3", - "ScoreSubjectExperimentV3": ".score_subject_experiment_v3", - "ScoreSubjectObservationV3": ".score_subject_observation_v3", - "ScoreSubjectSessionV3": ".score_subject_session_v3", - "ScoreSubjectTraceV3": ".score_subject_trace_v3", - "ScoreSubjectV3": ".score_subject_v3", - "ScoreSubjectV3_Experiment": ".score_subject_v3", - "ScoreSubjectV3_Observation": ".score_subject_v3", - "ScoreSubjectV3_Session": ".score_subject_v3", - "ScoreSubjectV3_Trace": ".score_subject_v3", - "ScoreV3": ".score_v3", - "ScoreV3_Boolean": ".score_v3", - "ScoreV3_Categorical": ".score_v3", - "ScoreV3_Correction": ".score_v3", - "ScoreV3_Numeric": ".score_v3", - "ScoreV3_Text": ".score_v3", - "TextScoreV3": ".text_score_v3", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "BaseScoreV3", - "BooleanScoreV3", - "CategoricalScoreV3", - "CorrectionScoreV3", - "GetScoresV3Meta", - "GetScoresV3Response", - "NumericScoreV3", - "ScoreSubjectExperimentV3", - "ScoreSubjectObservationV3", - "ScoreSubjectSessionV3", - "ScoreSubjectTraceV3", - "ScoreSubjectV3", - "ScoreSubjectV3_Experiment", - "ScoreSubjectV3_Observation", - "ScoreSubjectV3_Session", - "ScoreSubjectV3_Trace", - "ScoreV3", - "ScoreV3_Boolean", - "ScoreV3_Categorical", - "ScoreV3_Correction", - "ScoreV3_Numeric", - "ScoreV3_Text", - "TextScoreV3", -] diff --git a/langfuse/api/sessions/__init__.py b/langfuse/api/sessions/__init__.py deleted file mode 100644 index 89b7e63bc..000000000 --- a/langfuse/api/sessions/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import PaginatedSessions -_dynamic_imports: typing.Dict[str, str] = {"PaginatedSessions": ".types"} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["PaginatedSessions"] diff --git a/langfuse/api/sessions/client.py b/langfuse/api/sessions/client.py deleted file mode 100644 index d485c29ed..000000000 --- a/langfuse/api/sessions/client.py +++ /dev/null @@ -1,299 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import typing_extensions -from ..commons.types.session_with_traces import SessionWithTraces -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from .raw_client import AsyncRawSessionsClient, RawSessionsClient -from .types.paginated_sessions import PaginatedSessions - - -class SessionsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawSessionsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawSessionsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawSessionsClient - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedSessions: - """ - Get sessions. - - This legacy endpoint is not recommended for new data extraction workflows. - Use the v2 observations endpoint with a bounded time range and group rows by - `sessionId` instead: - `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for sessions where the environment is one of the provided values. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedSessions - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.sessions.list() - """ - _response = self._raw_client.list( - page=page, - limit=limit, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - environment=environment, - request_options=request_options, - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get( - self, - session_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> SessionWithTraces: - """ - Get a session. - - Please note that `traces` on this endpoint are not paginated. For large - sessions or new data extraction workflows, use the v2 observations endpoint - with a URL-encoded `sessionId` filter and a bounded time range: - `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. - - Parameters - ---------- - session_id : str - The unique id of a session - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SessionWithTraces - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.sessions.get( - session_id="sessionId", - ) - """ - _response = self._raw_client.get(session_id, request_options=request_options) - return _response.data - - -class AsyncSessionsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawSessionsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawSessionsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawSessionsClient - """ - return self._raw_client - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> PaginatedSessions: - """ - Get sessions. - - This legacy endpoint is not recommended for new data extraction workflows. - Use the v2 observations endpoint with a bounded time range and group rows by - `sessionId` instead: - `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for sessions where the environment is one of the provided values. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PaginatedSessions - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.sessions.list() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list( - page=page, - limit=limit, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - environment=environment, - request_options=request_options, - ) - return _response.data - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get( - self, - session_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> SessionWithTraces: - """ - Get a session. - - Please note that `traces` on this endpoint are not paginated. For large - sessions or new data extraction workflows, use the v2 observations endpoint - with a URL-encoded `sessionId` filter and a bounded time range: - `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. - - Parameters - ---------- - session_id : str - The unique id of a session - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SessionWithTraces - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.sessions.get( - session_id="sessionId", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get( - session_id, request_options=request_options - ) - return _response.data diff --git a/langfuse/api/sessions/raw_client.py b/langfuse/api/sessions/raw_client.py deleted file mode 100644 index 9e27d41e4..000000000 --- a/langfuse/api/sessions/raw_client.py +++ /dev/null @@ -1,537 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -import typing_extensions -from json.decoder import JSONDecodeError - -from ..commons.errors.access_denied_error import AccessDeniedError -from ..commons.errors.error import Error -from ..commons.errors.method_not_allowed_error import MethodNotAllowedError -from ..commons.errors.not_found_error import NotFoundError -from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.session_with_traces import SessionWithTraces -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.datetime_utils import serialize_datetime -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import jsonable_encoder -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from .types.paginated_sessions import PaginatedSessions - - -class RawSessionsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[PaginatedSessions]: - """ - Get sessions. - - This legacy endpoint is not recommended for new data extraction workflows. - Use the v2 observations endpoint with a bounded time range and group rows by - `sessionId` instead: - `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for sessions where the environment is one of the provided values. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[PaginatedSessions] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/sessions", - method="GET", - params={ - "page": page, - "limit": limit, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PaginatedSessions, - parse_obj_as( - type_=PaginatedSessions, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get( - self, - session_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[SessionWithTraces]: - """ - Get a session. - - Please note that `traces` on this endpoint are not paginated. For large - sessions or new data extraction workflows, use the v2 observations endpoint - with a URL-encoded `sessionId` filter and a bounded time range: - `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. - - Parameters - ---------- - session_id : str - The unique id of a session - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[SessionWithTraces] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/sessions/{jsonable_encoder(session_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - SessionWithTraces, - parse_obj_as( - type_=SessionWithTraces, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - -class AsyncRawSessionsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[PaginatedSessions]: - """ - Get sessions. - - This legacy endpoint is not recommended for new data extraction workflows. - Use the v2 observations endpoint with a bounded time range and group rows by - `sessionId` instead: - `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include sessions created before a certain datetime (ISO 8601) - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for sessions where the environment is one of the provided values. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[PaginatedSessions] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/sessions", - method="GET", - params={ - "page": page, - "limit": limit, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "environment": environment, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PaginatedSessions, - parse_obj_as( - type_=PaginatedSessions, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read session data via `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get( - self, - session_id: str, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[SessionWithTraces]: - """ - Get a session. - - Please note that `traces` on this endpoint are not paginated. For large - sessions or new data extraction workflows, use the v2 observations endpoint - with a URL-encoded `sessionId` filter and a bounded time range: - `GET /api/public/v2/observations?filter=&fromStartTime=&toStartTime=`. - - Parameters - ---------- - session_id : str - The unique id of a session - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[SessionWithTraces] - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/sessions/{jsonable_encoder(session_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - SessionWithTraces, - parse_obj_as( - type_=SessionWithTraces, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) diff --git a/langfuse/api/sessions/types/__init__.py b/langfuse/api/sessions/types/__init__.py deleted file mode 100644 index 45f21e139..000000000 --- a/langfuse/api/sessions/types/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .paginated_sessions import PaginatedSessions -_dynamic_imports: typing.Dict[str, str] = {"PaginatedSessions": ".paginated_sessions"} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError( - f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" - ) - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError( - f"Failed to import {attr_name} from {module_name}: {e}" - ) from e - except AttributeError as e: - raise AttributeError( - f"Failed to get {attr_name} from {module_name}: {e}" - ) from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["PaginatedSessions"] diff --git a/langfuse/api/sessions/types/paginated_sessions.py b/langfuse/api/sessions/types/paginated_sessions.py deleted file mode 100644 index cc1ef68b3..000000000 --- a/langfuse/api/sessions/types/paginated_sessions.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...commons.types.deprecation import Deprecation -from ...commons.types.session import Session -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from ...utils.pagination.types.meta_response import MetaResponse - - -class PaginatedSessions(UniversalBaseModel): - data: typing.List[Session] - meta: MetaResponse - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/trace/__init__.py b/langfuse/api/trace/__init__.py index fc9889dfa..37c2eb267 100644 --- a/langfuse/api/trace/__init__.py +++ b/langfuse/api/trace/__init__.py @@ -6,12 +6,8 @@ from importlib import import_module if typing.TYPE_CHECKING: - from .types import DeleteTraceResponse, Sort, Traces -_dynamic_imports: typing.Dict[str, str] = { - "DeleteTraceResponse": ".types", - "Sort": ".types", - "Traces": ".types", -} + from .types import DeleteTraceResponse +_dynamic_imports: typing.Dict[str, str] = {"DeleteTraceResponse": ".types"} def __getattr__(attr_name: str) -> typing.Any: @@ -41,4 +37,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["DeleteTraceResponse", "Sort", "Traces"] +__all__ = ["DeleteTraceResponse"] diff --git a/langfuse/api/trace/client.py b/langfuse/api/trace/client.py index 5674177c1..406f838c1 100644 --- a/langfuse/api/trace/client.py +++ b/langfuse/api/trace/client.py @@ -1,15 +1,11 @@ # This file was auto-generated by Fern from our API Definition. -import datetime as dt import typing -import typing_extensions -from ..commons.types.trace_with_full_details import TraceWithFullDetails from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper from ..core.request_options import RequestOptions from .raw_client import AsyncRawTraceClient, RawTraceClient from .types.delete_trace_response import DeleteTraceResponse -from .types.traces import Traces # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -30,56 +26,6 @@ def with_raw_response(self) -> RawTraceClient: """ return self._raw_client - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get( - self, - trace_id: str, - *, - fields: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> TraceWithFullDetails: - """ - Get a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of a trace - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TraceWithFullDetails - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.trace.get( - trace_id="traceId", - ) - """ - _response = self._raw_client.get( - trace_id, fields=fields, request_options=request_options - ) - return _response.data - def delete( self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> DeleteTraceResponse: @@ -117,219 +63,6 @@ def delete( _response = self._raw_client.delete(trace_id, request_options=request_options) return _response.data - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - order_by: typing.Optional[str] = None, - tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - version: typing.Optional[str] = None, - release: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> Traces: - """ - Get list of traces - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - - name : typing.Optional[str] - - session_id : typing.Optional[str] - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - - order_by : typing.Optional[str] - Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - - tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only traces that include all of these tags will be returned. - - version : typing.Optional[str] - Optional filter to only include traces with a certain version. - - release : typing.Optional[str] - Optional filter to only include traces with a certain release. - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for traces where the environment is one of the provided values. - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "booleanObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - booleanObject: "=", "<>" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, booleanObject, and categoryOptions types when filtering on nested fields like metadata or score names - } - ] - ``` - - ## Available Columns - - ### Core Trace Fields - - `id` (string) - Trace ID - - `name` (string) - Trace name - - `timestamp` (datetime) - Trace timestamp - - `userId` (string) - User ID - - `sessionId` (string) - Session ID - - `environment` (string) - Environment tag - - `version` (string) - Version tag - - `release` (string) - Release tag - - `tags` (arrayOptions) - Array of tags - - `bookmarked` (boolean) - Bookmark status - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Aggregated Metrics (from observations) - These metrics are aggregated from all observations within the trace: - - `latency` (number) - Latency in seconds (time from first observation start to last observation end) - - `inputTokens` (number) - Total input tokens across all observations - - `outputTokens` (number) - Total output tokens across all observations - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - `inputCost` (number) - Total input cost in USD - - `outputCost` (number) - Total output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Observation Level Aggregations - These fields aggregate observation levels within the trace: - - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) - - `warningCount` (number) - Count of WARNING level observations - - `errorCount` (number) - Count of ERROR level observations - - `defaultCount` (number) - Count of DEFAULT level observations - - `debugCount` (number) - Count of DEBUG level observations - - ### Scores (requires join with scores table) - - `scores_avg` (number) - Average of numeric scores (alias: `scores`) - - `score_categories` (categoryOptions) - Categorical score values - - `score_booleans` (booleanObject) - Boolean score values. Use `key` for the score name and a boolean `value`, e.g. `{"type": "booleanObject", "column": "score_booleans", "key": "is_correct", "operator": "=", "value": true}`. The `<>` operator also matches traces without a score of that name. - - ## Filter Examples - ```json - [ - { - "type": "datetime", - "column": "timestamp", - "operator": ">=", - "value": "2024-01-01T00:00:00Z" - }, - { - "type": "string", - "column": "userId", - "operator": "=", - "value": "user-123" - }, - { - "type": "number", - "column": "totalCost", - "operator": ">=", - "value": 0.01 - }, - { - "type": "arrayOptions", - "column": "tags", - "operator": "all of", - "value": ["production", "critical"] - }, - { - "type": "stringObject", - "column": "metadata", - "key": "customer_tier", - "operator": "=", - "value": "enterprise" - } - ] - ``` - - ## Performance Notes - - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance - - Score filters require a join with the scores table and may impact query performance - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Traces - - Examples - -------- - from langfuse import LangfuseAPI - - client = LangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - client.trace.list() - """ - _response = self._raw_client.list( - page=page, - limit=limit, - user_id=user_id, - name=name, - session_id=session_id, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - order_by=order_by, - tags=tags, - version=version, - release=release, - environment=environment, - fields=fields, - filter=filter, - request_options=request_options, - ) - return _response.data - def delete_multiple( self, *, @@ -388,64 +121,6 @@ def with_raw_response(self) -> AsyncRawTraceClient: """ return self._raw_client - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get( - self, - trace_id: str, - *, - fields: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> TraceWithFullDetails: - """ - Get a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of a trace - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - TraceWithFullDetails - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.trace.get( - trace_id="traceId", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get( - trace_id, fields=fields, request_options=request_options - ) - return _response.data - async def delete( self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> DeleteTraceResponse: @@ -493,227 +168,6 @@ async def main() -> None: ) return _response.data - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - order_by: typing.Optional[str] = None, - tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - version: typing.Optional[str] = None, - release: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> Traces: - """ - Get list of traces - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - - name : typing.Optional[str] - - session_id : typing.Optional[str] - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - - order_by : typing.Optional[str] - Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - - tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only traces that include all of these tags will be returned. - - version : typing.Optional[str] - Optional filter to only include traces with a certain version. - - release : typing.Optional[str] - Optional filter to only include traces with a certain release. - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for traces where the environment is one of the provided values. - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "booleanObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - booleanObject: "=", "<>" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, booleanObject, and categoryOptions types when filtering on nested fields like metadata or score names - } - ] - ``` - - ## Available Columns - - ### Core Trace Fields - - `id` (string) - Trace ID - - `name` (string) - Trace name - - `timestamp` (datetime) - Trace timestamp - - `userId` (string) - User ID - - `sessionId` (string) - Session ID - - `environment` (string) - Environment tag - - `version` (string) - Version tag - - `release` (string) - Release tag - - `tags` (arrayOptions) - Array of tags - - `bookmarked` (boolean) - Bookmark status - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Aggregated Metrics (from observations) - These metrics are aggregated from all observations within the trace: - - `latency` (number) - Latency in seconds (time from first observation start to last observation end) - - `inputTokens` (number) - Total input tokens across all observations - - `outputTokens` (number) - Total output tokens across all observations - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - `inputCost` (number) - Total input cost in USD - - `outputCost` (number) - Total output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Observation Level Aggregations - These fields aggregate observation levels within the trace: - - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) - - `warningCount` (number) - Count of WARNING level observations - - `errorCount` (number) - Count of ERROR level observations - - `defaultCount` (number) - Count of DEFAULT level observations - - `debugCount` (number) - Count of DEBUG level observations - - ### Scores (requires join with scores table) - - `scores_avg` (number) - Average of numeric scores (alias: `scores`) - - `score_categories` (categoryOptions) - Categorical score values - - `score_booleans` (booleanObject) - Boolean score values. Use `key` for the score name and a boolean `value`, e.g. `{"type": "booleanObject", "column": "score_booleans", "key": "is_correct", "operator": "=", "value": true}`. The `<>` operator also matches traces without a score of that name. - - ## Filter Examples - ```json - [ - { - "type": "datetime", - "column": "timestamp", - "operator": ">=", - "value": "2024-01-01T00:00:00Z" - }, - { - "type": "string", - "column": "userId", - "operator": "=", - "value": "user-123" - }, - { - "type": "number", - "column": "totalCost", - "operator": ">=", - "value": 0.01 - }, - { - "type": "arrayOptions", - "column": "tags", - "operator": "all of", - "value": ["production", "critical"] - }, - { - "type": "stringObject", - "column": "metadata", - "key": "customer_tier", - "operator": "=", - "value": "enterprise" - } - ] - ``` - - ## Performance Notes - - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance - - Score filters require a join with the scores table and may impact query performance - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Traces - - Examples - -------- - import asyncio - - from langfuse import AsyncLangfuseAPI - - client = AsyncLangfuseAPI( - x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", - x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", - x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", - username="YOUR_USERNAME", - password="YOUR_PASSWORD", - base_url="https://yourhost.com/path/to/api", - ) - - - async def main() -> None: - await client.trace.list() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list( - page=page, - limit=limit, - user_id=user_id, - name=name, - session_id=session_id, - from_timestamp=from_timestamp, - to_timestamp=to_timestamp, - order_by=order_by, - tags=tags, - version=version, - release=release, - environment=environment, - fields=fields, - filter=filter, - request_options=request_options, - ) - return _response.data - async def delete_multiple( self, *, diff --git a/langfuse/api/trace/raw_client.py b/langfuse/api/trace/raw_client.py index 0797b9919..833f4f052 100644 --- a/langfuse/api/trace/raw_client.py +++ b/langfuse/api/trace/raw_client.py @@ -1,8 +1,6 @@ # This file was auto-generated by Fern from our API Definition. -import datetime as dt import typing -import typing_extensions from json.decoder import JSONDecodeError from ..commons.errors.access_denied_error import AccessDeniedError @@ -10,16 +8,13 @@ from ..commons.errors.method_not_allowed_error import MethodNotAllowedError from ..commons.errors.not_found_error import NotFoundError from ..commons.errors.unauthorized_error import UnauthorizedError -from ..commons.types.trace_with_full_details import TraceWithFullDetails from ..core.api_error import ApiError from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.datetime_utils import serialize_datetime from ..core.http_response import AsyncHttpResponse, HttpResponse from ..core.jsonable_encoder import jsonable_encoder from ..core.pydantic_utilities import parse_obj_as from ..core.request_options import RequestOptions from .types.delete_trace_response import DeleteTraceResponse -from .types.traces import Traces # this is used as the default value for optional parameters OMIT = typing.cast(typing.Any, ...) @@ -29,121 +24,6 @@ class RawTraceClient: def __init__(self, *, client_wrapper: SyncClientWrapper): self._client_wrapper = client_wrapper - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def get( - self, - trace_id: str, - *, - fields: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[TraceWithFullDetails]: - """ - Get a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of a trace - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[TraceWithFullDetails] - """ - _response = self._client_wrapper.httpx_client.request( - f"api/public/traces/{jsonable_encoder(trace_id)}", - method="GET", - params={ - "fields": fields, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - TraceWithFullDetails, - parse_obj_as( - type_=TraceWithFullDetails, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - def delete( self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[DeleteTraceResponse]: @@ -245,289 +125,6 @@ def delete( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - order_by: typing.Optional[str] = None, - tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - version: typing.Optional[str] = None, - release: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[Traces]: - """ - Get list of traces - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - - name : typing.Optional[str] - - session_id : typing.Optional[str] - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - - order_by : typing.Optional[str] - Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - - tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only traces that include all of these tags will be returned. - - version : typing.Optional[str] - Optional filter to only include traces with a certain version. - - release : typing.Optional[str] - Optional filter to only include traces with a certain release. - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for traces where the environment is one of the provided values. - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "booleanObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - booleanObject: "=", "<>" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, booleanObject, and categoryOptions types when filtering on nested fields like metadata or score names - } - ] - ``` - - ## Available Columns - - ### Core Trace Fields - - `id` (string) - Trace ID - - `name` (string) - Trace name - - `timestamp` (datetime) - Trace timestamp - - `userId` (string) - User ID - - `sessionId` (string) - Session ID - - `environment` (string) - Environment tag - - `version` (string) - Version tag - - `release` (string) - Release tag - - `tags` (arrayOptions) - Array of tags - - `bookmarked` (boolean) - Bookmark status - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Aggregated Metrics (from observations) - These metrics are aggregated from all observations within the trace: - - `latency` (number) - Latency in seconds (time from first observation start to last observation end) - - `inputTokens` (number) - Total input tokens across all observations - - `outputTokens` (number) - Total output tokens across all observations - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - `inputCost` (number) - Total input cost in USD - - `outputCost` (number) - Total output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Observation Level Aggregations - These fields aggregate observation levels within the trace: - - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) - - `warningCount` (number) - Count of WARNING level observations - - `errorCount` (number) - Count of ERROR level observations - - `defaultCount` (number) - Count of DEFAULT level observations - - `debugCount` (number) - Count of DEBUG level observations - - ### Scores (requires join with scores table) - - `scores_avg` (number) - Average of numeric scores (alias: `scores`) - - `score_categories` (categoryOptions) - Categorical score values - - `score_booleans` (booleanObject) - Boolean score values. Use `key` for the score name and a boolean `value`, e.g. `{"type": "booleanObject", "column": "score_booleans", "key": "is_correct", "operator": "=", "value": true}`. The `<>` operator also matches traces without a score of that name. - - ## Filter Examples - ```json - [ - { - "type": "datetime", - "column": "timestamp", - "operator": ">=", - "value": "2024-01-01T00:00:00Z" - }, - { - "type": "string", - "column": "userId", - "operator": "=", - "value": "user-123" - }, - { - "type": "number", - "column": "totalCost", - "operator": ">=", - "value": 0.01 - }, - { - "type": "arrayOptions", - "column": "tags", - "operator": "all of", - "value": ["production", "critical"] - }, - { - "type": "stringObject", - "column": "metadata", - "key": "customer_tier", - "operator": "=", - "value": "enterprise" - } - ] - ``` - - ## Performance Notes - - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance - - Score filters require a join with the scores table and may impact query performance - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[Traces] - """ - _response = self._client_wrapper.httpx_client.request( - "api/public/traces", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "sessionId": session_id, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "orderBy": order_by, - "tags": tags, - "version": version, - "release": release, - "environment": environment, - "fields": fields, - "filter": filter, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - Traces, - parse_obj_as( - type_=Traces, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - def delete_multiple( self, *, @@ -641,121 +238,6 @@ class AsyncRawTraceClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def get( - self, - trace_id: str, - *, - fields: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[TraceWithFullDetails]: - """ - Get a specific trace - - Parameters - ---------- - trace_id : str - The unique langfuse identifier of a trace - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[TraceWithFullDetails] - """ - _response = await self._client_wrapper.httpx_client.request( - f"api/public/traces/{jsonable_encoder(trace_id)}", - method="GET", - params={ - "fields": fields, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - TraceWithFullDetails, - parse_obj_as( - type_=TraceWithFullDetails, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - async def delete( self, trace_id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[DeleteTraceResponse]: @@ -857,289 +339,6 @@ async def delete( body=_response_json, ) - @typing_extensions.deprecated( - "On Langfuse Cloud, Langfuse v3 is deprecated and this endpoint will be removed on November 16, 2026. In Langfuse v4, read span and trace data via `GET /api/public/v2/observations?fromStartTime=&toStartTime=`. Self-hosted deployments are unaffected by this date; the endpoint becomes unavailable when they upgrade to Langfuse v4.", - category=None, - ) - async def list( - self, - *, - page: typing.Optional[int] = None, - limit: typing.Optional[int] = None, - user_id: typing.Optional[str] = None, - name: typing.Optional[str] = None, - session_id: typing.Optional[str] = None, - from_timestamp: typing.Optional[dt.datetime] = None, - to_timestamp: typing.Optional[dt.datetime] = None, - order_by: typing.Optional[str] = None, - tags: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - version: typing.Optional[str] = None, - release: typing.Optional[str] = None, - environment: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - fields: typing.Optional[str] = None, - filter: typing.Optional[str] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[Traces]: - """ - Get list of traces - - Parameters - ---------- - page : typing.Optional[int] - Page number, starts at 1 - - limit : typing.Optional[int] - Limit of items per page. If you encounter api issues due to too large page sizes, try to reduce the limit. - - user_id : typing.Optional[str] - - name : typing.Optional[str] - - session_id : typing.Optional[str] - - from_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp on or after a certain datetime (ISO 8601) - - to_timestamp : typing.Optional[dt.datetime] - Optional filter to only include traces with a trace.timestamp before a certain datetime (ISO 8601) - - order_by : typing.Optional[str] - Format of the string [field].[asc/desc]. Fields: id, timestamp, name, userId, release, version, public, bookmarked, sessionId. Example: timestamp.asc - - tags : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Only traces that include all of these tags will be returned. - - version : typing.Optional[str] - Optional filter to only include traces with a certain version. - - release : typing.Optional[str] - Optional filter to only include traces with a certain release. - - environment : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Optional filter for traces where the environment is one of the provided values. - - fields : typing.Optional[str] - Comma-separated list of fields to include in the response. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. - - filter : typing.Optional[str] - JSON string containing an array of filter conditions. When provided, this takes precedence over query parameter filters (userId, name, sessionId, tags, version, release, environment, fromTimestamp, toTimestamp). - - ## Filter Structure - Each filter condition has the following structure: - ```json - [ - { - "type": string, // Required. One of: "datetime", "string", "number", "stringOptions", "categoryOptions", "arrayOptions", "stringObject", "numberObject", "booleanObject", "boolean", "null" - "column": string, // Required. Column to filter on (see available columns below) - "operator": string, // Required. Operator based on type: - // - datetime: ">", "<", ">=", "<=" - // - string: "=", "contains", "does not contain", "starts with", "ends with" - // - stringOptions: "any of", "none of" - // - categoryOptions: "any of", "none of" - // - arrayOptions: "any of", "none of", "all of" - // - number: "=", ">", "<", ">=", "<=" - // - stringObject: "=", "contains", "does not contain", "starts with", "ends with", "is set", "is not set" - // - numberObject: "=", ">", "<", ">=", "<=" - // - booleanObject: "=", "<>" - // - boolean: "=", "<>" - // - null: "is null", "is not null" - "value": any, // Required (except for null type). Value to compare against. Type depends on filter type - "key": string // Required only for stringObject, numberObject, booleanObject, and categoryOptions types when filtering on nested fields like metadata or score names - } - ] - ``` - - ## Available Columns - - ### Core Trace Fields - - `id` (string) - Trace ID - - `name` (string) - Trace name - - `timestamp` (datetime) - Trace timestamp - - `userId` (string) - User ID - - `sessionId` (string) - Session ID - - `environment` (string) - Environment tag - - `version` (string) - Version tag - - `release` (string) - Release tag - - `tags` (arrayOptions) - Array of tags - - `bookmarked` (boolean) - Bookmark status - - ### Structured Data - - `metadata` (stringObject/numberObject/categoryOptions) - Metadata key-value pairs. Use `key` parameter to filter on specific metadata keys. - - ### Aggregated Metrics (from observations) - These metrics are aggregated from all observations within the trace: - - `latency` (number) - Latency in seconds (time from first observation start to last observation end) - - `inputTokens` (number) - Total input tokens across all observations - - `outputTokens` (number) - Total output tokens across all observations - - `totalTokens` (number) - Total tokens (alias: `tokens`) - - `inputCost` (number) - Total input cost in USD - - `outputCost` (number) - Total output cost in USD - - `totalCost` (number) - Total cost in USD - - ### Observation Level Aggregations - These fields aggregate observation levels within the trace: - - `level` (string) - Highest severity level (ERROR > WARNING > DEFAULT > DEBUG) - - `warningCount` (number) - Count of WARNING level observations - - `errorCount` (number) - Count of ERROR level observations - - `defaultCount` (number) - Count of DEFAULT level observations - - `debugCount` (number) - Count of DEBUG level observations - - ### Scores (requires join with scores table) - - `scores_avg` (number) - Average of numeric scores (alias: `scores`) - - `score_categories` (categoryOptions) - Categorical score values - - `score_booleans` (booleanObject) - Boolean score values. Use `key` for the score name and a boolean `value`, e.g. `{"type": "booleanObject", "column": "score_booleans", "key": "is_correct", "operator": "=", "value": true}`. The `<>` operator also matches traces without a score of that name. - - ## Filter Examples - ```json - [ - { - "type": "datetime", - "column": "timestamp", - "operator": ">=", - "value": "2024-01-01T00:00:00Z" - }, - { - "type": "string", - "column": "userId", - "operator": "=", - "value": "user-123" - }, - { - "type": "number", - "column": "totalCost", - "operator": ">=", - "value": 0.01 - }, - { - "type": "arrayOptions", - "column": "tags", - "operator": "all of", - "value": ["production", "critical"] - }, - { - "type": "stringObject", - "column": "metadata", - "key": "customer_tier", - "operator": "=", - "value": "enterprise" - } - ] - ``` - - ## Performance Notes - - Filtering on `userId`, `sessionId`, or `metadata` may enable skip indexes for better query performance - - Score filters require a join with the scores table and may impact query performance - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[Traces] - """ - _response = await self._client_wrapper.httpx_client.request( - "api/public/traces", - method="GET", - params={ - "page": page, - "limit": limit, - "userId": user_id, - "name": name, - "sessionId": session_id, - "fromTimestamp": serialize_datetime(from_timestamp) - if from_timestamp is not None - else None, - "toTimestamp": serialize_datetime(to_timestamp) - if to_timestamp is not None - else None, - "orderBy": order_by, - "tags": tags, - "version": version, - "release": release, - "environment": environment, - "fields": fields, - "filter": filter, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - Traces, - parse_obj_as( - type_=Traces, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise Error( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise AccessDeniedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 405: - raise MethodNotAllowedError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - typing.Any, - parse_obj_as( - type_=typing.Any, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response.text, - ) - raise ApiError( - status_code=_response.status_code, - headers=dict(_response.headers), - body=_response_json, - ) - async def delete_multiple( self, *, diff --git a/langfuse/api/trace/types/__init__.py b/langfuse/api/trace/types/__init__.py index 3eab30d21..fbd53980d 100644 --- a/langfuse/api/trace/types/__init__.py +++ b/langfuse/api/trace/types/__init__.py @@ -7,12 +7,8 @@ if typing.TYPE_CHECKING: from .delete_trace_response import DeleteTraceResponse - from .sort import Sort - from .traces import Traces _dynamic_imports: typing.Dict[str, str] = { - "DeleteTraceResponse": ".delete_trace_response", - "Sort": ".sort", - "Traces": ".traces", + "DeleteTraceResponse": ".delete_trace_response" } @@ -43,4 +39,4 @@ def __dir__(): return sorted(lazy_attrs) -__all__ = ["DeleteTraceResponse", "Sort", "Traces"] +__all__ = ["DeleteTraceResponse"] diff --git a/langfuse/api/trace/types/sort.py b/langfuse/api/trace/types/sort.py deleted file mode 100644 index b6b992870..000000000 --- a/langfuse/api/trace/types/sort.py +++ /dev/null @@ -1,14 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ...core.pydantic_utilities import UniversalBaseModel - - -class Sort(UniversalBaseModel): - id: str - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/trace/types/traces.py b/langfuse/api/trace/types/traces.py deleted file mode 100644 index 3ab791cf9..000000000 --- a/langfuse/api/trace/types/traces.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ...commons.types.deprecation import Deprecation -from ...commons.types.trace_with_details import TraceWithDetails -from ...core.pydantic_utilities import UniversalBaseModel -from ...core.serialization import FieldMetadata -from ...utils.pagination.types.meta_response import MetaResponse - - -class Traces(UniversalBaseModel): - data: typing.List[TraceWithDetails] - meta: MetaResponse - deprecation: typing_extensions.Annotated[ - typing.Optional[Deprecation], FieldMetadata(alias="_deprecation") - ] = None - - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( - extra="allow", frozen=True - ) diff --git a/langfuse/api/unstable/__init__.py b/langfuse/api/unstable/__init__.py index 2a249200a..331154d29 100644 --- a/langfuse/api/unstable/__init__.py +++ b/langfuse/api/unstable/__init__.py @@ -20,7 +20,7 @@ TooManyRequestsError, UnauthorizedError, ) - from . import dashboard_widgets, dashboards, errors + from . import dashboard_widgets, dashboards, errors, skills from .dashboard_widgets import ( CreateDashboardWidgetRequest, DashboardWidget, @@ -59,6 +59,22 @@ UpdateDashboardRequest, WidgetPlacement, ) + from .skills import ( + CreateSkillVersionRequest, + DeleteSkillVersionResponse, + SkillFile, + SkillFileContent, + SkillFileContentsResponse, + SkillListPagination, + SkillMeta, + SkillMetaListResponse, + SkillVersion, + SkillVersionFileCreateInput, + SkillVersionFileInput, + SkillVersionFileReference, + UpdateSkillLabelsRequest, + UpdateSkillRequest, + ) _dynamic_imports: typing.Dict[str, str] = { "AccessDeniedError": ".errors", "BadRequestError": ".errors", @@ -69,6 +85,7 @@ "CreateDashboardRequest": ".dashboards", "CreateDashboardWidgetRequest": ".dashboard_widgets", "CreatePresetPlacement": ".dashboards", + "CreateSkillVersionRequest": ".skills", "CreateWidgetPlacement": ".dashboards", "Dashboard": ".dashboards", "DashboardDefinition": ".dashboards", @@ -92,6 +109,7 @@ "DeleteDashboardPlacementResponse": ".dashboards", "DeleteDashboardResponse": ".dashboards", "DeleteDashboardWidgetResponse": ".dashboard_widgets", + "DeleteSkillVersionResponse": ".skills", "InternalServerError": ".errors", "MethodNotAllowedError": ".errors", "NotFoundError": ".errors", @@ -100,15 +118,28 @@ "PublicApiErrorCode": ".errors", "PublicApiErrorDetails": ".errors", "PublicApiValidationIssue": ".errors", + "SkillFile": ".skills", + "SkillFileContent": ".skills", + "SkillFileContentsResponse": ".skills", + "SkillListPagination": ".skills", + "SkillMeta": ".skills", + "SkillMetaListResponse": ".skills", + "SkillVersion": ".skills", + "SkillVersionFileCreateInput": ".skills", + "SkillVersionFileInput": ".skills", + "SkillVersionFileReference": ".skills", "TooManyRequestsError": ".errors", "UnauthorizedError": ".errors", "UpdateDashboardPlacementRequest": ".dashboards", "UpdateDashboardRequest": ".dashboards", "UpdateDashboardWidgetRequest": ".dashboard_widgets", + "UpdateSkillLabelsRequest": ".skills", + "UpdateSkillRequest": ".skills", "WidgetPlacement": ".dashboards", "dashboard_widgets": ".dashboard_widgets", "dashboards": ".dashboards", "errors": ".errors", + "skills": ".skills", } @@ -149,6 +180,7 @@ def __dir__(): "CreateDashboardRequest", "CreateDashboardWidgetRequest", "CreatePresetPlacement", + "CreateSkillVersionRequest", "CreateWidgetPlacement", "Dashboard", "DashboardDefinition", @@ -172,6 +204,7 @@ def __dir__(): "DeleteDashboardPlacementResponse", "DeleteDashboardResponse", "DeleteDashboardWidgetResponse", + "DeleteSkillVersionResponse", "InternalServerError", "MethodNotAllowedError", "NotFoundError", @@ -180,13 +213,26 @@ def __dir__(): "PublicApiErrorCode", "PublicApiErrorDetails", "PublicApiValidationIssue", + "SkillFile", + "SkillFileContent", + "SkillFileContentsResponse", + "SkillListPagination", + "SkillMeta", + "SkillMetaListResponse", + "SkillVersion", + "SkillVersionFileCreateInput", + "SkillVersionFileInput", + "SkillVersionFileReference", "TooManyRequestsError", "UnauthorizedError", "UpdateDashboardPlacementRequest", "UpdateDashboardRequest", "UpdateDashboardWidgetRequest", + "UpdateSkillLabelsRequest", + "UpdateSkillRequest", "WidgetPlacement", "dashboard_widgets", "dashboards", "errors", + "skills", ] diff --git a/langfuse/api/unstable/client.py b/langfuse/api/unstable/client.py index 23050bc80..3cc2a9298 100644 --- a/langfuse/api/unstable/client.py +++ b/langfuse/api/unstable/client.py @@ -13,6 +13,7 @@ DashboardWidgetsClient, ) from .dashboards.client import AsyncDashboardsClient, DashboardsClient + from .skills.client import AsyncSkillsClient, SkillsClient class UnstableClient: @@ -21,6 +22,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): self._client_wrapper = client_wrapper self._dashboard_widgets: typing.Optional[DashboardWidgetsClient] = None self._dashboards: typing.Optional[DashboardsClient] = None + self._skills: typing.Optional[SkillsClient] = None @property def with_raw_response(self) -> RawUnstableClient: @@ -51,6 +53,14 @@ def dashboards(self): self._dashboards = DashboardsClient(client_wrapper=self._client_wrapper) return self._dashboards + @property + def skills(self): + if self._skills is None: + from .skills.client import SkillsClient # noqa: E402 + + self._skills = SkillsClient(client_wrapper=self._client_wrapper) + return self._skills + class AsyncUnstableClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): @@ -58,6 +68,7 @@ def __init__(self, *, client_wrapper: AsyncClientWrapper): self._client_wrapper = client_wrapper self._dashboard_widgets: typing.Optional[AsyncDashboardWidgetsClient] = None self._dashboards: typing.Optional[AsyncDashboardsClient] = None + self._skills: typing.Optional[AsyncSkillsClient] = None @property def with_raw_response(self) -> AsyncRawUnstableClient: @@ -89,3 +100,11 @@ def dashboards(self): client_wrapper=self._client_wrapper ) return self._dashboards + + @property + def skills(self): + if self._skills is None: + from .skills.client import AsyncSkillsClient # noqa: E402 + + self._skills = AsyncSkillsClient(client_wrapper=self._client_wrapper) + return self._skills diff --git a/langfuse/api/unstable/skills/__init__.py b/langfuse/api/unstable/skills/__init__.py new file mode 100644 index 000000000..710cf53a6 --- /dev/null +++ b/langfuse/api/unstable/skills/__init__.py @@ -0,0 +1,85 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + CreateSkillVersionRequest, + DeleteSkillVersionResponse, + SkillFile, + SkillFileContent, + SkillFileContentsResponse, + SkillListPagination, + SkillMeta, + SkillMetaListResponse, + SkillVersion, + SkillVersionFileCreateInput, + SkillVersionFileInput, + SkillVersionFileReference, + UpdateSkillLabelsRequest, + UpdateSkillRequest, + ) +_dynamic_imports: typing.Dict[str, str] = { + "CreateSkillVersionRequest": ".types", + "DeleteSkillVersionResponse": ".types", + "SkillFile": ".types", + "SkillFileContent": ".types", + "SkillFileContentsResponse": ".types", + "SkillListPagination": ".types", + "SkillMeta": ".types", + "SkillMetaListResponse": ".types", + "SkillVersion": ".types", + "SkillVersionFileCreateInput": ".types", + "SkillVersionFileInput": ".types", + "SkillVersionFileReference": ".types", + "UpdateSkillLabelsRequest": ".types", + "UpdateSkillRequest": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateSkillVersionRequest", + "DeleteSkillVersionResponse", + "SkillFile", + "SkillFileContent", + "SkillFileContentsResponse", + "SkillListPagination", + "SkillMeta", + "SkillMetaListResponse", + "SkillVersion", + "SkillVersionFileCreateInput", + "SkillVersionFileInput", + "SkillVersionFileReference", + "UpdateSkillLabelsRequest", + "UpdateSkillRequest", +] diff --git a/langfuse/api/unstable/skills/client.py b/langfuse/api/unstable/skills/client.py new file mode 100644 index 000000000..ffbd4899e --- /dev/null +++ b/langfuse/api/unstable/skills/client.py @@ -0,0 +1,808 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from .raw_client import AsyncRawSkillsClient, RawSkillsClient +from .types.delete_skill_version_response import DeleteSkillVersionResponse +from .types.skill_file_contents_response import SkillFileContentsResponse +from .types.skill_meta_list_response import SkillMetaListResponse +from .types.skill_version import SkillVersion +from .types.skill_version_file_create_input import SkillVersionFileCreateInput + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class SkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawSkillsClient + """ + return self._raw_client + + def list( + self, + *, + name: typing.Optional[str] = None, + search: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillMetaListResponse: + """ + List skills with metadata and timestamps from their latest version, shared tags, and the version assigned to production when present. + + Parameters + ---------- + name : typing.Optional[str] + Filter by exact skill name. + + search : typing.Optional[str] + Case-insensitive search across skill names and latest-version descriptions. + + tag : typing.Optional[str] + Filter by a shared skill tag. + + page : typing.Optional[int] + + limit : typing.Optional[int] + + from_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated at or after this timestamp. + + to_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated before this timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillMetaListResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.list() + """ + _response = self._raw_client.list( + name=name, + search=search, + tag=tag, + page=page, + limit=limit, + from_updated_at=from_updated_at, + to_updated_at=to_updated_at, + request_options=request_options, + ) + return _response.data + + def create_version( + self, + *, + files: typing.Sequence[SkillVersionFileCreateInput], + commit_message: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Create skill version + + Parameters + ---------- + files : typing.Sequence[SkillVersionFileCreateInput] + + commit_message : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + from langfuse import LangfuseAPI + from langfuse.unstable.skills import SkillVersionFileInput + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.create_version( + files=[ + SkillVersionFileInput( + path="path", + content="content", + ), + SkillVersionFileInput( + path="path", + content="content", + ), + ], + ) + """ + _response = self._raw_client.create_version( + files=files, commit_message=commit_message, request_options=request_options + ) + return _response.data + + def get( + self, + skill_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Resolve a skill's metadata and file manifest by version or label. Defaults to the production label. Use each file's sha256Hash with getFileContents, individually or in batches across manifests. + + Parameters + ---------- + skill_name : str + + version : typing.Optional[int] + + label : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.get( + skill_name="skillName", + ) + """ + _response = self._raw_client.get( + skill_name, version=version, label=label, request_options=request_options + ) + return _response.data + + def update( + self, + skill_name: str, + *, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Replace the shared tags across all versions of a skill. An empty tags list clears all tags. Returns the latest skill version with the updated tags. + + Parameters + ---------- + skill_name : str + + tags : typing.Sequence[str] + The complete set of tags to apply to every version of the skill. Pass an empty list to clear all tags. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.update( + skill_name="skillName", + tags=["tags", "tags"], + ) + """ + _response = self._raw_client.update( + skill_name, tags=tags, request_options=request_options + ) + return _response.data + + def get_file_contents( + self, + *, + sha256hashes: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillFileContentsResponse: + """ + Read a batch of text contents by canonical base64-encoded SHA-256 hashes. + + Parameters + ---------- + sha256hashes : str + Comma-separated list of one to 50 canonical base64-encoded SHA-256 hashes from skill file manifests. URL-encode the value, including +, /, and = characters. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillFileContentsResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.get_file_contents( + sha256hashes="sha256Hashes", + ) + """ + _response = self._raw_client.get_file_contents( + sha256hashes=sha256hashes, request_options=request_options + ) + return _response.data + + def set_labels( + self, + skill_name: str, + skill_version: int, + *, + labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Replace the labels on a skill version and atomically move them from other versions. + + Parameters + ---------- + skill_name : str + + skill_version : int + + labels : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.set_labels( + skill_name="skillName", + skill_version=1, + labels=["labels", "labels"], + ) + """ + _response = self._raw_client.set_labels( + skill_name, skill_version, labels=labels, request_options=request_options + ) + return _response.data + + def delete_version( + self, + skill_name: str, + skill_version: int, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteSkillVersionResponse: + """ + Delete one immutable skill version. Unreferenced blobs are retained for asynchronous cleanup. + + Parameters + ---------- + skill_name : str + + skill_version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteSkillVersionResponse + + Examples + -------- + from langfuse import LangfuseAPI + + client = LangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + client.unstable.skills.delete_version( + skill_name="skillName", + skill_version=1, + ) + """ + _response = self._raw_client.delete_version( + skill_name, skill_version, request_options=request_options + ) + return _response.data + + +class AsyncSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawSkillsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawSkillsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawSkillsClient + """ + return self._raw_client + + async def list( + self, + *, + name: typing.Optional[str] = None, + search: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillMetaListResponse: + """ + List skills with metadata and timestamps from their latest version, shared tags, and the version assigned to production when present. + + Parameters + ---------- + name : typing.Optional[str] + Filter by exact skill name. + + search : typing.Optional[str] + Case-insensitive search across skill names and latest-version descriptions. + + tag : typing.Optional[str] + Filter by a shared skill tag. + + page : typing.Optional[int] + + limit : typing.Optional[int] + + from_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated at or after this timestamp. + + to_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated before this timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillMetaListResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.list() + + + asyncio.run(main()) + """ + _response = await self._raw_client.list( + name=name, + search=search, + tag=tag, + page=page, + limit=limit, + from_updated_at=from_updated_at, + to_updated_at=to_updated_at, + request_options=request_options, + ) + return _response.data + + async def create_version( + self, + *, + files: typing.Sequence[SkillVersionFileCreateInput], + commit_message: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Create skill version + + Parameters + ---------- + files : typing.Sequence[SkillVersionFileCreateInput] + + commit_message : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + from langfuse.unstable.skills import SkillVersionFileInput + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.create_version( + files=[ + SkillVersionFileInput( + path="path", + content="content", + ), + SkillVersionFileInput( + path="path", + content="content", + ), + ], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_version( + files=files, commit_message=commit_message, request_options=request_options + ) + return _response.data + + async def get( + self, + skill_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Resolve a skill's metadata and file manifest by version or label. Defaults to the production label. Use each file's sha256Hash with getFileContents, individually or in batches across manifests. + + Parameters + ---------- + skill_name : str + + version : typing.Optional[int] + + label : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.get( + skill_name="skillName", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + skill_name, version=version, label=label, request_options=request_options + ) + return _response.data + + async def update( + self, + skill_name: str, + *, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Replace the shared tags across all versions of a skill. An empty tags list clears all tags. Returns the latest skill version with the updated tags. + + Parameters + ---------- + skill_name : str + + tags : typing.Sequence[str] + The complete set of tags to apply to every version of the skill. Pass an empty list to clear all tags. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.update( + skill_name="skillName", + tags=["tags", "tags"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + skill_name, tags=tags, request_options=request_options + ) + return _response.data + + async def get_file_contents( + self, + *, + sha256hashes: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillFileContentsResponse: + """ + Read a batch of text contents by canonical base64-encoded SHA-256 hashes. + + Parameters + ---------- + sha256hashes : str + Comma-separated list of one to 50 canonical base64-encoded SHA-256 hashes from skill file manifests. URL-encode the value, including +, /, and = characters. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillFileContentsResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.get_file_contents( + sha256hashes="sha256Hashes", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_file_contents( + sha256hashes=sha256hashes, request_options=request_options + ) + return _response.data + + async def set_labels( + self, + skill_name: str, + skill_version: int, + *, + labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> SkillVersion: + """ + Replace the labels on a skill version and atomically move them from other versions. + + Parameters + ---------- + skill_name : str + + skill_version : int + + labels : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + SkillVersion + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.set_labels( + skill_name="skillName", + skill_version=1, + labels=["labels", "labels"], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.set_labels( + skill_name, skill_version, labels=labels, request_options=request_options + ) + return _response.data + + async def delete_version( + self, + skill_name: str, + skill_version: int, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> DeleteSkillVersionResponse: + """ + Delete one immutable skill version. Unreferenced blobs are retained for asynchronous cleanup. + + Parameters + ---------- + skill_name : str + + skill_version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + DeleteSkillVersionResponse + + Examples + -------- + import asyncio + + from langfuse import AsyncLangfuseAPI + + client = AsyncLangfuseAPI( + x_langfuse_sdk_name="YOUR_X_LANGFUSE_SDK_NAME", + x_langfuse_sdk_version="YOUR_X_LANGFUSE_SDK_VERSION", + x_langfuse_public_key="YOUR_X_LANGFUSE_PUBLIC_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.unstable.skills.delete_version( + skill_name="skillName", + skill_version=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete_version( + skill_name, skill_version, request_options=request_options + ) + return _response.data diff --git a/langfuse/api/unstable/skills/raw_client.py b/langfuse/api/unstable/skills/raw_client.py new file mode 100644 index 000000000..152cc2781 --- /dev/null +++ b/langfuse/api/unstable/skills/raw_client.py @@ -0,0 +1,1645 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing +from json.decoder import JSONDecodeError + +from ...commons.errors.access_denied_error import AccessDeniedError +from ...commons.errors.error import Error +from ...commons.errors.method_not_allowed_error import MethodNotAllowedError +from ...commons.errors.not_found_error import NotFoundError +from ...commons.errors.unauthorized_error import UnauthorizedError +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.datetime_utils import serialize_datetime +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import jsonable_encoder +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from ...core.serialization import convert_and_respect_annotation_metadata +from .types.delete_skill_version_response import DeleteSkillVersionResponse +from .types.skill_file_contents_response import SkillFileContentsResponse +from .types.skill_meta_list_response import SkillMetaListResponse +from .types.skill_version import SkillVersion +from .types.skill_version_file_create_input import SkillVersionFileCreateInput + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawSkillsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + name: typing.Optional[str] = None, + search: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SkillMetaListResponse]: + """ + List skills with metadata and timestamps from their latest version, shared tags, and the version assigned to production when present. + + Parameters + ---------- + name : typing.Optional[str] + Filter by exact skill name. + + search : typing.Optional[str] + Case-insensitive search across skill names and latest-version descriptions. + + tag : typing.Optional[str] + Filter by a shared skill tag. + + page : typing.Optional[int] + + limit : typing.Optional[int] + + from_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated at or after this timestamp. + + to_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated before this timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SkillMetaListResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/skills", + method="GET", + params={ + "name": name, + "search": search, + "tag": tag, + "page": page, + "limit": limit, + "fromUpdatedAt": serialize_datetime(from_updated_at) + if from_updated_at is not None + else None, + "toUpdatedAt": serialize_datetime(to_updated_at) + if to_updated_at is not None + else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillMetaListResponse, + parse_obj_as( + type_=SkillMetaListResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def create_version( + self, + *, + files: typing.Sequence[SkillVersionFileCreateInput], + commit_message: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SkillVersion]: + """ + Create skill version + + Parameters + ---------- + files : typing.Sequence[SkillVersionFileCreateInput] + + commit_message : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SkillVersion] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/skills", + method="POST", + json={ + "files": convert_and_respect_annotation_metadata( + object_=files, + annotation=typing.Sequence[SkillVersionFileCreateInput], + direction="write", + ), + "commitMessage": commit_message, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get( + self, + skill_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SkillVersion]: + """ + Resolve a skill's metadata and file manifest by version or label. Defaults to the production label. Use each file's sha256Hash with getFileContents, individually or in batches across manifests. + + Parameters + ---------- + skill_name : str + + version : typing.Optional[int] + + label : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SkillVersion] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}", + method="GET", + params={ + "version": version, + "label": label, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def update( + self, + skill_name: str, + *, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SkillVersion]: + """ + Replace the shared tags across all versions of a skill. An empty tags list clears all tags. Returns the latest skill version with the updated tags. + + Parameters + ---------- + skill_name : str + + tags : typing.Sequence[str] + The complete set of tags to apply to every version of the skill. Pass an empty list to clear all tags. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SkillVersion] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}", + method="PATCH", + json={ + "tags": tags, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def get_file_contents( + self, + *, + sha256hashes: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SkillFileContentsResponse]: + """ + Read a batch of text contents by canonical base64-encoded SHA-256 hashes. + + Parameters + ---------- + sha256hashes : str + Comma-separated list of one to 50 canonical base64-encoded SHA-256 hashes from skill file manifests. URL-encode the value, including +, /, and = characters. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SkillFileContentsResponse] + """ + _response = self._client_wrapper.httpx_client.request( + "api/public/unstable/skills/files/content", + method="GET", + params={ + "sha256Hashes": sha256hashes, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillFileContentsResponse, + parse_obj_as( + type_=SkillFileContentsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def set_labels( + self, + skill_name: str, + skill_version: int, + *, + labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[SkillVersion]: + """ + Replace the labels on a skill version and atomically move them from other versions. + + Parameters + ---------- + skill_name : str + + skill_version : int + + labels : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[SkillVersion] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}/versions/{jsonable_encoder(skill_version)}", + method="PATCH", + json={ + "labels": labels, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + def delete_version( + self, + skill_name: str, + skill_version: int, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[DeleteSkillVersionResponse]: + """ + Delete one immutable skill version. Unreferenced blobs are retained for asynchronous cleanup. + + Parameters + ---------- + skill_name : str + + skill_version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[DeleteSkillVersionResponse] + """ + _response = self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}/versions/{jsonable_encoder(skill_version)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteSkillVersionResponse, + parse_obj_as( + type_=DeleteSkillVersionResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + +class AsyncRawSkillsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + name: typing.Optional[str] = None, + search: typing.Optional[str] = None, + tag: typing.Optional[str] = None, + page: typing.Optional[int] = None, + limit: typing.Optional[int] = None, + from_updated_at: typing.Optional[dt.datetime] = None, + to_updated_at: typing.Optional[dt.datetime] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SkillMetaListResponse]: + """ + List skills with metadata and timestamps from their latest version, shared tags, and the version assigned to production when present. + + Parameters + ---------- + name : typing.Optional[str] + Filter by exact skill name. + + search : typing.Optional[str] + Case-insensitive search across skill names and latest-version descriptions. + + tag : typing.Optional[str] + Filter by a shared skill tag. + + page : typing.Optional[int] + + limit : typing.Optional[int] + + from_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated at or after this timestamp. + + to_updated_at : typing.Optional[dt.datetime] + Include skills whose latest version was updated before this timestamp. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SkillMetaListResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/skills", + method="GET", + params={ + "name": name, + "search": search, + "tag": tag, + "page": page, + "limit": limit, + "fromUpdatedAt": serialize_datetime(from_updated_at) + if from_updated_at is not None + else None, + "toUpdatedAt": serialize_datetime(to_updated_at) + if to_updated_at is not None + else None, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillMetaListResponse, + parse_obj_as( + type_=SkillMetaListResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def create_version( + self, + *, + files: typing.Sequence[SkillVersionFileCreateInput], + commit_message: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SkillVersion]: + """ + Create skill version + + Parameters + ---------- + files : typing.Sequence[SkillVersionFileCreateInput] + + commit_message : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SkillVersion] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/skills", + method="POST", + json={ + "files": convert_and_respect_annotation_metadata( + object_=files, + annotation=typing.Sequence[SkillVersionFileCreateInput], + direction="write", + ), + "commitMessage": commit_message, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get( + self, + skill_name: str, + *, + version: typing.Optional[int] = None, + label: typing.Optional[str] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SkillVersion]: + """ + Resolve a skill's metadata and file manifest by version or label. Defaults to the production label. Use each file's sha256Hash with getFileContents, individually or in batches across manifests. + + Parameters + ---------- + skill_name : str + + version : typing.Optional[int] + + label : typing.Optional[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SkillVersion] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}", + method="GET", + params={ + "version": version, + "label": label, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def update( + self, + skill_name: str, + *, + tags: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SkillVersion]: + """ + Replace the shared tags across all versions of a skill. An empty tags list clears all tags. Returns the latest skill version with the updated tags. + + Parameters + ---------- + skill_name : str + + tags : typing.Sequence[str] + The complete set of tags to apply to every version of the skill. Pass an empty list to clear all tags. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SkillVersion] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}", + method="PATCH", + json={ + "tags": tags, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def get_file_contents( + self, + *, + sha256hashes: str, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SkillFileContentsResponse]: + """ + Read a batch of text contents by canonical base64-encoded SHA-256 hashes. + + Parameters + ---------- + sha256hashes : str + Comma-separated list of one to 50 canonical base64-encoded SHA-256 hashes from skill file manifests. URL-encode the value, including +, /, and = characters. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SkillFileContentsResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + "api/public/unstable/skills/files/content", + method="GET", + params={ + "sha256Hashes": sha256hashes, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillFileContentsResponse, + parse_obj_as( + type_=SkillFileContentsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def set_labels( + self, + skill_name: str, + skill_version: int, + *, + labels: typing.Sequence[str], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[SkillVersion]: + """ + Replace the labels on a skill version and atomically move them from other versions. + + Parameters + ---------- + skill_name : str + + skill_version : int + + labels : typing.Sequence[str] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[SkillVersion] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}/versions/{jsonable_encoder(skill_version)}", + method="PATCH", + json={ + "labels": labels, + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + SkillVersion, + parse_obj_as( + type_=SkillVersion, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) + + async def delete_version( + self, + skill_name: str, + skill_version: int, + *, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[DeleteSkillVersionResponse]: + """ + Delete one immutable skill version. Unreferenced blobs are retained for asynchronous cleanup. + + Parameters + ---------- + skill_name : str + + skill_version : int + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[DeleteSkillVersionResponse] + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/public/unstable/skills/{jsonable_encoder(skill_name)}/versions/{jsonable_encoder(skill_version)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + DeleteSkillVersionResponse, + parse_obj_as( + type_=DeleteSkillVersionResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise Error( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise AccessDeniedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 405: + raise MethodNotAllowedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response.text, + ) + raise ApiError( + status_code=_response.status_code, + headers=dict(_response.headers), + body=_response_json, + ) diff --git a/langfuse/api/unstable/skills/types/__init__.py b/langfuse/api/unstable/skills/types/__init__.py new file mode 100644 index 000000000..bafccc040 --- /dev/null +++ b/langfuse/api/unstable/skills/types/__init__.py @@ -0,0 +1,83 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .create_skill_version_request import CreateSkillVersionRequest + from .delete_skill_version_response import DeleteSkillVersionResponse + from .skill_file import SkillFile + from .skill_file_content import SkillFileContent + from .skill_file_contents_response import SkillFileContentsResponse + from .skill_list_pagination import SkillListPagination + from .skill_meta import SkillMeta + from .skill_meta_list_response import SkillMetaListResponse + from .skill_version import SkillVersion + from .skill_version_file_create_input import SkillVersionFileCreateInput + from .skill_version_file_input import SkillVersionFileInput + from .skill_version_file_reference import SkillVersionFileReference + from .update_skill_labels_request import UpdateSkillLabelsRequest + from .update_skill_request import UpdateSkillRequest +_dynamic_imports: typing.Dict[str, str] = { + "CreateSkillVersionRequest": ".create_skill_version_request", + "DeleteSkillVersionResponse": ".delete_skill_version_response", + "SkillFile": ".skill_file", + "SkillFileContent": ".skill_file_content", + "SkillFileContentsResponse": ".skill_file_contents_response", + "SkillListPagination": ".skill_list_pagination", + "SkillMeta": ".skill_meta", + "SkillMetaListResponse": ".skill_meta_list_response", + "SkillVersion": ".skill_version", + "SkillVersionFileCreateInput": ".skill_version_file_create_input", + "SkillVersionFileInput": ".skill_version_file_input", + "SkillVersionFileReference": ".skill_version_file_reference", + "UpdateSkillLabelsRequest": ".update_skill_labels_request", + "UpdateSkillRequest": ".update_skill_request", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__}" + ) + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "CreateSkillVersionRequest", + "DeleteSkillVersionResponse", + "SkillFile", + "SkillFileContent", + "SkillFileContentsResponse", + "SkillListPagination", + "SkillMeta", + "SkillMetaListResponse", + "SkillVersion", + "SkillVersionFileCreateInput", + "SkillVersionFileInput", + "SkillVersionFileReference", + "UpdateSkillLabelsRequest", + "UpdateSkillRequest", +] diff --git a/langfuse/api/unstable/skills/types/create_skill_version_request.py b/langfuse/api/unstable/skills/types/create_skill_version_request.py new file mode 100644 index 000000000..cf7c073ca --- /dev/null +++ b/langfuse/api/unstable/skills/types/create_skill_version_request.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .skill_version_file_create_input import SkillVersionFileCreateInput + + +class CreateSkillVersionRequest(UniversalBaseModel): + files: typing.List[SkillVersionFileCreateInput] + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = None + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/sdk_log_body.py b/langfuse/api/unstable/skills/types/delete_skill_version_response.py similarity index 62% rename from langfuse/api/ingestion/types/sdk_log_body.py rename to langfuse/api/unstable/skills/types/delete_skill_version_response.py index d5b46f118..cf92c2867 100644 --- a/langfuse/api/ingestion/types/sdk_log_body.py +++ b/langfuse/api/unstable/skills/types/delete_skill_version_response.py @@ -3,11 +3,11 @@ import typing import pydantic -from ...core.pydantic_utilities import UniversalBaseModel +from ....core.pydantic_utilities import UniversalBaseModel -class SdkLogBody(UniversalBaseModel): - log: typing.Any +class DeleteSkillVersionResponse(UniversalBaseModel): + deleted: bool model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/unstable/skills/types/skill_file.py b/langfuse/api/unstable/skills/types/skill_file.py new file mode 100644 index 000000000..683b13679 --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_file.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata + + +class SkillFile(UniversalBaseModel): + id: str + path: str + blob_id: typing_extensions.Annotated[str, FieldMetadata(alias="blobId")] + sha256hash: typing_extensions.Annotated[str, FieldMetadata(alias="sha256Hash")] + content_type: typing_extensions.Annotated[str, FieldMetadata(alias="contentType")] + content_length: typing_extensions.Annotated[ + int, FieldMetadata(alias="contentLength") + ] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/skills/types/skill_file_content.py b/langfuse/api/unstable/skills/types/skill_file_content.py new file mode 100644 index 000000000..ce324848d --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_file_content.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata + + +class SkillFileContent(UniversalBaseModel): + sha256hash: typing_extensions.Annotated[str, FieldMetadata(alias="sha256Hash")] + content: str + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/create_event_body.py b/langfuse/api/unstable/skills/types/skill_file_contents_response.py similarity index 52% rename from langfuse/api/ingestion/types/create_event_body.py rename to langfuse/api/unstable/skills/types/skill_file_contents_response.py index 0473d8a0d..85bb7e2fa 100644 --- a/langfuse/api/ingestion/types/create_event_body.py +++ b/langfuse/api/unstable/skills/types/skill_file_contents_response.py @@ -3,11 +3,12 @@ import typing import pydantic -from .optional_observation_body import OptionalObservationBody +from ....core.pydantic_utilities import UniversalBaseModel +from .skill_file_content import SkillFileContent -class CreateEventBody(OptionalObservationBody): - id: typing.Optional[str] = None +class SkillFileContentsResponse(UniversalBaseModel): + data: typing.List[SkillFileContent] model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/legacy/observations_v1/types/observations.py b/langfuse/api/unstable/skills/types/skill_list_pagination.py similarity index 57% rename from langfuse/api/legacy/observations_v1/types/observations.py rename to langfuse/api/unstable/skills/types/skill_list_pagination.py index 860047ef2..f7ae797de 100644 --- a/langfuse/api/legacy/observations_v1/types/observations.py +++ b/langfuse/api/unstable/skills/types/skill_list_pagination.py @@ -3,14 +3,13 @@ import typing import pydantic -from ....commons.types.observation import Observation -from ....core.pydantic_utilities import UniversalBaseModel +import typing_extensions +from ....core.serialization import FieldMetadata from ....utils.pagination.types.meta_response import MetaResponse -class Observations(UniversalBaseModel): - data: typing.List[Observation] - meta: MetaResponse +class SkillListPagination(MetaResponse): + has_next_page: typing_extensions.Annotated[bool, FieldMetadata(alias="hasNextPage")] model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/unstable/skills/types/skill_meta.py b/langfuse/api/unstable/skills/types/skill_meta.py new file mode 100644 index 000000000..a391972df --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_meta.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata + + +class SkillMeta(UniversalBaseModel): + name: str + description: str + tags: typing.List[str] + latest_version: typing_extensions.Annotated[ + int, FieldMetadata(alias="latestVersion") + ] + latest_version_created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="latestVersionCreatedAt") + ] + latest_version_last_updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="latestVersionLastUpdatedAt") + ] + production_version: typing_extensions.Annotated[ + typing.Optional[int], FieldMetadata(alias="productionVersion") + ] = pydantic.Field(default=None) + """ + Version assigned the production label, or null when no version is assigned. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/skills/types/skill_meta_list_response.py b/langfuse/api/unstable/skills/types/skill_meta_list_response.py new file mode 100644 index 000000000..a097e1bc0 --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_meta_list_response.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel +from .skill_list_pagination import SkillListPagination +from .skill_meta import SkillMeta + + +class SkillMetaListResponse(UniversalBaseModel): + data: typing.List[SkillMeta] + meta: SkillListPagination + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/skills/types/skill_version.py b/langfuse/api/unstable/skills/types/skill_version.py new file mode 100644 index 000000000..c3486c23d --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_version.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata +from .skill_file import SkillFile + + +class SkillVersion(UniversalBaseModel): + id: str + created_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="createdAt") + ] + updated_at: typing_extensions.Annotated[ + dt.datetime, FieldMetadata(alias="updatedAt") + ] + project_id: typing_extensions.Annotated[str, FieldMetadata(alias="projectId")] + created_by: typing_extensions.Annotated[str, FieldMetadata(alias="createdBy")] + name: str + description: str + frontmatter: typing.Any + version: int + tags: typing.List[str] + labels: typing.List[str] + commit_message: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="commitMessage") + ] = None + files: typing.List[SkillFile] + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/unstable/skills/types/skill_version_file_create_input.py b/langfuse/api/unstable/skills/types/skill_version_file_create_input.py new file mode 100644 index 000000000..495002d8c --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_version_file_create_input.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .skill_version_file_input import SkillVersionFileInput +from .skill_version_file_reference import SkillVersionFileReference + +SkillVersionFileCreateInput = typing.Union[ + SkillVersionFileInput, SkillVersionFileReference +] diff --git a/langfuse/api/commons/types/numeric_score_v1.py b/langfuse/api/unstable/skills/types/skill_version_file_input.py similarity index 50% rename from langfuse/api/commons/types/numeric_score_v1.py rename to langfuse/api/unstable/skills/types/skill_version_file_input.py index 872c89aca..c6f20d51e 100644 --- a/langfuse/api/commons/types/numeric_score_v1.py +++ b/langfuse/api/unstable/skills/types/skill_version_file_input.py @@ -3,13 +3,14 @@ import typing import pydantic -from .base_score_v1 import BaseScoreV1 +from ....core.pydantic_utilities import UniversalBaseModel -class NumericScoreV1(BaseScoreV1): - value: float = pydantic.Field() +class SkillVersionFileInput(UniversalBaseModel): + path: str + content: str = pydantic.Field() """ - The numeric value of the score + UTF-8 text content. The server computes the hash and byte length. """ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( diff --git a/langfuse/api/unstable/skills/types/skill_version_file_reference.py b/langfuse/api/unstable/skills/types/skill_version_file_reference.py new file mode 100644 index 000000000..4960d9e90 --- /dev/null +++ b/langfuse/api/unstable/skills/types/skill_version_file_reference.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import UniversalBaseModel +from ....core.serialization import FieldMetadata + + +class SkillVersionFileReference(UniversalBaseModel): + path: str + sha256hash: typing_extensions.Annotated[str, FieldMetadata(alias="sha256Hash")] = ( + pydantic.Field() + ) + """ + Canonical base64-encoded SHA-256 from a skill file manifest. The content must already exist in this project. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/api/ingestion/types/score_event.py b/langfuse/api/unstable/skills/types/update_skill_labels_request.py similarity index 60% rename from langfuse/api/ingestion/types/score_event.py rename to langfuse/api/unstable/skills/types/update_skill_labels_request.py index d0d470899..c1931a1e4 100644 --- a/langfuse/api/ingestion/types/score_event.py +++ b/langfuse/api/unstable/skills/types/update_skill_labels_request.py @@ -3,12 +3,11 @@ import typing import pydantic -from .base_event import BaseEvent -from .score_body import ScoreBody +from ....core.pydantic_utilities import UniversalBaseModel -class ScoreEvent(BaseEvent): - body: ScoreBody +class UpdateSkillLabelsRequest(UniversalBaseModel): + labels: typing.List[str] model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( extra="allow", frozen=True diff --git a/langfuse/api/unstable/skills/types/update_skill_request.py b/langfuse/api/unstable/skills/types/update_skill_request.py new file mode 100644 index 000000000..3c211b921 --- /dev/null +++ b/langfuse/api/unstable/skills/types/update_skill_request.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import UniversalBaseModel + + +class UpdateSkillRequest(UniversalBaseModel): + tags: typing.List[str] = pydantic.Field() + """ + The complete set of tags to apply to every version of the skill. Pass an empty list to clear all tags. + """ + + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( + extra="allow", frozen=True + ) diff --git a/langfuse/batch_evaluation.py b/langfuse/batch_evaluation.py index 723b45757..4d7bf3371 100644 --- a/langfuse/batch_evaluation.py +++ b/langfuse/batch_evaluation.py @@ -1,9 +1,10 @@ """Batch evaluation functionality for Langfuse. This module provides comprehensive batch evaluation capabilities for running evaluations -on traces and observations fetched from Langfuse. It includes type definitions, -protocols, result classes, and the implementation for large-scale evaluation workflows -with error handling, retry logic, and resume capability. +on observations fetched from Langfuse via the v2 observations API +(`GET /api/public/v2/observations`). It includes type definitions, protocols, result +classes, and the implementation for large-scale evaluation workflows with error +handling, retry logic, and resume capability. """ import asyncio @@ -20,35 +21,47 @@ Set, Tuple, Union, - cast, ) -from langfuse.api import ( - ObservationsView, - TraceWithFullDetails, -) +from langfuse.api import ObservationV2 from langfuse.experiment import Evaluation, EvaluatorFunction from langfuse.logger import langfuse_logger as logger if TYPE_CHECKING: from langfuse._client.client import Langfuse +DEFAULT_BATCH_EVALUATION_FIELDS = "core,basic,io,metadata" +"""Default v2 observation field groups fetched for batch evaluation. + +Includes ``io`` (raw ``input``/``output`` strings) and ``metadata`` so that mappers +work without extra configuration. Available groups: core, basic, time, io, +metadata, model, usage, prompt, metrics, trace_context. +""" + + +def _with_time_fields(fields: Optional[str]) -> str: + """Add the ``time`` group, whose ``updatedAt`` picks the newest duplicate row.""" + groups = [group.strip() for group in (fields or "").split(",") if group.strip()] + if not groups: + # The API's default when no groups are requested. + groups = ["core", "basic"] + if "time" not in groups: + groups.append("time") + return ",".join(groups) + class EvaluatorInputs: """Input data structure for evaluators, returned by mapper functions. - This class provides a strongly-typed container for transforming API response - objects (traces, observations) into the standardized format expected - by evaluator functions. It ensures consistent access to input, output, expected - output, and metadata regardless of the source entity type. + This class provides a strongly-typed container for transforming `ObservationV2` + objects returned by the v2 observations API into the standardized format + expected by evaluator functions. Attributes: - input: The input data that was provided to generate the output being evaluated. - For traces, this might be the initial prompt or request. For observations, - this could be the span's input. The exact meaning depends on your use case. - output: The actual output that was produced and needs to be evaluated. - For traces, this is typically the final response. For observations, - this might be the generation output or span result. + input: The input data that was provided to generate the output being evaluated, + for example the observation's input. + output: The actual output that was produced and needs to be evaluated, + for example the generation output or span result. expected_output: Optional ground truth or expected result for comparison. Used by evaluators to assess correctness. May be None if no ground truth is available for the entity being evaluated. @@ -57,38 +70,31 @@ class EvaluatorInputs: or any other relevant data that evaluators might use. Examples: - Simple mapper for traces: + Simple observation mapper: ```python from langfuse import EvaluatorInputs - def trace_mapper(trace): + def simple_mapper(*, item): return EvaluatorInputs( - input=trace.input, - output=trace.output, + input=item.input, # raw string as returned by the API + output=item.output, expected_output=None, # No ground truth available - metadata={"user_id": trace.user_id, "tags": trace.tags} + metadata={"trace_id": item.trace_id, "user_id": item.user_id}, ) ``` - Mapper for observations extracting specific fields: + Mapper that decodes JSON input/output: ```python - def observation_mapper(observation): - # Extract input/output from observation's data - input_data = observation.input if hasattr(observation, 'input') else None - output_data = observation.output if hasattr(observation, 'output') else None + import json + def observation_mapper(*, item): return EvaluatorInputs( - input=input_data, - output=output_data, + input=json.loads(item.input) if item.input else None, + output=json.loads(item.output) if item.output else None, expected_output=None, - metadata={ - "observation_type": observation.type, - "model": observation.model, - "latency_ms": observation.end_time - observation.start_time - } + metadata={"observation_type": item.type, "name": item.name}, ) ``` - ``` Note: All arguments must be passed as keywords when instantiating this class. @@ -122,34 +128,39 @@ def __init__( class MapperFunction(Protocol): """Protocol defining the interface for mapper functions in batch evaluation. - Mapper functions transform API response objects (traces or observations) - into the standardized EvaluatorInputs format that evaluators expect. This abstraction - allows you to define how to extract and structure evaluation data from different - entity types. + Mapper functions transform `ObservationV2` objects from the v2 observations API + into the standardized EvaluatorInputs format that evaluators expect. Mapper functions must: - - Accept a single item parameter (trace, observation) + - Accept a single keyword argument `item` (an `ObservationV2`) - Return an EvaluatorInputs instance with input, output, expected_output, metadata - Can be either synchronous or asynchronous - Should handle missing or malformed data gracefully + + Notes on `ObservationV2`: + - `input` and `output` are raw strings exactly as stored; the SDK does not + parse them. Use `json.loads` in the mapper if you need structured data. + - Only the requested field groups (see the `fields` argument of + `Langfuse.run_batched_evaluation`) are populated; fields of other groups + are None. + - `metadata` values longer than 200 characters are truncated by the API. + - Price fields (`input_price`, `output_price`, `total_price`) are strings. """ def __call__( self, *, - item: Union["TraceWithFullDetails", "ObservationsView"], + item: ObservationV2, **kwargs: Dict[str, Any], ) -> Union[EvaluatorInputs, Awaitable[EvaluatorInputs]]: - """Transform an API response object into evaluator inputs. + """Transform an observation into evaluator inputs. - This method defines how to extract evaluation-relevant data from the raw - API response object. The implementation should map entity-specific fields - to the standardized input/output/expected_output/metadata structure. + This method defines how to extract evaluation-relevant data from the + observation. The implementation should map its fields to the standardized + input/output/expected_output/metadata structure. Args: - item: The API response object to transform. The type depends on the scope: - - TraceWithFullDetails: When evaluating traces - - ObservationsView: When evaluating observations + item: The `ObservationV2` to transform. Returns: EvaluatorInputs: A structured container with: @@ -162,48 +173,45 @@ def __call__( (for async mappers that need to fetch additional data). Examples: - Basic trace mapper: + Basic observation mapper: ```python - def map_trace(trace): + def map_basic(*, item): return EvaluatorInputs( - input=trace.input, - output=trace.output, + input=item.input, + output=item.output, expected_output=None, - metadata={"trace_id": trace.id, "user": trace.user_id} + metadata={"trace_id": item.trace_id, "user": item.user_id} ) ``` Observation mapper with conditional logic: ```python - def map_observation(observation): - # Extract fields based on observation type - if observation.type == "GENERATION": - input_data = observation.input - output_data = observation.output + import json + + def map_observation(*, item): + if item.type == "GENERATION": + input_data = json.loads(item.input) if item.input else None else: - # For other types, use different fields - input_data = observation.metadata.get("input") - output_data = observation.metadata.get("output") + input_data = item.input return EvaluatorInputs( input=input_data, - output=output_data, + output=item.output, expected_output=None, - metadata={"obs_id": observation.id, "type": observation.type} + metadata={"obs_id": item.id, "type": item.type} ) ``` Async mapper (if additional processing needed): ```python - async def map_trace_async(trace): - # Could do async processing here if needed - processed_output = await some_async_transformation(trace.output) + async def map_async(*, item): + processed_output = await some_async_transformation(item.output) return EvaluatorInputs( - input=trace.input, + input=item.input, output=processed_output, expected_output=None, - metadata={"trace_id": trace.id} + metadata={"trace_id": item.trace_id} ) ``` """ @@ -452,125 +460,96 @@ def __init__( class BatchEvaluationResumeToken: - """Token for resuming a failed batch evaluation run. + """Token for resuming an interrupted or limited batch evaluation run. + + The v2 observations API returns observations ordered by start time, newest + first, and paginates with an opaque cursor. The token stores the cursor of + the next page that has not been processed yet, so a resumed run continues + exactly where the previous one stopped, without re-evaluating or skipping + items, even if new observations were ingested in the meantime. - This class encapsulates all the information needed to resume a batch evaluation - that was interrupted or failed partway through. It uses timestamp-based filtering - to avoid re-processing items that were already evaluated, even if the underlying - dataset changed between runs. + A token is returned when a run stops because a batch fetch failed after all + retries (`completed=False`) or because `max_items` was reached while more + items exist (`has_more_items=True`). Attributes: - scope: The type of items being evaluated ("traces", "observations"). - filter: The original JSON filter string used to query items. - last_processed_timestamp: ISO 8601 timestamp of the last successfully processed item. - Used to construct a filter that only fetches items after this timestamp. - last_processed_id: The ID of the last successfully processed item, for reference. - items_processed: Count of items successfully processed before interruption. + filter: The original JSON filter string used to query items. Pass the + same filter when resuming. + cursor: Cursor of the next page to fetch. None if no page was fetched yet. + last_processed_timestamp: ISO 8601 start time of the oldest processed + observation. Only used to resume when `cursor` is None, by fetching + observations that started strictly before this timestamp. + last_processed_id: The ID of the last processed observation, for reference. + last_page_ids: IDs of the observations on the last processed page. A + resumed run skips these, because unmerged duplicate rows of one + observation can span a page boundary. + items_processed: Number of items successfully processed so far, including + items processed by the runs this token was resumed from. Examples: - Resuming a failed batch evaluation: + Resuming a run that stopped early: ```python - # Initial run that fails partway through - try: - result = client.run_batched_evaluation( - scope="traces", - mapper=my_mapper, - evaluators=[evaluator1, evaluator2], - filter='{"tags": ["production"]}', - max_items=10000 - ) - except Exception as e: - print(f"Evaluation failed: {e}") - - # Save the resume token - if result.resume_token: - # Store resume token for later (e.g., in a file or database) - import json - with open("resume_token.json", "w") as f: - json.dump({ - "scope": result.resume_token.scope, - "filter": result.resume_token.filter, - "last_timestamp": result.resume_token.last_processed_timestamp, - "last_id": result.resume_token.last_processed_id, - "items_done": result.resume_token.items_processed - }, f) - - # Later, resume from where it left off - with open("resume_token.json") as f: - token_data = json.load(f) - - resume_token = BatchEvaluationResumeToken( - scope=token_data["scope"], - filter=token_data["filter"], - last_processed_timestamp=token_data["last_timestamp"], - last_processed_id=token_data["last_id"], - items_processed=token_data["items_done"] - ) - - # Resume the evaluation result = client.run_batched_evaluation( - scope="traces", mapper=my_mapper, evaluators=[evaluator1, evaluator2], - resume_from=resume_token + filter=my_filter, + max_items=10000, ) - print(f"Processed {result.total_items_processed} additional items") + if result.resume_token: + result = client.run_batched_evaluation( + mapper=my_mapper, + evaluators=[evaluator1, evaluator2], + filter=my_filter, + resume_from=result.resume_token, + ) ``` - Handling partial completion: + Persisting a token between processes: ```python - result = client.run_batched_evaluation(...) + import json - if not result.completed: - print(f"Evaluation incomplete. Processed {result.resume_token.items_processed} items") - print(f"Last item: {result.resume_token.last_processed_id}") - print(f"Resume from: {result.resume_token.last_processed_timestamp}") + token = result.resume_token + with open("resume_token.json", "w") as f: + json.dump(vars(token), f) - # Optionally retry automatically - if result.resume_token: - print("Retrying...") - result = client.run_batched_evaluation( - scope=result.resume_token.scope, - mapper=my_mapper, - evaluators=my_evaluators, - resume_from=result.resume_token - ) + with open("resume_token.json") as f: + token = BatchEvaluationResumeToken(**json.load(f)) ``` Note: All arguments must be passed as keywords when instantiating this class. - The timestamp-based approach means that items created after the initial run - but before the timestamp will be skipped. This is intentional to avoid - duplicates and ensure consistent evaluation. """ def __init__( self, *, - scope: str, filter: Optional[str], last_processed_timestamp: str, last_processed_id: str, items_processed: int, + cursor: Optional[str] = None, + last_page_ids: Optional[List[str]] = None, ): """Initialize BatchEvaluationResumeToken with the provided state. Args: - scope: The scope type ("traces", "observations"). filter: The original JSON filter string. - last_processed_timestamp: ISO 8601 timestamp of last processed item. + last_processed_timestamp: ISO 8601 start time of the oldest processed item. last_processed_id: ID of last processed item. items_processed: Count of items processed before interruption. + cursor: Cursor of the next page to fetch. + last_page_ids: IDs of the observations on the last processed page. Note: All arguments must be provided as keywords. """ - self.scope = scope self.filter = filter + self.cursor = cursor self.last_processed_timestamp = last_processed_timestamp self.last_processed_id = last_processed_id self.items_processed = items_processed + self.last_page_ids = last_page_ids class BatchEvaluationResult: @@ -588,13 +567,16 @@ class BatchEvaluationResult: total_composite_scores_created: Scores created by the composite evaluator. total_evaluations_failed: Number of individual evaluator failures across all items. evaluator_stats: List of per-evaluator statistics (success/failure rates, scores created). - resume_token: Token for resuming if evaluation was interrupted (None if completed). - completed: True if all items were processed, False if stopped early or failed. + resume_token: Token for continuing the run. Set when a batch fetch failed + (`completed=False`) or when `max_items` was reached while more items + exist (`has_more_items=True`); None otherwise. + completed: False if the run stopped because a batch fetch failed, True otherwise + (including when it stopped at `max_items`). duration_seconds: Total time taken to execute the batch evaluation. - failed_item_ids: List of IDs for items that failed evaluation. + failed_item_ids: List of observation IDs for items that failed evaluation. error_summary: Dictionary mapping error types to occurrence counts. has_more_items: True if max_items limit was reached but more items exist. - item_evaluations: Dictionary mapping item IDs to their evaluation results (both regular and composite). + item_evaluations: Dictionary mapping observation IDs to their evaluation results (both regular and composite). Examples: Basic result inspection: @@ -644,9 +626,7 @@ class BatchEvaluationResult: if result.resume_token: print(f"Processed {result.resume_token.items_processed} items before failure") - print(f"Use resume_from parameter to continue from:") - print(f" Timestamp: {result.resume_token.last_processed_timestamp}") - print(f" Last ID: {result.resume_token.last_processed_id}") + print("Pass it as resume_from to continue") if result.has_more_items: print(f"ℹ️ More items available beyond max_items limit") @@ -839,57 +819,65 @@ def __init__(self, client: "Langfuse"): async def run_async( self, *, - scope: str, mapper: MapperFunction, evaluators: List[EvaluatorFunction], filter: Optional[str] = None, fetch_batch_size: int = 50, - fetch_trace_fields: Optional[str] = "io", + fields: Optional[str] = DEFAULT_BATCH_EVALUATION_FIELDS, max_items: Optional[int] = None, max_concurrency: int = 5, composite_evaluator: Optional[CompositeEvaluatorFunction] = None, metadata: Optional[Dict[str, Any]] = None, _add_observation_scores_to_trace: bool = False, - _additional_trace_tags: Optional[List[str]] = None, max_retries: int = 3, verbose: bool = False, resume_from: Optional[BatchEvaluationResumeToken] = None, ) -> BatchEvaluationResult: - """Run batch evaluation asynchronously using legacy read APIs. + """Run batch evaluation asynchronously. This is the main implementation method that orchestrates the entire batch - evaluation process: fetching items, mapping, evaluating, creating scores, - and tracking statistics. - - This runner reads traces from `GET /api/public/traces` and observations - from the legacy `GET /api/public/observations` endpoint. It is supported - with Langfuse platform v3 and is not yet supported with platform v4. + evaluation process: fetching items from `GET /api/public/v2/observations` + with cursor pagination, mapping, evaluating, creating scores, and tracking + statistics. Args: - scope: The type of items to evaluate ("traces", "observations"). - mapper: Function to transform API response items to evaluator inputs. + mapper: Function to transform `ObservationV2` items to evaluator inputs. evaluators: List of evaluation functions to run on each item. - filter: JSON filter string for querying items. - fetch_batch_size: Number of items to fetch per API call. - fetch_trace_fields: Comma-separated list of fields to include when fetching traces. Available field groups: 'core' (always included), 'io' (input, output, metadata), 'scores', 'observations', 'metrics'. If not specified, all fields are returned. Example: 'core,scores,metrics'. Note: Excluded 'observations' or 'scores' fields return empty arrays; excluded 'metrics' returns -1 for 'totalCost' and 'latency'. Only relevant if scope is 'traces'. Default: 'io' + filter: JSON filter string (v2 observations filter schema). + fetch_batch_size: Number of items to fetch per API call (max 1000). + fields: Comma-separated v2 observation field groups to fetch. max_items: Maximum number of items to process (None = all). max_concurrency: Maximum number of concurrent evaluations. composite_evaluator: Optional function to create composite scores. metadata: Metadata to add to all created scores. _add_observation_scores_to_trace: Private option to duplicate observation-level scores onto the parent trace. - _additional_trace_tags: Private option to add tags on traces via - ingestion trace-create events. max_retries: Maximum retries for failed batch fetches. verbose: If True, log progress to console. - resume_from: Resume token from a previous failed run. + resume_from: Resume token from a previous run. If `filter` is omitted, + the token's filter is reused. Returns: BatchEvaluationResult with comprehensive statistics. + + Raises: + ValueError: If the filter is not a JSON array, or the resume token was + created for a different filter. """ start_time = time.time() - # Initialize tracking variables + # The token's cursor only continues the query that produced it. + if resume_from is not None: + if filter is None: + filter = resume_from.filter + elif filter != resume_from.filter: + raise ValueError( + "Resume token was created for a different filter. Pass the " + "same filter, or omit it to reuse the token's filter." + ) + + effective_filter = self._build_filter(filter=filter, resume_from=resume_from) + total_items_fetched = 0 total_items_processed = 0 total_items_failed = 0 @@ -899,8 +887,8 @@ async def run_async( failed_item_ids: List[str] = [] error_summary: Dict[str, int] = {} item_evaluations: Dict[str, List[Evaluation]] = {} + previously_processed = resume_from.items_processed if resume_from else 0 - # Initialize evaluator stats evaluator_stats_dict = { getattr(evaluator, "__name__", "unknown_evaluator"): EvaluatorStats( name=getattr(evaluator, "__name__", "unknown_evaluator") @@ -908,65 +896,74 @@ async def run_async( for evaluator in evaluators } - # Handle resume token by modifying filter - effective_filter = self._build_timestamp_filter(filter, resume_from) - normalized_additional_trace_tags = ( - self._dedupe_tags(_additional_trace_tags) - if _additional_trace_tags is not None - else [] - ) - updated_trace_ids: Set[str] = set() - - # Create semaphore for concurrency control semaphore = asyncio.Semaphore(max_concurrency) - # Pagination state - page = 1 + cursor: Optional[str] = resume_from.cursor if resume_from else None has_more = True - last_item_timestamp: Optional[str] = None - last_item_id: Optional[str] = None + last_item_timestamp = ( + resume_from.last_processed_timestamp if resume_from else "" + ) + last_item_id = resume_from.last_processed_id if resume_from else "" + # The start-time fallback is inclusive so tied items are not lost; skip + # the one item the token says was already processed. + resumed_item_id = ( + resume_from.last_processed_id + if resume_from is not None + and resume_from.cursor is None + and resume_from.last_processed_timestamp + else None + ) + previous_page_ids: Set[Optional[str]] = set( + (resume_from.last_page_ids or []) if resume_from else [] + ) + batch_number = 0 if verbose: - logger.info("Starting batch evaluation on %s", scope) - if scope == "traces" and fetch_trace_fields: - logger.info("Fetching trace fields: %s", fetch_trace_fields) + logger.info("Starting batch evaluation on observations") + if fields: + logger.info("Fetching observation fields: %s", fields) if resume_from: logger.info( - "Resuming from %s (%s items already processed)", - resume_from.last_processed_timestamp, + "Resuming after %s (%s items already processed)", + resume_from.last_processed_timestamp or "start", resume_from.items_processed, ) - # Main pagination loop - while has_more: - # Check if we've reached max_items + def build_resume_token() -> BatchEvaluationResumeToken: + return BatchEvaluationResumeToken( + filter=filter, + cursor=cursor, + last_processed_timestamp=last_item_timestamp, + last_processed_id=last_item_id, + items_processed=previously_processed + total_items_processed, + last_page_ids=sorted( + item_id for item_id in previous_page_ids if item_id is not None + ), + ) + + while True: if max_items is not None and total_items_fetched >= max_items: if verbose: logger.info("Reached max_items limit (%s)", max_items) - has_more = True # More items may exist break - # Fetch next batch with retry logic + # Clamping the page size keeps the cursor aligned with the last + # processed item, so resuming after max_items skips nothing. + limit = fetch_batch_size + if max_items is not None: + limit = min(limit, max_items - total_items_fetched) + try: - items = await self._fetch_batch_with_retry( - scope=scope, + items, next_cursor = await self._fetch_batch_with_retry( filter=effective_filter, - page=page, - limit=fetch_batch_size, + cursor=cursor, + limit=limit, max_retries=max_retries, - fields=fetch_trace_fields, + fields=fields, ) except Exception as e: - # Failed after max_retries - create resume token and return - error_msg = f"Failed to fetch batch after {max_retries} retries" - logger.error("%s: %s", error_msg, e) - - resume_token = BatchEvaluationResumeToken( - scope=scope, - filter=filter, # Original filter, not modified - last_processed_timestamp=last_item_timestamp or "", - last_processed_id=last_item_id or "", - items_processed=total_items_processed, + logger.error( + "Failed to fetch batch after %s retries: %s", max_retries, e ) return self._build_result( @@ -977,51 +974,28 @@ async def run_async( total_composite_scores_created=total_composite_scores_created, total_evaluations_failed=total_evaluations_failed, evaluator_stats_dict=evaluator_stats_dict, - resume_token=resume_token, + resume_token=build_resume_token(), completed=False, start_time=start_time, failed_item_ids=failed_item_ids, error_summary=error_summary, - has_more_items=has_more, + has_more_items=False, item_evaluations=item_evaluations, ) - # Check if we got any items - if not items: - has_more = False - if verbose: - logger.info("No more items to fetch") - break - + batch_number += 1 total_items_fetched += len(items) if verbose: - logger.info("Fetched batch %s (%s items)", page, len(items)) + logger.info("Fetched batch %s (%s items)", batch_number, len(items)) - # Limit items if max_items would be exceeded - items_to_process = items - if max_items is not None: - remaining_capacity = max_items - total_items_processed - if len(items) > remaining_capacity: - items_to_process = items[:remaining_capacity] - if verbose: - logger.info( - "Limiting batch to %s items to respect max_items=%s", - len(items_to_process), - max_items, - ) - - # Process items concurrently async def process_item( - item: Union[TraceWithFullDetails, ObservationsView], + item: ObservationV2, ) -> Tuple[str, Union[Tuple[int, int, int, List[Evaluation]], Exception]]: - """Process a single item and return (item_id, result).""" async with semaphore: - item_id = self._get_item_id(item, scope) try: result = await self._process_batch_evaluation_item( item=item, - scope=scope, mapper=mapper, evaluators=evaluators, composite_evaluator=composite_evaluator, @@ -1029,25 +1003,27 @@ async def process_item( _add_observation_scores_to_trace=_add_observation_scores_to_trace, evaluator_stats_dict=evaluator_stats_dict, ) - return (item_id, result) + return (item.id, result) except Exception as e: - return (item_id, e) + return (item.id, e) - # Run all items in batch concurrently - tasks = [process_item(item) for item in items_to_process] - results = await asyncio.gather(*tasks) + items_to_process = self._deduplicate_page( + items, skip_ids=previous_page_ids | {resumed_item_id} + ) + previous_page_ids = {item.id for item in items} + + results = await asyncio.gather( + *[process_item(item) for item in items_to_process] + ) - # Process results and update statistics for item, (item_id, result) in zip(items_to_process, results): if isinstance(result, Exception): - # Item processing failed total_items_failed += 1 failed_item_ids.append(item_id) error_type = type(result).__name__ error_summary[error_type] = error_summary.get(error_type, 0) + 1 logger.warning("Item %s failed: %s", item_id, result) else: - # Item processed successfully total_items_processed += 1 scores_created, composite_created, evals_failed, evaluations = ( result @@ -1055,36 +1031,19 @@ async def process_item( total_scores_created += scores_created total_composite_scores_created += composite_created total_evaluations_failed += evals_failed - - # Store evaluations for this item item_evaluations[item_id] = evaluations - if normalized_additional_trace_tags: - trace_id = ( - item_id - if scope == "traces" - else cast(ObservationsView, item).trace_id - ) - - if trace_id and trace_id not in updated_trace_ids: - self.client._create_trace_tags_via_ingestion( - trace_id=trace_id, - tags=normalized_additional_trace_tags, - ) - updated_trace_ids.add(trace_id) - - # Update last processed tracking - last_item_timestamp = self._get_item_timestamp(item, scope) - last_item_id = item_id + if items: + last_item_timestamp = items[-1].start_time.isoformat() + last_item_id = items[-1].id if verbose: if max_items is not None and max_items > 0: - progress_pct = total_items_processed / max_items * 100 logger.info( "Progress: %s/%s items (%.1f%%), %s scores created", total_items_processed, max_items, - progress_pct, + total_items_processed / max_items * 100, total_scores_created, ) else: @@ -1094,40 +1053,22 @@ async def process_item( total_scores_created, ) - # Check if we should continue to next page - if len(items) < fetch_batch_size: - # Last page - no more items available + cursor = next_cursor + if cursor is None or not items: has_more = False - else: - page += 1 - - # Check max_items again before next fetch - if max_items is not None and total_items_fetched >= max_items: - has_more = True # More items exist but we're stopping - break + break - # Flush all scores to Langfuse if verbose: logger.info("Flushing scores to Langfuse...") self.client.flush() - # Build final result - duration = time.time() - start_time - if verbose: logger.info( "Batch evaluation complete: %s items processed in %.2fs", total_items_processed, - duration, + time.time() - start_time, ) - # Completed successfully if we either: - # 1. Ran out of items (has_more is False), OR - # 2. Hit max_items limit (intentionally stopped) - completed_successfully = not has_more or ( - max_items is not None and total_items_fetched >= max_items - ) - return self._build_result( total_items_fetched=total_items_fetched, total_items_processed=total_items_processed, @@ -1136,69 +1077,54 @@ async def process_item( total_composite_scores_created=total_composite_scores_created, total_evaluations_failed=total_evaluations_failed, evaluator_stats_dict=evaluator_stats_dict, - resume_token=None, # No resume needed on successful completion - completed=completed_successfully, + resume_token=build_resume_token() if has_more else None, + completed=True, start_time=start_time, failed_item_ids=failed_item_ids, error_summary=error_summary, - has_more_items=( - has_more and max_items is not None and total_items_fetched >= max_items - ), + has_more_items=has_more, item_evaluations=item_evaluations, ) async def _fetch_batch_with_retry( self, *, - scope: str, filter: Optional[str], - page: int, + cursor: Optional[str], limit: int, max_retries: int, fields: Optional[str], - ) -> List[Union[TraceWithFullDetails, ObservationsView]]: - """Fetch a batch of items with retry logic. + ) -> Tuple[List[ObservationV2], Optional[str]]: + """Fetch one page of observations from the v2 observations API. Args: - scope: The type of items ("traces", "observations"). filter: JSON filter string for querying. - page: Page number (1-indexed). - limit: Number of items per page. + cursor: Cursor returned with the previous page; None for the first page. + limit: Number of items to request. max_retries: Maximum number of retry attempts. - verbose: Whether to log retry attempts. - fields: Trace fields to fetch + fields: Comma-separated v2 field groups to include. Returns: - List of items from the API. + Tuple of the page's observations and the cursor for the next page + (None when there are no more pages). Raises: Exception: If all retry attempts fail. """ - if scope == "traces": - response = self.client.api.trace.list( - page=page, - limit=limit, - filter=filter, - request_options={"max_retries": max_retries}, - fields=fields, - ) # type: ignore - return list(response.data) # type: ignore - elif scope == "observations": - response = self.client.api.legacy.observations_v1.get_many( - page=page, - limit=limit, - filter=filter, - request_options={"max_retries": max_retries}, - ) # type: ignore - return list(response.data) # type: ignore - else: - error_message = f"Invalid scope: {scope}" - raise ValueError(error_message) + response = await asyncio.to_thread( + self.client.api.observations.get_many, + fields=_with_time_fields(fields), + limit=limit, + cursor=cursor, + filter=filter, + request_options={"max_retries": max_retries}, + ) + + return list(response.data), response.meta.cursor async def _process_batch_evaluation_item( self, - item: Union[TraceWithFullDetails, ObservationsView], - scope: str, + item: ObservationV2, mapper: MapperFunction, evaluators: List[EvaluatorFunction], composite_evaluator: Optional[CompositeEvaluatorFunction], @@ -1209,8 +1135,7 @@ async def _process_batch_evaluation_item( """Process a single item: map, evaluate, create scores. Args: - item: The API response object to evaluate. - scope: The type of item ("traces", "observations"). + item: The observation to evaluate. mapper: Function to transform item to evaluator inputs. evaluators: List of evaluator functions. composite_evaluator: Optional composite evaluator function. @@ -1225,14 +1150,16 @@ async def _process_batch_evaluation_item( Raises: Exception: If mapping fails or item processing encounters fatal error. """ + if not item.trace_id: + message = f"Observation {item.id} has no trace_id" + raise ValueError(message) + scores_created = 0 composite_scores_created = 0 evaluations_failed = 0 - # Run mapper to transform item evaluator_inputs = await self._run_mapper(mapper, item) - # Run all evaluators evaluations: List[Evaluation] = [] for evaluator in evaluators: evaluator_name = getattr(evaluator, "__name__", "unknown_evaluator") @@ -1253,31 +1180,23 @@ async def _process_batch_evaluation_item( evaluations.extend(eval_results) except Exception as e: - # Evaluator failed - log warning and continue with other evaluators stats.failed_runs += 1 evaluations_failed += 1 logger.warning( "Evaluator %s failed on item %s: %s", evaluator_name, - self._get_item_id(item, scope), + item.id, e, ) - # Create scores for item-level evaluations - item_id = self._get_item_id(item, scope) for evaluation in evaluations: - scores_created += self._create_score_for_scope( - scope=scope, - item_id=item_id, - trace_id=cast(ObservationsView, item).trace_id - if scope == "observations" - else None, + scores_created += self._create_score( + item=item, evaluation=evaluation, additional_metadata=metadata, add_observation_score_to_trace=_add_observation_scores_to_trace, ) - # Run composite evaluator if provided and we have evaluations if composite_evaluator and evaluations: try: composite_evals = await self._run_composite_evaluator( @@ -1289,24 +1208,18 @@ async def _process_batch_evaluation_item( evaluations=evaluations, ) - # Create scores for all composite evaluations for composite_eval in composite_evals: - composite_scores_created += self._create_score_for_scope( - scope=scope, - item_id=item_id, - trace_id=cast(ObservationsView, item).trace_id - if scope == "observations" - else None, + composite_scores_created += self._create_score( + item=item, evaluation=composite_eval, additional_metadata=metadata, add_observation_score_to_trace=_add_observation_scores_to_trace, ) - # Add composite evaluations to the list evaluations.extend(composite_evals) except Exception as e: - logger.warning("Composite evaluator failed on item %s: %s", item_id, e) + logger.warning("Composite evaluator failed on item %s: %s", item.id, e) return ( scores_created, @@ -1337,11 +1250,9 @@ async def _run_evaluator_internal( """ result = evaluator(**kwargs) - # Handle async evaluators if asyncio.iscoroutine(result): result = await result - # Normalize to list if isinstance(result, (dict, Evaluation)): return [result] # type: ignore elif isinstance(result, list): @@ -1352,13 +1263,13 @@ async def _run_evaluator_internal( async def _run_mapper( self, mapper: MapperFunction, - item: Union[TraceWithFullDetails, ObservationsView], + item: ObservationV2, ) -> EvaluatorInputs: """Run mapper function (handles both sync and async mappers). Args: mapper: The mapper function to run. - item: The API response object to map. + item: The observation to map. Returns: EvaluatorInputs instance. @@ -1406,7 +1317,6 @@ async def _run_composite_evaluator( if asyncio.iscoroutine(result): result = await result - # Normalize to list (same as regular evaluator) if isinstance(result, (dict, Evaluation)): return [result] # type: ignore elif isinstance(result, list): @@ -1414,22 +1324,18 @@ async def _run_composite_evaluator( else: return [] - def _create_score_for_scope( + def _create_score( self, *, - scope: str, - item_id: str, - trace_id: Optional[str] = None, + item: ObservationV2, evaluation: Evaluation, additional_metadata: Optional[Dict[str, Any]], add_observation_score_to_trace: bool = False, ) -> int: - """Create a score linked to the appropriate entity based on scope. + """Create a score on the evaluated observation, optionally duplicated onto its trace. Args: - scope: The type of entity ("traces", "observations"). - item_id: The ID of the entity. - trace_id: The trace ID of the entity; required if scope=observations + item: The evaluated observation. evaluation: The evaluation result to create a score from. additional_metadata: Additional metadata to merge with evaluation metadata. add_observation_score_to_trace: Whether to duplicate observation @@ -1438,165 +1344,104 @@ def _create_score_for_scope( Returns: Number of score events created. """ - # Merge metadata score_metadata = { **(evaluation.metadata or {}), **(additional_metadata or {}), } + score_kwargs: Dict[str, Any] = { + "name": evaluation.name, + "value": evaluation.value, + "comment": evaluation.comment, + "metadata": score_metadata, + "data_type": evaluation.data_type, + "config_id": evaluation.config_id, + } - if scope == "traces": - self.client.create_score( - trace_id=item_id, - name=evaluation.name, - value=evaluation.value, # type: ignore - comment=evaluation.comment, - metadata=score_metadata, - data_type=evaluation.data_type, # type: ignore[arg-type] - config_id=evaluation.config_id, - ) + self.client.create_score( + observation_id=item.id, trace_id=item.trace_id, **score_kwargs + ) + if not add_observation_score_to_trace: return 1 - elif scope == "observations": - self.client.create_score( - observation_id=item_id, - trace_id=trace_id, - name=evaluation.name, - value=evaluation.value, # type: ignore - comment=evaluation.comment, - metadata=score_metadata, - data_type=evaluation.data_type, # type: ignore[arg-type] - config_id=evaluation.config_id, - ) - score_count = 1 - - if add_observation_score_to_trace and trace_id: - self.client.create_score( - trace_id=trace_id, - name=evaluation.name, - value=evaluation.value, # type: ignore - comment=evaluation.comment, - metadata=score_metadata, - data_type=evaluation.data_type, # type: ignore[arg-type] - config_id=evaluation.config_id, - ) - score_count += 1 - - return score_count - return 0 + self.client.create_score(trace_id=item.trace_id, **score_kwargs) + return 2 - def _build_timestamp_filter( - self, - original_filter: Optional[str], + @staticmethod + def _build_filter( + *, + filter: Optional[str], resume_from: Optional[BatchEvaluationResumeToken], ) -> Optional[str]: - """Build filter with timestamp constraint for resume capability. + """Combine the user filter with the resume constraint. + + Constraints are added as JSON filter conditions rather than query + parameters because the API drops a query-parameter filter whenever the + JSON filter has a condition on the same column. Args: - original_filter: The original JSON filter string. - resume_from: Optional resume token with timestamp information. + filter: The user-provided JSON filter string (a JSON array). + resume_from: Optional resume token. Returns: - Modified filter string with timestamp constraint, or original filter. - """ - if not resume_from: - return original_filter + The JSON filter string to send, or None if there are no conditions. - # Parse original filter (should be array) or create empty array - try: - filter_list = json.loads(original_filter) if original_filter else [] - if not isinstance(filter_list, list): - logger.warning( - "Filter should be a JSON array, got: %s", type(filter_list).__name__ + Raises: + ValueError: If the filter is not a JSON array. + """ + conditions: List[Any] = [] + if filter: + try: + parsed = json.loads(filter) + except json.JSONDecodeError as e: + message = f"filter must be a JSON array: {e}" + raise ValueError(message) from e + if not isinstance(parsed, list): + message = ( + "filter must be a JSON array of conditions, " + f"got {type(parsed).__name__}" ) - filter_list = [] - except json.JSONDecodeError: - logger.warning( - "Invalid JSON in original filter, ignoring: %s", original_filter + raise ValueError(message) + conditions.extend(parsed) + + # Results are ordered by start time descending, so items that remain + # after the last processed one started before it. The cursor resumes + # exactly; the timestamp is the fallback for tokens without a cursor. + if ( + resume_from is not None + and resume_from.cursor is None + and resume_from.last_processed_timestamp + ): + conditions.append( + { + "type": "datetime", + "column": "startTime", + "operator": "<=", + "value": resume_from.last_processed_timestamp, + } ) - filter_list = [] - - # Add timestamp constraint to filter array - timestamp_field = self._get_timestamp_field_for_scope(resume_from.scope) - timestamp_filter = { - "type": "datetime", - "column": timestamp_field, - "operator": ">", - "value": resume_from.last_processed_timestamp, - } - filter_list.append(timestamp_filter) - - return json.dumps(filter_list) - - @staticmethod - def _get_item_id( - item: Union[TraceWithFullDetails, ObservationsView], - scope: str, - ) -> str: - """Extract ID from item based on scope. - - Args: - item: The API response object. - scope: The type of item. - - Returns: - The item's ID. - """ - return item.id - - @staticmethod - def _get_item_timestamp( - item: Union[TraceWithFullDetails, ObservationsView], - scope: str, - ) -> str: - """Extract timestamp from item based on scope. - Args: - item: The API response object. - scope: The type of item. - - Returns: - ISO 8601 timestamp string. - """ - if scope == "traces": - # Type narrowing for traces - if hasattr(item, "timestamp"): - return item.timestamp.isoformat() # type: ignore[attr-defined] - elif scope == "observations": - # Type narrowing for observations - if hasattr(item, "start_time"): - return item.start_time.isoformat() # type: ignore[attr-defined] - return "" + return json.dumps(conditions) if conditions else None @staticmethod - def _get_timestamp_field_for_scope(scope: str) -> str: - """Get the timestamp field name for filtering based on scope. - - Args: - scope: The type of items. + def _deduplicate_page( + items: List[ObservationV2], *, skip_ids: Set[Optional[str]] + ) -> List[ObservationV2]: + """Keep one row per observation id, preferring the most recently updated. - Returns: - The field name to use in filters. + Until ClickHouse merges them, the events table can return several rows + for one observation, within a page or across a page boundary. """ - if scope == "traces": - return "timestamp" - elif scope == "observations": - return "start_time" - return "timestamp" # Default - - @staticmethod - def _dedupe_tags(tags: Optional[List[str]]) -> List[str]: - """Deduplicate tags while preserving order.""" - if tags is None: - return [] - - deduped: List[str] = [] - seen = set() - for tag in tags: - if tag not in seen: - deduped.append(tag) - seen.add(tag) - - return deduped + latest_by_id: Dict[str, ObservationV2] = {} + for item in items: + if item.id in skip_ids: + continue + current = latest_by_id.get(item.id) + if current is None or (item.updated_at or item.start_time) > ( + current.updated_at or current.start_time + ): + latest_by_id[item.id] = item + + return list(latest_by_id.values()) def _build_result( self, @@ -1625,12 +1470,12 @@ def _build_result( total_composite_scores_created: Scores from composite evaluator. total_evaluations_failed: Individual evaluator failures. evaluator_stats_dict: Per-evaluator statistics. - resume_token: Resume token if incomplete. - completed: Whether evaluation completed fully. + resume_token: Resume token if the run can be continued. + completed: Whether evaluation completed without a fetch failure. start_time: Start time (unix timestamp). failed_item_ids: IDs of failed items. error_summary: Error type counts. - has_more_items: Whether more items exist. + has_more_items: Whether more items exist beyond max_items. item_evaluations: Dictionary mapping item IDs to their evaluation results. Returns: diff --git a/langfuse/experiment.py b/langfuse/experiment.py index aa5481829..ee29d8611 100644 --- a/langfuse/experiment.py +++ b/langfuse/experiment.py @@ -6,6 +6,7 @@ """ import asyncio +import warnings from datetime import datetime from typing import ( TYPE_CHECKING, @@ -216,6 +217,14 @@ def __init__( self.config_id = config_id +def _warn_deprecated_alias(old: str, new: str) -> None: + warnings.warn( + f"{old} is deprecated and will be removed in a future major version. Use {new} instead.", + DeprecationWarning, + stacklevel=3, + ) + + class ExperimentItemResult: """Result structure for individual experiment items. @@ -233,8 +242,8 @@ class ExperimentItemResult: contains a name, value, optional comment, and optional metadata. trace_id: Optional Langfuse trace ID for this item's execution. Used to link the experiment result with the detailed trace in Langfuse UI. - dataset_run_id: Optional dataset run ID if this item was part of a - Langfuse dataset. None for local experiments. + experiment_id: ID of the experiment run this item belongs to. + dataset_run_id: Deprecated alias of `experiment_id`. Examples: Accessing item result data: @@ -275,7 +284,8 @@ def __init__( output: Any, evaluations: List[Evaluation], trace_id: Optional[str], - dataset_run_id: Optional[str], + experiment_id: Optional[str] = None, + dataset_run_id: Optional[str] = None, ): """Initialize an ExperimentItemResult with the provided data. @@ -284,7 +294,9 @@ def __init__( output: The actual output produced by the task function for this item. evaluations: List of evaluation results for this item. trace_id: Optional Langfuse trace ID for this item's execution. - dataset_run_id: Optional dataset run ID if this item was part of a Langfuse dataset. + experiment_id: ID of the experiment run this item belongs to. + dataset_run_id: Deprecated alias of `experiment_id`. Used only when + `experiment_id` is not provided. Note: All arguments must be provided as keywords. Positional arguments will raise a TypeError. @@ -293,7 +305,13 @@ def __init__( self.output = output self.evaluations = evaluations self.trace_id = trace_id - self.dataset_run_id = dataset_run_id + self.experiment_id = experiment_id or dataset_run_id + + @property + def dataset_run_id(self) -> Optional[str]: + """Deprecated alias of `experiment_id`.""" + _warn_deprecated_alias("ExperimentItemResult.dataset_run_id", "experiment_id") + return self.experiment_id class ExperimentResult: @@ -312,10 +330,13 @@ class ExperimentResult: run_evaluations: List of aggregate evaluation results computed across all items, such as average scores, statistical summaries, or cross-item analyses. experiment_id: ID of the experiment run propagated across all items. For - Langfuse datasets, this matches the dataset run ID. For local experiments, - this is a stable SDK-generated identifier for the run. - dataset_run_id: Optional ID of the dataset run in Langfuse (when using Langfuse datasets). - dataset_run_url: Optional direct URL to view the experiment results in Langfuse UI. + Langfuse datasets, it is derived from the project, dataset and run name, + so re-running with the same `run_name` adds to the same experiment. For + local experiments, it is a random SDK-generated identifier for the run. + experiment_url: Optional direct URL to view the experiment results in the + Langfuse UI. None if the project ID could not be resolved. + dataset_run_id: Deprecated alias of `experiment_id`. + dataset_run_url: Deprecated alias of `experiment_url`. Examples: Basic usage with local dataset: @@ -347,8 +368,8 @@ class ExperimentResult: ) # View in Langfuse UI - if result.dataset_run_url: - print(f"View detailed results: {result.dataset_run_url}") + if result.experiment_url: + print(f"View detailed results: {result.experiment_url}") ``` Formatted output: @@ -373,6 +394,7 @@ def __init__( item_results: List[ExperimentItemResult], run_evaluations: List[Evaluation], experiment_id: str, + experiment_url: Optional[str] = None, dataset_run_id: Optional[str] = None, dataset_run_url: Optional[str] = None, ): @@ -385,8 +407,11 @@ def __init__( item_results: List of results from processing individual dataset items. run_evaluations: List of aggregate evaluation results for the entire run. experiment_id: ID of the experiment run. - dataset_run_id: Optional ID of the dataset run (for Langfuse datasets). - dataset_run_url: Optional URL to view results in Langfuse UI. + experiment_url: Optional URL to view results in Langfuse UI. + dataset_run_id: Deprecated and ignored; `dataset_run_id` always + returns `experiment_id`. + dataset_run_url: Deprecated alias of `experiment_url`. Used only when + `experiment_url` is not provided. """ self.name = name self.run_name = run_name @@ -394,8 +419,19 @@ def __init__( self.item_results = item_results self.run_evaluations = run_evaluations self.experiment_id = experiment_id - self.dataset_run_id = dataset_run_id - self.dataset_run_url = dataset_run_url + self.experiment_url = experiment_url or dataset_run_url + + @property + def dataset_run_id(self) -> str: + """Deprecated alias of `experiment_id`.""" + _warn_deprecated_alias("ExperimentResult.dataset_run_id", "experiment_id") + return self.experiment_id + + @property + def dataset_run_url(self) -> Optional[str]: + """Deprecated alias of `experiment_url`.""" + _warn_deprecated_alias("ExperimentResult.dataset_run_url", "experiment_url") + return self.experiment_url def format(self, *, include_item_results: bool = False) -> str: r"""Format the experiment result for human-readable display. @@ -426,7 +462,7 @@ def format(self, *, include_item_results: bool = False) -> str: - List of all evaluation metrics that were applied - Average scores across all items for each numeric metric - Run-level evaluation results with comments - - Dataset run URL for viewing in Langfuse UI (if applicable) + - Experiment URL for viewing in Langfuse UI (if available) - Individual item details including inputs, outputs, and scores (if requested) Examples: @@ -588,9 +624,8 @@ def format(self, *, include_item_results: bool = False) -> str: output += f"\n 💭 {run_eval.comment}" output += "\n" - # Add dataset run URL if available - if self.dataset_run_url: - output += f"\n🔗 Dataset Run:\n {self.dataset_run_url}" + if self.experiment_url: + output += f"\n🔗 Experiment:\n {self.experiment_url}" return output @@ -845,7 +880,7 @@ def __call__( - output: The task function's output for this item - evaluations: List of item-level evaluation results - trace_id: Langfuse trace ID for this execution - - dataset_run_id: Dataset run ID (if using Langfuse datasets) + - experiment_id: ID of the experiment run Note: This list only includes items that were successfully processed. Failed items are excluded but logged separately. @@ -1106,7 +1141,7 @@ def __init__( dataset_version: Optional pinned dataset version. Injected by the action when ``dataset_version`` is configured. metadata: Default metadata attached to every experiment trace and - the dataset run. The action injects GitHub-sourced tags (SHA, + the experiment run. The action injects GitHub-sourced tags (SHA, PR link, workflow run link, branch, GH user, etc.). Merged with any ``metadata`` passed to :meth:`run_experiment`, with user-supplied keys winning on collision. diff --git a/langfuse/langchain/CallbackHandler.py b/langfuse/langchain/CallbackHandler.py index f6126a066..898a6b22c 100644 --- a/langfuse/langchain/CallbackHandler.py +++ b/langfuse/langchain/CallbackHandler.py @@ -38,50 +38,25 @@ from langfuse.types import TraceContext try: - import langchain - - if langchain.__version__.startswith("1"): - # Langchain v1 - from langchain_core.agents import AgentAction, AgentFinish - from langchain_core.callbacks import ( - BaseCallbackHandler as LangchainBaseCallbackHandler, - ) - from langchain_core.documents import Document - from langchain_core.messages import ( - AIMessage, - BaseMessage, - ChatMessage, - FunctionMessage, - HumanMessage, - SystemMessage, - ToolMessage, - ) - from langchain_core.outputs import ChatGeneration, LLMResult - - else: - # Langchain v0 - from langchain.callbacks.base import ( # type: ignore - BaseCallbackHandler as LangchainBaseCallbackHandler, - ) - from langchain.schema.agent import AgentAction, AgentFinish # type: ignore - from langchain.schema.document import Document # type: ignore - from langchain_core.messages import ( - AIMessage, - BaseMessage, - ChatMessage, - FunctionMessage, - HumanMessage, - SystemMessage, - ToolMessage, - ) - from langchain_core.outputs import ( - ChatGeneration, - LLMResult, - ) + from langchain_core.agents import AgentAction, AgentFinish + from langchain_core.callbacks import ( + BaseCallbackHandler as LangchainBaseCallbackHandler, + ) + from langchain_core.documents import Document + from langchain_core.messages import ( + AIMessage, + BaseMessage, + ChatMessage, + FunctionMessage, + HumanMessage, + SystemMessage, + ToolMessage, + ) + from langchain_core.outputs import ChatGeneration, LLMResult except ImportError: raise ModuleNotFoundError( - "Please install langchain to use the Langfuse langchain integration: 'pip install langchain'" + "Please install langchain-core>=1 to use the Langfuse langchain integration: 'pip install \"langchain-core>=1\"'" ) LANGSMITH_TAG_HIDDEN: str = "langsmith:hidden" @@ -1344,7 +1319,6 @@ def on_llm_end( if generation is not None: generation.update( output=extracted_response, - usage=llm_usage, usage_details=llm_usage, input=kwargs.get("inputs"), model=model, diff --git a/langfuse/model.py b/langfuse/model.py index 69d721597..30c6fe0f3 100644 --- a/langfuse/model.py +++ b/langfuse/model.py @@ -9,14 +9,11 @@ CreateDatasetRequest, # noqa Dataset, # noqa DatasetItem, # noqa - DatasetRun, # noqa DatasetStatus, # noqa MapValue, # noqa - Observation, # noqa Prompt, Prompt_Chat, Prompt_Text, - TraceWithFullDetails, # noqa ) from langfuse.logger import langfuse_logger diff --git a/langfuse/openai.py b/langfuse/openai.py index 16e7f1c1f..43e00b54a 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -80,24 +80,6 @@ class OpenAiDefinition: max_version: Optional[str] = None -OPENAI_METHODS_V0 = [ - OpenAiDefinition( - module="openai", - object="ChatCompletion", - method="create", - type="chat", - sync=True, - ), - OpenAiDefinition( - module="openai", - object="Completion", - method="create", - type="completion", - sync=True, - ), -] - - OPENAI_METHODS_V1 = [ OpenAiDefinition( module="openai.resources.chat.completions", @@ -798,8 +780,7 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any: model, usage, finish_reason, service_tier = None, None, None, None for chunk in chunks: - if _is_openai_v1(): - chunk = chunk.__dict__ + chunk = chunk.__dict__ model = model or chunk.get("model", None) or None service_tier = service_tier or chunk.get("service_tier", None) or None @@ -810,15 +791,14 @@ def _extract_streamed_openai_response(resource: Any, chunks: Any) -> Any: choices = chunk.get("choices") or [] for choice in choices: - if _is_openai_v1(): - choice = choice.__dict__ + choice = choice.__dict__ if resource.type == "chat": delta = choice.get("delta", None) choice_finish_reason = choice.get("finish_reason", None) if choice_finish_reason is not None: finish_reason = choice_finish_reason - if _is_openai_v1() and delta is not None: + if delta is not None: delta = delta.__dict__ if delta is None: @@ -956,7 +936,7 @@ def _get_langfuse_data_from_default_response( if len(choices) > 0: choice = choices[-1] - completion = choice.text if _is_openai_v1() else choice.get("text", None) + completion = choice.text elif resource.object == "Responses" or resource.object == "AsyncResponses": completion = _extract_response_api_completion(response.get("output", {})) @@ -968,17 +948,11 @@ def _get_langfuse_data_from_default_response( if len(choices) > 1: completion = [ _extract_chat_response(choice.message.__dict__) - if _is_openai_v1() - else choice.get("message", None) for choice in choices ] else: choice = choices[0] - completion = ( - _extract_chat_response(choice.message.__dict__) - if _is_openai_v1() - else choice.get("message", None) - ) + completion = _extract_chat_response(choice.message.__dict__) elif resource.type == "embedding": data = response.get("data") or [] @@ -1015,16 +989,12 @@ def _merge_service_tier_into_model_parameters( return {**(model_parameters or {}), "service_tier": service_tier} -def _is_openai_v1() -> bool: - return Version(openai.__version__) >= Version("1.0.0") - - def _is_streaming_response(response: Any) -> bool: return ( isinstance(response, types.GeneratorType) or isinstance(response, types.AsyncGeneratorType) - or (_is_openai_v1() and isinstance(response, openai.Stream)) - or (_is_openai_v1() and isinstance(response, openai.AsyncStream)) + or isinstance(response, openai.Stream) + or isinstance(response, openai.AsyncStream) ) @@ -1034,9 +1004,6 @@ def _is_streaming_response(response: Any) -> bool: def _install_openai_stream_iteration_hooks() -> None: global _openai_stream_iter_hook_installed - if not _is_openai_v1(): - return - if not _openai_stream_iter_hook_installed: original_iter = openai.Stream.__iter__ original_aiter = openai.AsyncStream.__aiter__ @@ -1318,7 +1285,7 @@ def _wrap( try: openai_response = wrapped(**arg_extractor.get_openai_args()) - if _is_openai_v1() and isinstance(openai_response, openai.Stream): + if isinstance(openai_response, openai.Stream): return _instrument_openai_stream( resource=open_ai_resource, response=openai_response, @@ -1338,9 +1305,7 @@ def _wrap( model, completion, usage, service_tier = ( _get_langfuse_data_from_default_response( open_ai_resource, - (parsed_response and parsed_response.__dict__) - if _is_openai_v1() - else parsed_response, + parsed_response and parsed_response.__dict__, ) ) @@ -1407,7 +1372,7 @@ async def _wrap_async( try: openai_response = await wrapped(**arg_extractor.get_openai_args()) - if _is_openai_v1() and isinstance(openai_response, openai.AsyncStream): + if isinstance(openai_response, openai.AsyncStream): return _instrument_openai_async_stream( resource=open_ai_resource, response=openai_response, @@ -1427,15 +1392,12 @@ async def _wrap_async( model, completion, usage, service_tier = ( _get_langfuse_data_from_default_response( open_ai_resource, - (parsed_response and parsed_response.__dict__) - if _is_openai_v1() - else parsed_response, + parsed_response and parsed_response.__dict__, ) ) generation.update( model=model, output=completion, - usage=usage, # backward compat for all V2 self hosters usage_details=usage, cost_details=_parse_cost(parsed_response.usage) if hasattr(parsed_response, "usage") @@ -1460,9 +1422,7 @@ async def _wrap_async( def register_tracing() -> None: - resources = OPENAI_METHODS_V1 if _is_openai_v1() else OPENAI_METHODS_V0 - - for resource in resources: + for resource in OPENAI_METHODS_V1: if resource.min_version is not None and Version(openai.__version__) < Version( resource.min_version ): diff --git a/langfuse/types.py b/langfuse/types.py index 66bddbca6..d20bd6a55 100644 --- a/langfuse/types.py +++ b/langfuse/types.py @@ -24,6 +24,7 @@ def my_evaluator(*, output: str, **kwargs) -> Evaluation: Dict, Literal, Mapping, + NotRequired, Optional, Protocol, Sequence, @@ -31,12 +32,6 @@ def my_evaluator(*, output: str, **kwargs) -> Evaluation: Union, ) -try: - from typing import NotRequired # type: ignore -except ImportError: - from typing_extensions import NotRequired - - from langfuse.api import MediaContentType # Span attribute values accepted by the OpenTelemetry trace API. OpenTelemetry 1.45 diff --git a/pyproject.toml b/pyproject.toml index 8e05c69c8..5451528a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,15 @@ readme = "README.md" authors = [{ name = "langfuse", email = "developers@langfuse.com" }] license = "MIT" license-files = ["LICENSE"] -requires-python = ">=3.10,<4.0" +requires-python = ">=3.11,<4.0" +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", +] keywords = [ "langfuse", "llm", @@ -52,10 +60,12 @@ dev = [ "pytest-httpserver>=1.0.8,<2", "ruff>=0.15.2,<0.16", "mypy>=1.0.0,<2", - "openai>=0.27.8", - "langchain-openai>=0.0.5,<0.4", - "langchain>=1,<2", - "langgraph>=1,<2", + "openai>=1.0.0", + "langchain-core>=1,<2", + "langchain-openai>=1,<2", + # langgraph-checkpoint requires ormsgpack, which cannot be built for Python 3.15 yet + "langchain>=1,<2; python_version < '3.15'", + "langgraph>=1,<2; python_version < '3.15'", "autoevals>=0.0.130,<0.1", "opentelemetry-instrumentation-threading>=0.59b0,<1", "tenacity>=9.1.4", @@ -71,8 +81,9 @@ build-backend = "uv_build" [tool.uv] # Basic protection against supply chain attacks exclude-newer = "7 days" -# OpenTelemetry 1.45 ships the OTLP request size cap the SDK relies on -exclude-newer-package = { "opentelemetry-api" = "3 days", "opentelemetry-sdk" = "3 days", "opentelemetry-semantic-conventions" = "3 days", "opentelemetry-proto" = "3 days", "opentelemetry-exporter-otlp-proto-common" = "3 days", "opentelemetry-exporter-otlp-proto-http" = "3 days", "opentelemetry-exporter-otlp-common" = "3 days", "opentelemetry-exporter-http-transport" = "3 days", "opentelemetry-instrumentation" = "3 days", "opentelemetry-instrumentation-threading" = "3 days" } +# OpenTelemetry 1.45 ships the OTLP request size cap the SDK relies on; +# pydantic 2.14 is the first release with Python 3.15 wheels +exclude-newer-package = { "pydantic" = "3 days", "pydantic-core" = "3 days", "opentelemetry-api" = "3 days", "opentelemetry-sdk" = "3 days", "opentelemetry-semantic-conventions" = "3 days", "opentelemetry-proto" = "3 days", "opentelemetry-exporter-otlp-proto-common" = "3 days", "opentelemetry-exporter-otlp-proto-http" = "3 days", "opentelemetry-exporter-otlp-common" = "3 days", "opentelemetry-exporter-http-transport" = "3 days", "opentelemetry-instrumentation" = "3 days", "opentelemetry-instrumentation-threading" = "3 days" } [tool.uv.build-backend] module-root = "" @@ -139,7 +150,7 @@ module = "langfuse.api.*" disable_error_code = ["redundant-cast", "no-untyped-def"] [tool.ruff] -target-version = "py310" +target-version = "py311" [tool.ruff.lint] extend-select = [ diff --git a/scripts/patch_generated_score_compat.py b/scripts/patch_generated_score_compat.py deleted file mode 100644 index 46c23ec6b..000000000 --- a/scripts/patch_generated_score_compat.py +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env python3 -"""Preserve the legacy score-create API after Fern moves it to ``scores``. - -Fern owns ``langfuse/api`` and regenerates it from the main Langfuse repository. -This postprocessor is intentionally narrow and fail-closed: it only patches the -known generated shape where ``scores.create`` exists and -``legacy.score_v1.create`` no longer does. -""" - -from __future__ import annotations - -import argparse -import ast -from pathlib import Path - -CLIENT_IMPORTS = """\ -from ...commons.types.create_score_value import CreateScoreValue -from ...commons.types.score_data_type import ScoreDataType -from ...scores.client import ( - AsyncScoresClient as CanonicalAsyncScoresClient, - ScoresClient as CanonicalScoresClient, - OMIT, -) -from ...scores.types.create_score_response import CreateScoreResponse -from ...scores.types.create_score_source import CreateScoreSource -""" - -SYNC_CREATE_METHOD = '''\ - def create( - self, - *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: - """**Deprecated compatibility alias.** Use ``client.scores.create``.""" - return CanonicalScoresClient( - client_wrapper=self._raw_client._client_wrapper - ).create( - name=name, - value=value, - id=id, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - dataset_run_id=dataset_run_id, - comment=comment, - metadata=metadata, - environment=environment, - queue_id=queue_id, - data_type=data_type, - config_id=config_id, - source=source, - request_options=request_options, - ) - -''' - -ASYNC_CREATE_METHOD = '''\ - async def create( - self, - *, - name: str, - value: CreateScoreValue, - id: typing.Optional[str] = OMIT, - trace_id: typing.Optional[str] = OMIT, - session_id: typing.Optional[str] = OMIT, - observation_id: typing.Optional[str] = OMIT, - dataset_run_id: typing.Optional[str] = OMIT, - comment: typing.Optional[str] = OMIT, - metadata: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, - environment: typing.Optional[str] = OMIT, - queue_id: typing.Optional[str] = OMIT, - data_type: typing.Optional[ScoreDataType] = OMIT, - config_id: typing.Optional[str] = OMIT, - source: typing.Optional[CreateScoreSource] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> CreateScoreResponse: - """**Deprecated compatibility alias.** Use ``client.scores.create``.""" - return await CanonicalAsyncScoresClient( - client_wrapper=self._raw_client._client_wrapper - ).create( - name=name, - value=value, - id=id, - trace_id=trace_id, - session_id=session_id, - observation_id=observation_id, - dataset_run_id=dataset_run_id, - comment=comment, - metadata=metadata, - environment=environment, - queue_id=queue_id, - data_type=data_type, - config_id=config_id, - source=source, - request_options=request_options, - ) - -''' - -TYPE_NAMES = ( - "CreateScoreRequest", - "CreateScoreResponse", - "CreateScoreSource", -) -LEGACY_ALIAS_MARKER = ( - "**Deprecated compatibility alias.** Use ``client.scores.create``." -) - - -def _replace_once(contents: str, marker: str, replacement: str, *, path: Path) -> str: - count = contents.count(marker) - if count != 1: - raise RuntimeError( - f"Expected exactly one compatibility marker in {path}: {marker!r}; " - f"found {count}" - ) - return contents.replace(marker, replacement, 1) - - -def _append_type_exports(path: Path, imports_by_type: dict[str, str]) -> None: - exports_marker = "# Score-create compatibility aliases (LFE-10397)." - contents = path.read_text() - if exports_marker in contents: - return - - imports = "\n".join( - f"from {module_name} import {type_name}" - for type_name, module_name in imports_by_type.items() - ) - names = ", ".join(repr(type_name) for type_name in TYPE_NAMES) - defines_all = any( - ( - isinstance(node, ast.Assign) - and any( - isinstance(target, ast.Name) and target.id == "__all__" - for target in node.targets - ) - ) - or ( - isinstance(node, ast.AnnAssign) - and isinstance(node.target, ast.Name) - and node.target.id == "__all__" - ) - for node in ast.parse(contents).body - ) - exports = ( - f"__all__ = [*__all__, {names}]" if defines_all else f"__all__ = [{names}]" - ) - path.write_text(f"{contents.rstrip()}\n\n{exports_marker}\n{imports}\n{exports}\n") - - -def _create_type_aliases(types_dir: Path) -> None: - types_dir.mkdir(parents=True, exist_ok=True) - for type_name in TYPE_NAMES: - module_name = "".join( - f"_{character.lower()}" if character.isupper() else character - for character in type_name - ).lstrip("_") - alias_path = types_dir / f"{module_name}.py" - alias_path.write_text( - "# This compatibility alias is applied after Fern generation.\n\n" - f"from ....scores.types.{module_name} import {type_name}\n\n" - f'__all__ = ["{type_name}"]\n' - ) - - -def patch_generated_api(api_root: Path) -> bool: - canonical_client_path = api_root / "scores" / "client.py" - legacy_client_path = api_root / "legacy" / "score_v1" / "client.py" - - canonical_client = canonical_client_path.read_text() - legacy_client = legacy_client_path.read_text() - - canonical_has_create = ( - "\n def create(" in canonical_client - and "\n async def create(" in canonical_client - ) - legacy_has_create = ( - "\n def create(" in legacy_client - and "\n async def create(" in legacy_client - ) - - if legacy_has_create: - if LEGACY_ALIAS_MARKER in legacy_client and canonical_has_create: - return False - if canonical_has_create: - raise RuntimeError( - "Both canonical and legacy generated score clients define create; " - "refusing to introduce a third compatibility surface" - ) - return False - - if not canonical_has_create: - raise RuntimeError( - "Neither generated score client defines both sync and async create methods" - ) - - legacy_client = _replace_once( - legacy_client, - "from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper\n", - "from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper\n" - f"{CLIENT_IMPORTS}", - path=legacy_client_path, - ) - legacy_client = _replace_once( - legacy_client, - "\n def delete(", - f"\n{SYNC_CREATE_METHOD} def delete(", - path=legacy_client_path, - ) - legacy_client = _replace_once( - legacy_client, - "\n async def delete(", - f"\n{ASYNC_CREATE_METHOD} async def delete(", - path=legacy_client_path, - ) - legacy_client_path.write_text(legacy_client) - - legacy_package_path = api_root / "legacy" / "score_v1" / "__init__.py" - legacy_parent_package_path = api_root / "legacy" / "__init__.py" - legacy_types_path = api_root / "legacy" / "score_v1" / "types" / "__init__.py" - _create_type_aliases(legacy_types_path.parent) - - if not legacy_types_path.exists(): - legacy_types_path.write_text( - "# This package is generated by Fern and extended for compatibility.\n\n" - "__all__: list[str] = []\n" - ) - - _append_type_exports( - legacy_types_path, - { - "CreateScoreRequest": ".create_score_request", - "CreateScoreResponse": ".create_score_response", - "CreateScoreSource": ".create_score_source", - }, - ) - _append_type_exports( - legacy_package_path, {type_name: ".types" for type_name in TYPE_NAMES} - ) - _append_type_exports( - legacy_parent_package_path, - {type_name: ".score_v1" for type_name in TYPE_NAMES}, - ) - return True - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument( - "--api-root", - type=Path, - default=Path("langfuse/api"), - help="Path to the freshly generated Fern Python package", - ) - args = parser.parse_args() - changed = patch_generated_api(args.api_root) - print( - "Patched legacy score create compatibility alias." - if changed - else "No patch needed." - ) - - -if __name__ == "__main__": - main() diff --git a/tests/conftest.py b/tests/conftest.py index 0163842c6..97a78d212 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,10 +102,6 @@ def mock_init(self: Any, **kwargs: Any) -> None: from langfuse._client.span_filter import is_default_export_span self.public_key = kwargs.get("public_key", "test-public-key") - blocked_scopes = kwargs.get("blocked_instrumentation_scopes") - self.blocked_instrumentation_scopes = ( - blocked_scopes if blocked_scopes is not None else [] - ) self._should_export_span = ( kwargs.get("should_export_span") or is_default_export_span ) diff --git a/tests/e2e/test_batch_evaluation.py b/tests/e2e/test_batch_evaluation.py index 0632b21b8..1e2d83ce8 100644 --- a/tests/e2e/test_batch_evaluation.py +++ b/tests/e2e/test_batch_evaluation.py @@ -1,1139 +1,268 @@ -"""Comprehensive tests for batch evaluation functionality. +"""End-to-end tests for run_batched_evaluation against a Langfuse server. -This test suite covers the run_batched_evaluation method which allows evaluating -traces, observations, and sessions fetched from Langfuse with mappers, evaluators, -and composite evaluators. +Every run is restricted to a corpus seeded by this module (filtered by a unique +tag), so assertions do not depend on other data in the project. Runner logic +that does not need a server is covered in tests/unit/test_batch_evaluation.py. """ -import asyncio -import time +import json +from dataclasses import dataclass +from typing import Any, List import pytest from langfuse import get_client, propagate_attributes -from langfuse.batch_evaluation import ( - BatchEvaluationResult, - BatchEvaluationResumeToken, - EvaluatorInputs, - EvaluatorStats, -) +from langfuse.api import ObservationV2 +from langfuse.batch_evaluation import EvaluatorInputs from langfuse.experiment import Evaluation from tests.support.utils import create_uuid, get_api, wait_for_result -# ============================================================================ -# FIXTURES & SETUP -# ============================================================================ +TRACE_COUNT = 4 -# pytestmark = pytest.mark.skip(reason="Github CI runner overwhelmed by score volume") +@dataclass +class Corpus: + tag: str + filter: str + trace_ids: List[str] + root_ids: List[str] + child_ids: List[str] -@pytest.fixture -def langfuse_client(): - """Get a Langfuse client for testing.""" - return get_client() - - -@pytest.fixture -def sample_trace_name(): - """Generate a unique trace name for filtering.""" - return f"batch-eval-test-{create_uuid()}" - - -def _seed_trace_corpus( - *, trace_count: int = 6, tag: str | None = None -) -> tuple[str, list[str]]: - langfuse_client = get_client() - corpus_tag = tag or f"batch-eval-seed-{create_uuid()}" - trace_names: list[str] = [] - - for index in range(trace_count): - trace_name = f"{corpus_tag}-trace-{index}" - trace_names.append(trace_name) - with langfuse_client.start_as_current_observation(name=trace_name) as span: - with propagate_attributes(tags=[corpus_tag]): - span.set_trace_io( - input=f"Seed input {index}", - output=f"Seed output {index}", - ) +def _tag_filter(tag: str) -> str: + return json.dumps( + [ + { + "type": "arrayOptions", + "column": "tags", + "operator": "any of", + "value": [tag], + } + ] + ) - langfuse_client.flush() - filter_json = f'[{{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["{corpus_tag}"]}}]' +def _wait_for_observations(filter_json: str, expected_count: int) -> List[Any]: api = get_api(retry=False) - wait_for_result( - lambda: api.trace.list(filter=filter_json, limit=trace_count), - is_result_ready=lambda response: len(response.data) >= trace_count, + response = wait_for_result( + lambda: api.observations.get_many(filter=filter_json, limit=100), + is_result_ready=lambda r: len(r.data) >= expected_count, ) - - return corpus_tag, trace_names - - -@pytest.fixture(scope="module", autouse=True) -def seeded_batch_evaluation_traces(): - _seed_trace_corpus() + return list(response.data) -def simple_trace_mapper(*, item): - """Simple mapper for traces.""" +def _wait_for_scores(**kwargs: Any) -> List[Any]: + api = get_api(retry=False) + response = wait_for_result( + lambda: api.scores.get_many(fields="details,subject", **kwargs), + is_result_ready=lambda r: len(r.data) > 0, + ) + return list(response.data) + + +@pytest.fixture(scope="module") +def corpus() -> Corpus: + langfuse = get_client() + tag = f"batch-eval-{create_uuid()}" + trace_ids, root_ids, child_ids = [], [], [] + + for index in range(TRACE_COUNT): + with langfuse.start_as_current_observation(name=f"{tag}-root") as root: + with propagate_attributes(tags=[tag]): + with langfuse.start_as_current_observation( + as_type="generation", + name=f"{tag}-child", + input={"question": index}, + output=f"child answer {index}", + ) as child: + child_ids.append(child.id) + root.update(input={"question": index}, output=f"answer {index}") + trace_ids.append(root.trace_id) + root_ids.append(root.id) + + langfuse.flush() + _wait_for_observations(_tag_filter(tag), 2 * TRACE_COUNT) + + return Corpus( + tag=tag, + filter=_tag_filter(tag), + trace_ids=trace_ids, + root_ids=root_ids, + child_ids=child_ids, + ) + + +def io_mapper(*, item: ObservationV2) -> EvaluatorInputs: return EvaluatorInputs( - input=item.input if hasattr(item, "input") else None, - output=item.output if hasattr(item, "output") else None, - expected_output=None, - metadata={"trace_id": item.id}, + input=item.input, + output=item.output, + metadata={"trace_id": item.trace_id}, ) -def simple_evaluator(*, input, output, expected_output=None, metadata=None, **kwargs): - """Simple evaluator that returns a score based on output length.""" - if output is None: - return Evaluation(name="length_score", value=0.0, comment="No output") - - return Evaluation( - name="length_score", - value=float(len(str(output))) / 10.0, - comment=f"Length: {len(str(output))}", - ) +def length_evaluator(*, output, **kwargs): + return Evaluation(name="length", value=float(len(output or ""))) -# ============================================================================ -# BASIC FUNCTIONALITY TESTS -# ============================================================================ +def test_evaluates_every_observation(corpus): + seen: List[ObservationV2] = [] + def recording_mapper(*, item): + seen.append(item) + return io_mapper(item=item) -def test_run_batched_evaluation_on_observations_basic(langfuse_client): - """Test basic batch evaluation on traces.""" - result = langfuse_client.run_batched_evaluation( - scope="observations", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=1, - verbose=True, + result = get_client().run_batched_evaluation( + mapper=recording_mapper, + evaluators=[length_evaluator], + filter=corpus.filter, ) - # Validate result structure - assert isinstance(result, BatchEvaluationResult) - assert result.total_items_fetched >= 0 - assert result.total_items_processed >= 0 - assert result.total_scores_created >= 0 assert result.completed is True - assert isinstance(result.duration_seconds, float) - assert result.duration_seconds > 0 - - # Verify evaluator stats - assert len(result.evaluator_stats) == 1 - stats = result.evaluator_stats[0] - assert isinstance(stats, EvaluatorStats) - assert stats.name == "simple_evaluator" - - -def test_run_batched_evaluation_on_traces_basic(langfuse_client): - """Test basic batch evaluation on traces.""" - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=5, - verbose=True, - ) - - # Validate result structure - assert isinstance(result, BatchEvaluationResult) - assert result.total_items_fetched >= 0 - assert result.total_items_processed >= 0 - assert result.total_scores_created >= 0 - assert result.completed is True - assert isinstance(result.duration_seconds, float) - assert result.duration_seconds > 0 - - # Verify evaluator stats - assert len(result.evaluator_stats) == 1 - stats = result.evaluator_stats[0] - assert isinstance(stats, EvaluatorStats) - assert stats.name == "simple_evaluator" - - -def test_batch_evaluation_with_filter(langfuse_client): - """Test batch evaluation with JSON filter.""" - # Create a trace with specific tag - unique_tag = f"test-filter-{create_uuid()}" - with langfuse_client.start_as_current_observation( - name=f"filtered-trace-{create_uuid()}" - ) as span: - with propagate_attributes(tags=[unique_tag]): - span.set_trace_io( - input="Filtered test", - output="Filtered output", - ) - - langfuse_client.flush() - time.sleep(3) - - # Filter format: array of filter conditions - filter_json = f'[{{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["{unique_tag}"]}}]' - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - filter=filter_json, - verbose=True, - ) - - # Should only process the filtered trace - assert result.total_items_fetched >= 1 - assert result.completed is True - - -def test_batch_evaluation_with_metadata(langfuse_client): - """Test that additional metadata is added to all scores.""" - - def metadata_checking_evaluator(*, input, output, metadata=None, **kwargs): - return Evaluation( - name="test_score", - value=1.0, - metadata={"evaluator_data": "test"}, - ) - - additional_metadata = { - "batch_run_id": "test-batch-123", - "evaluation_version": "v2.0", + assert result.has_more_items is False + assert result.resume_token is None + assert result.total_items_fetched == 2 * TRACE_COUNT + assert result.total_items_processed == 2 * TRACE_COUNT + assert result.total_scores_created == 2 * TRACE_COUNT + assert set(result.item_evaluations) == set(corpus.root_ids + corpus.child_ids) + + by_id = {item.id: item for item in seen} + child = by_id[corpus.child_ids[0]] + assert isinstance(child.input, str) + assert json.loads(child.input) == {"question": 0} + assert child.output == "child answer 0" + + +def test_observation_scores_are_attached_to_observations(corpus): + score_name = f"obs-score-{create_uuid()}" + child_filter = json.loads(corpus.filter) + [ + { + "type": "string", + "column": "id", + "operator": "=", + "value": corpus.child_ids[1], + } + ] + + def observation_evaluator(**kwargs): + return Evaluation(name=score_name, value=0.5, comment="ok") + + result = get_client().run_batched_evaluation( + mapper=io_mapper, + evaluators=[observation_evaluator], + filter=json.dumps(child_filter), + metadata={"run": "e2e"}, + ) + + assert result.total_items_processed == 1 + + scores = _wait_for_scores( + trace_id=corpus.trace_ids[1], + observation_id=corpus.child_ids[1], + name=score_name, + ) + assert len(scores) == 1 + assert scores[0].subject.kind == "observation" + assert scores[0].subject.id == corpus.child_ids[1] + assert scores[0].subject.trace_id == corpus.trace_ids[1] + assert scores[0].comment == "ok" + assert scores[0].metadata == {"run": "e2e"} + + +def test_max_items_then_resume_covers_corpus_exactly_once(corpus): + langfuse = get_client() + run_kwargs: Any = { + "mapper": io_mapper, + "evaluators": [length_evaluator], + "filter": corpus.filter, + "fetch_batch_size": 2, } - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[metadata_checking_evaluator], - metadata=additional_metadata, - max_items=2, - ) - - assert result.total_scores_created > 0 - - # Verify scores were created with merged metadata - langfuse_client.flush() - time.sleep(3) - - # Note: In a real test, you'd verify via API that metadata was merged - # For now, just verify the operation completed - assert result.completed is True + first = langfuse.run_batched_evaluation(max_items=3, **run_kwargs) + assert first.total_items_fetched == 3 + assert first.has_more_items is True + assert first.resume_token is not None + assert first.resume_token.cursor is not None -def test_result_structure_fields(langfuse_client): - """Test that BatchEvaluationResult has all expected fields.""" - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=3, + second = langfuse.run_batched_evaluation( + resume_from=first.resume_token, **run_kwargs ) - # Check all result fields exist - assert hasattr(result, "total_items_fetched") - assert hasattr(result, "total_items_processed") - assert hasattr(result, "total_items_failed") - assert hasattr(result, "total_scores_created") - assert hasattr(result, "total_composite_scores_created") - assert hasattr(result, "total_evaluations_failed") - assert hasattr(result, "evaluator_stats") - assert hasattr(result, "resume_token") - assert hasattr(result, "completed") - assert hasattr(result, "duration_seconds") - assert hasattr(result, "failed_item_ids") - assert hasattr(result, "error_summary") - assert hasattr(result, "has_more_items") - assert hasattr(result, "item_evaluations") - - # Check types - assert isinstance(result.evaluator_stats, list) - assert isinstance(result.failed_item_ids, list) - assert isinstance(result.error_summary, dict) - assert isinstance(result.completed, bool) - assert isinstance(result.has_more_items, bool) - assert isinstance(result.item_evaluations, dict) - - -# ============================================================================ -# MAPPER FUNCTION TESTS -# ============================================================================ - - -def test_simple_mapper(langfuse_client): - """Test basic mapper functionality.""" - - def custom_mapper(*, item): - return EvaluatorInputs( - input=item.input if hasattr(item, "input") else "no input", - output=item.output if hasattr(item, "output") else "no output", - expected_output=None, - metadata={"custom_field": "test_value"}, - ) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=custom_mapper, - evaluators=[simple_evaluator], - max_items=2, + assert second.completed is True + assert second.resume_token is None + assert set(first.item_evaluations).isdisjoint(second.item_evaluations) + assert set(first.item_evaluations) | set(second.item_evaluations) == set( + corpus.root_ids + corpus.child_ids ) - assert result.total_items_processed > 0 - - -@pytest.mark.asyncio -async def test_async_mapper(langfuse_client): - """Test that async mappers work correctly.""" - - async def async_mapper(*, item): - await asyncio.sleep(0.01) # Simulate async work - return EvaluatorInputs( - input=item.input if hasattr(item, "input") else None, - output=item.output if hasattr(item, "output") else None, - expected_output=None, - metadata={"async": True}, - ) - - # Note: run_batched_evaluation is synchronous but handles async mappers - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=async_mapper, - evaluators=[simple_evaluator], - max_items=2, - ) - - assert result.total_items_processed > 0 - - -def test_mapper_failure_handling(langfuse_client): - """Test that mapper failures cause items to be skipped.""" - - def failing_mapper(*, item): - raise ValueError("Intentional mapper failure") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=failing_mapper, - evaluators=[simple_evaluator], - max_items=3, - ) - - # All items should fail due to mapper failures - assert result.total_items_failed > 0 - assert len(result.failed_item_ids) > 0 - assert "ValueError" in result.error_summary or "Exception" in result.error_summary - - -def test_mapper_with_missing_fields(langfuse_client): - """Test mapper handles traces with missing fields gracefully.""" - - def robust_mapper(*, item): - # Handle missing fields with defaults - input_val = getattr(item, "input", None) or "default_input" - output_val = getattr(item, "output", None) or "default_output" - - return EvaluatorInputs( - input=input_val, - output=output_val, - expected_output=None, - metadata={}, - ) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=robust_mapper, - evaluators=[simple_evaluator], - max_items=2, - ) - - assert result.total_items_processed > 0 - - -# ============================================================================ -# EVALUATOR TESTS -# ============================================================================ - - -def test_single_evaluator(langfuse_client): - """Test with a single evaluator.""" - - def quality_evaluator(*, input, output, **kwargs): - return Evaluation(name="quality", value=0.85, comment="High quality") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[quality_evaluator], - max_items=2, - ) - assert result.total_scores_created > 0 - assert len(result.evaluator_stats) == 1 - assert result.evaluator_stats[0].name == "quality_evaluator" +def test_fields_control_populated_field_groups(corpus): + seen: List[ObservationV2] = [] + def recording_mapper(*, item): + seen.append(item) + return EvaluatorInputs(input=None, output=None) -def test_multiple_evaluators(langfuse_client): - """Test with multiple evaluators running in parallel.""" - - def accuracy_evaluator(*, input, output, **kwargs): - return Evaluation(name="accuracy", value=0.9) - - def relevance_evaluator(*, input, output, **kwargs): - return Evaluation(name="relevance", value=0.8) - - def safety_evaluator(*, input, output, **kwargs): - return Evaluation(name="safety", value=1.0) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[accuracy_evaluator, relevance_evaluator, safety_evaluator], - max_items=2, - ) - - # Should have 3 evaluators - assert len(result.evaluator_stats) == 3 - assert result.total_scores_created >= result.total_items_processed * 3 - - -@pytest.mark.asyncio -async def test_async_evaluator(langfuse_client): - """Test that async evaluators work correctly.""" - - async def async_evaluator(*, input, output, **kwargs): - await asyncio.sleep(0.01) # Simulate async work - return Evaluation(name="async_score", value=0.75) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[async_evaluator], - max_items=2, - ) - - assert result.total_scores_created > 0 - - -def test_evaluator_returning_list(langfuse_client): - """Test evaluator that returns multiple Evaluations.""" - - def multi_score_evaluator(*, input, output, **kwargs): - return [ - Evaluation(name="score_1", value=0.8), - Evaluation(name="score_2", value=0.9), - Evaluation(name="score_3", value=0.7), - ] - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[multi_score_evaluator], - max_items=2, - ) - - # Should create 3 scores per item - assert result.total_scores_created >= result.total_items_processed * 3 - - -def test_evaluator_failure_statistics(langfuse_client): - """Test that evaluator failures are tracked in statistics.""" - - def working_evaluator(*, input, output, **kwargs): - return Evaluation(name="working", value=1.0) - - def failing_evaluator(*, input, output, **kwargs): - raise RuntimeError("Intentional evaluator failure") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[working_evaluator, failing_evaluator], - max_items=3, - ) - - # Verify evaluator stats - assert len(result.evaluator_stats) == 2 - - working_stats = next( - s for s in result.evaluator_stats if s.name == "working_evaluator" - ) - assert working_stats.successful_runs > 0 - assert working_stats.failed_runs == 0 - - failing_stats = next( - s for s in result.evaluator_stats if s.name == "failing_evaluator" - ) - assert failing_stats.failed_runs > 0 - assert failing_stats.successful_runs == 0 - - # Total evaluations failed should be tracked - assert result.total_evaluations_failed > 0 - - -def test_mixed_sync_async_evaluators(langfuse_client): - """Test mixing synchronous and asynchronous evaluators.""" - - def sync_evaluator(*, input, output, **kwargs): - return Evaluation(name="sync_score", value=0.8) - - async def async_evaluator(*, input, output, **kwargs): - await asyncio.sleep(0.01) - return Evaluation(name="async_score", value=0.9) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[sync_evaluator, async_evaluator], - max_items=2, - ) - - assert len(result.evaluator_stats) == 2 - assert result.total_scores_created >= result.total_items_processed * 2 - - -# ============================================================================ -# COMPOSITE EVALUATOR TESTS -# ============================================================================ - - -def test_composite_evaluator_weighted_average(langfuse_client): - """Test composite evaluator that computes weighted average.""" - - def accuracy_evaluator(*, input, output, **kwargs): - return Evaluation(name="accuracy", value=0.8) - - def relevance_evaluator(*, input, output, **kwargs): - return Evaluation(name="relevance", value=0.9) - - def composite_evaluator(*, input, output, expected_output, metadata, evaluations): - weights = {"accuracy": 0.6, "relevance": 0.4} - total = sum( - e.value * weights.get(e.name, 0) - for e in evaluations - if isinstance(e.value, (int, float)) - ) - - return Evaluation( - name="composite_score", - value=total, - comment=f"Weighted average of {len(evaluations)} metrics", - ) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[accuracy_evaluator, relevance_evaluator], - composite_evaluator=composite_evaluator, - max_items=2, - ) - - # Should have both regular and composite scores - assert result.total_scores_created > 0 - assert result.total_composite_scores_created > 0 - assert result.total_scores_created > result.total_composite_scores_created - - -def test_composite_evaluator_pass_fail(langfuse_client): - """Test composite evaluator that implements pass/fail logic.""" - - def metric1_evaluator(*, input, output, **kwargs): - return Evaluation(name="metric1", value=0.9) - - def metric2_evaluator(*, input, output, **kwargs): - return Evaluation(name="metric2", value=0.7) - - def pass_fail_composite(*, input, output, expected_output, metadata, evaluations): - thresholds = {"metric1": 0.8, "metric2": 0.6} - - passes = all( - e.value >= thresholds.get(e.name, 0) - for e in evaluations - if isinstance(e.value, (int, float)) - ) - - return Evaluation( - name="passes_all_checks", - value=1.0 if passes else 0.0, - comment="All checks passed" if passes else "Some checks failed", - ) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[metric1_evaluator, metric2_evaluator], - composite_evaluator=pass_fail_composite, - max_items=2, - ) - - assert result.total_composite_scores_created > 0 - - -@pytest.mark.asyncio -async def test_async_composite_evaluator(langfuse_client): - """Test async composite evaluator.""" - - def evaluator1(*, input, output, **kwargs): - return Evaluation(name="eval1", value=0.8) - - async def async_composite(*, input, output, expected_output, metadata, evaluations): - await asyncio.sleep(0.01) # Simulate async processing - avg = sum( - e.value for e in evaluations if isinstance(e.value, (int, float)) - ) / len(evaluations) - return Evaluation(name="async_composite", value=avg) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[evaluator1], - composite_evaluator=async_composite, - max_items=2, - ) - - assert result.total_composite_scores_created > 0 - - -def test_composite_evaluator_with_no_evaluations(langfuse_client): - """Test composite evaluator when no evaluations are present.""" - - def always_failing_evaluator(*, input, output, **kwargs): - raise Exception("Always fails") - - def composite_evaluator(*, input, output, expected_output, metadata, evaluations): - # Should not be called if no evaluations succeed - return Evaluation(name="composite", value=0.0) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[always_failing_evaluator], - composite_evaluator=composite_evaluator, - max_items=2, - ) - - # Composite evaluator should not create scores if no evaluations - assert result.total_composite_scores_created == 0 - - -def test_composite_evaluator_failure_handling(langfuse_client): - """Test that composite evaluator failures are handled gracefully.""" - - def evaluator1(*, input, output, **kwargs): - return Evaluation(name="eval1", value=0.8) - - def failing_composite(*, input, output, expected_output, metadata, evaluations): - raise ValueError("Composite evaluator failed") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[evaluator1], - composite_evaluator=failing_composite, - max_items=2, - ) - - # Regular scores should still be created - assert result.total_scores_created > 0 - # But no composite scores - assert result.total_composite_scores_created == 0 - - -# ============================================================================ -# ERROR HANDLING TESTS -# ============================================================================ - - -def test_mapper_failure_skips_item(langfuse_client): - """Test that mapper failure causes item to be skipped.""" - - call_count = {"count": 0} - - def sometimes_failing_mapper(*, item): - call_count["count"] += 1 - if call_count["count"] % 2 == 0: - raise Exception("Mapper failed") - return simple_trace_mapper(item=item) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=sometimes_failing_mapper, - evaluators=[simple_evaluator], - max_items=4, - ) - - # Some items should fail, some should succeed - assert result.total_items_failed > 0 - assert result.total_items_processed > 0 - - -def test_evaluator_failure_continues(langfuse_client): - """Test that one evaluator failing doesn't stop others.""" - - def working_evaluator1(*, input, output, **kwargs): - return Evaluation(name="working1", value=0.8) - - def failing_evaluator(*, input, output, **kwargs): - raise Exception("Evaluator failed") - - def working_evaluator2(*, input, output, **kwargs): - return Evaluation(name="working2", value=0.9) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[working_evaluator1, failing_evaluator, working_evaluator2], - max_items=2, - ) - - # Working evaluators should still create scores - assert result.total_scores_created >= result.total_items_processed * 2 - - # Failing evaluator should be tracked - failing_stats = next( - s for s in result.evaluator_stats if s.name == "failing_evaluator" - ) - assert failing_stats.failed_runs > 0 - - -def test_all_evaluators_fail(langfuse_client): - """Test when all evaluators fail but item is still processed.""" - - def failing_evaluator1(*, input, output, **kwargs): - raise Exception("Failed 1") - - def failing_evaluator2(*, input, output, **kwargs): - raise Exception("Failed 2") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[failing_evaluator1, failing_evaluator2], - max_items=2, - ) - - # Items should be processed even if all evaluators fail - assert result.total_items_processed > 0 - # But no scores created - assert result.total_scores_created == 0 - # All evaluations failed - assert result.total_evaluations_failed > 0 - - -# ============================================================================ -# EDGE CASES TESTS -# ============================================================================ - - -def test_empty_results_handling(langfuse_client): - """Test batch evaluation when filter returns no items.""" - nonexistent_name = f"nonexistent-trace-{create_uuid()}" - nonexistent_filter = f'[{{"type": "string", "column": "name", "operator": "=", "value": "{nonexistent_name}"}}]' - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - filter=nonexistent_filter, - ) - - assert result.total_items_fetched == 0 - assert result.total_items_processed == 0 - assert result.total_scores_created == 0 - assert result.completed is True - assert result.has_more_items is False - - -def test_max_items_zero(langfuse_client): - """Test with max_items=0 (should process no items).""" - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=0, - ) - - assert result.total_items_fetched == 0 - assert result.total_items_processed == 0 - - -def test_evaluation_value_type_conversions(langfuse_client): - """Test that different evaluation value types are handled correctly.""" - - def multi_type_evaluator(*, input, output, **kwargs): - return [ - Evaluation(name="int_score", value=5), # int - Evaluation(name="float_score", value=0.85), # float - Evaluation(name="bool_score", value=True), # bool - Evaluation(name="none_score", value=None), # None - ] - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[multi_type_evaluator], + get_client().run_batched_evaluation( + mapper=recording_mapper, + evaluators=[length_evaluator], + filter=corpus.filter, + fields="core,basic", max_items=1, ) - # All value types should be converted and scores created - assert result.total_scores_created >= 4 - - -# ============================================================================ -# PAGINATION TESTS -# ============================================================================ - - -def test_pagination_with_max_items(langfuse_client): - """Test that max_items limit is respected.""" - # Create more traces to ensure we have enough data - for i in range(10): - with langfuse_client.start_as_current_observation( - name=f"pagination-test-{create_uuid()}" - ) as span: - with propagate_attributes(tags=["pagination_test"]): - span.set_trace_io( - input=f"Input {i}", - output=f"Output {i}", - ) - - langfuse_client.flush() - time.sleep(3) - - filter_json = '[{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["pagination_test"]}]' - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - filter=filter_json, - max_items=5, - fetch_batch_size=2, - ) - - # Should not exceed max_items - assert result.total_items_processed <= 5 - - -def test_has_more_items_flag(langfuse_client): - """Test that has_more_items flag is set correctly when max_items is reached.""" - # Create enough traces to exceed max_items - batch_tag = f"batch-test-{create_uuid()}" - for i in range(15): - with langfuse_client.start_as_current_observation( - name=f"more-items-test-{i}" - ) as span: - with propagate_attributes(tags=[batch_tag]): - span.set_trace_io( - input=f"Input {i}", - output=f"Output {i}", - ) - - langfuse_client.flush() - time.sleep(3) - - filter_json = f'[{{"type": "arrayOptions", "column": "tags", "operator": "any of", "value": ["{batch_tag}"]}}]' - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - filter=filter_json, - max_items=5, - fetch_batch_size=2, - ) - - # has_more_items should be True if we hit the limit - if result.total_items_fetched >= 5: - assert result.has_more_items is True - - -def test_fetch_batch_size_parameter(langfuse_client): - """Test that different fetch_batch_size values work correctly.""" - for batch_size in [1, 5, 10]: - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=3, - fetch_batch_size=batch_size, - ) - - # Should complete regardless of batch size - assert result.completed is True or result.total_items_processed > 0 - - -# ============================================================================ -# RESUME FUNCTIONALITY TESTS -# ============================================================================ - - -def test_resume_token_structure(langfuse_client): - """Test that BatchEvaluationResumeToken has correct structure.""" - resume_token = BatchEvaluationResumeToken( - scope="traces", - filter='{"test": "filter"}', - last_processed_timestamp="2024-01-01T00:00:00Z", - last_processed_id="trace-123", - items_processed=10, - ) - - assert resume_token.scope == "traces" - assert resume_token.filter == '{"test": "filter"}' - assert resume_token.last_processed_timestamp == "2024-01-01T00:00:00Z" - assert resume_token.last_processed_id == "trace-123" - assert resume_token.items_processed == 10 - - -# ============================================================================ -# CONCURRENCY TESTS -# ============================================================================ - - -def test_max_concurrency_parameter(langfuse_client): - """Test that max_concurrency parameter works correctly.""" - for concurrency in [1, 5, 10]: - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=3, - max_concurrency=concurrency, - ) - - # Should complete regardless of concurrency - assert result.completed is True or result.total_items_processed > 0 + assert len(seen) == 1 + assert seen[0].input is None + assert seen[0].output is None -# ============================================================================ -# STATISTICS TESTS -# ============================================================================ +def test_composite_evaluator_and_failures(corpus): + def failing_evaluator(**kwargs): + raise RuntimeError("intentional") + def composite(*, evaluations, **kwargs): + return Evaluation(name="composite", value=float(len(evaluations))) -def test_evaluator_stats_structure(langfuse_client): - """Test that EvaluatorStats has correct structure.""" - - def test_evaluator(*, input, output, **kwargs): - return Evaluation(name="test", value=1.0) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[test_evaluator], - max_items=2, + result = get_client().run_batched_evaluation( + mapper=io_mapper, + evaluators=[length_evaluator, failing_evaluator], + composite_evaluator=composite, + filter=corpus.filter, ) - assert len(result.evaluator_stats) == 1 - stats = result.evaluator_stats[0] - - # Check all fields exist - assert hasattr(stats, "name") - assert hasattr(stats, "total_runs") - assert hasattr(stats, "successful_runs") - assert hasattr(stats, "failed_runs") - assert hasattr(stats, "total_scores_created") - - # Check values - assert stats.name == "test_evaluator" - assert stats.total_runs == result.total_items_processed - assert stats.successful_runs == result.total_items_processed - assert stats.failed_runs == 0 - + item_count = 2 * TRACE_COUNT + assert result.total_items_processed == item_count + assert result.total_scores_created == item_count + assert result.total_composite_scores_created == item_count + assert result.total_evaluations_failed == item_count + stats = {s.name: s for s in result.evaluator_stats} + assert stats["failing_evaluator"].failed_runs == item_count -def test_evaluator_stats_tracking(langfuse_client): - """Test that evaluator statistics are tracked correctly.""" - - call_count = {"count": 0} - - def sometimes_failing_evaluator(*, input, output, **kwargs): - call_count["count"] += 1 - if call_count["count"] % 2 == 0: - raise Exception("Failed") - return Evaluation(name="test", value=1.0) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[sometimes_failing_evaluator], - max_items=4, - ) - - stats = result.evaluator_stats[0] - assert stats.total_runs == result.total_items_processed - assert stats.successful_runs > 0 - assert stats.failed_runs > 0 - assert stats.successful_runs + stats.failed_runs == stats.total_runs - - -def test_error_summary_aggregation(langfuse_client): - """Test that error types are aggregated correctly in error_summary.""" - - def failing_mapper(*, item): - raise ValueError("Mapper error") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=failing_mapper, - evaluators=[simple_evaluator], - max_items=3, - ) - - # Error summary should contain the error type - assert len(result.error_summary) > 0 - assert any("Error" in key for key in result.error_summary.keys()) - - -def test_failed_item_ids_collected(langfuse_client): - """Test that failed item IDs are collected.""" +def test_mapper_failures_are_reported_per_item(corpus): def failing_mapper(*, item): - raise Exception("Failed") + raise ValueError("intentional") - result = langfuse_client.run_batched_evaluation( - scope="traces", + result = get_client().run_batched_evaluation( mapper=failing_mapper, - evaluators=[simple_evaluator], - max_items=3, - ) - - assert len(result.failed_item_ids) > 0 - # Each failed ID should be a string - assert all(isinstance(item_id, str) for item_id in result.failed_item_ids) - - -# ============================================================================ -# PERFORMANCE TESTS -# ============================================================================ - - -def test_duration_tracking(langfuse_client): - """Test that duration is tracked correctly.""" - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=2, - ) - - assert result.duration_seconds > 0 - assert result.duration_seconds < 60 # Should complete quickly for small batch - - -def test_verbose_logging(langfuse_client): - """Test that verbose=True doesn't cause errors.""" - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[simple_evaluator], - max_items=2, - verbose=True, # Should log progress + evaluators=[length_evaluator], + filter=corpus.filter, ) assert result.completed is True + assert result.total_items_failed == 2 * TRACE_COUNT + assert set(result.failed_item_ids) == set(corpus.root_ids + corpus.child_ids) + assert result.error_summary == {"ValueError": 2 * TRACE_COUNT} -# ============================================================================ -# ITEM EVALUATIONS TESTS -# ============================================================================ - - -def test_item_evaluations_basic(langfuse_client): - """Test that item_evaluations dict contains correct structure.""" - - def test_evaluator(*, input, output, **kwargs): - return Evaluation(name="test_metric", value=0.5) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[test_evaluator], - max_items=3, - ) - - # Check that item_evaluations is a dict - assert isinstance(result.item_evaluations, dict) - - # Should have evaluations for each processed item - assert len(result.item_evaluations) == result.total_items_processed - - # Each entry should be a list of Evaluation objects - for item_id, evaluations in result.item_evaluations.items(): - assert isinstance(item_id, str) - assert isinstance(evaluations, list) - assert all(isinstance(e, Evaluation) for e in evaluations) - # Should have one evaluation per evaluator - assert len(evaluations) == 1 - assert evaluations[0].name == "test_metric" - - -def test_item_evaluations_multiple_evaluators(langfuse_client): - """Test item_evaluations with multiple evaluators.""" - - def accuracy_evaluator(*, input, output, **kwargs): - return Evaluation(name="accuracy", value=0.8) - - def relevance_evaluator(*, input, output, **kwargs): - return Evaluation(name="relevance", value=0.9) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[accuracy_evaluator, relevance_evaluator], - max_items=2, - ) - - # Check structure - assert len(result.item_evaluations) == result.total_items_processed - - # Each item should have evaluations from both evaluators - for item_id, evaluations in result.item_evaluations.items(): - assert len(evaluations) == 2 - eval_names = {e.name for e in evaluations} - assert eval_names == {"accuracy", "relevance"} - - -def test_item_evaluations_with_composite(langfuse_client): - """Test that item_evaluations includes composite evaluations.""" - - def base_evaluator(*, input, output, **kwargs): - return Evaluation(name="base_score", value=0.7) - - def composite_evaluator(*, input, output, expected_output, metadata, evaluations): - return Evaluation( - name="composite_score", - value=sum( - e.value for e in evaluations if isinstance(e.value, (int, float)) - ), - ) - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=simple_trace_mapper, - evaluators=[base_evaluator], - composite_evaluator=composite_evaluator, - max_items=2, - ) - - # Each item should have both base and composite evaluations - for item_id, evaluations in result.item_evaluations.items(): - assert len(evaluations) == 2 - eval_names = {e.name for e in evaluations} - assert eval_names == {"base_score", "composite_score"} - - # Verify composite scores were created - assert result.total_composite_scores_created > 0 - - -def test_item_evaluations_empty_on_failure(langfuse_client): - """Test that failed items don't appear in item_evaluations.""" - - def failing_mapper(*, item): - raise Exception("Mapper failed") - - result = langfuse_client.run_batched_evaluation( - scope="traces", - mapper=failing_mapper, - evaluators=[simple_evaluator], - max_items=3, +def test_filter_without_matches_completes_empty(): + result = get_client().run_batched_evaluation( + mapper=io_mapper, + evaluators=[length_evaluator], + filter=_tag_filter(f"nonexistent-{create_uuid()}"), ) - # All items failed, so item_evaluations should be empty - assert len(result.item_evaluations) == 0 - assert result.total_items_failed > 0 + assert result.completed is True + assert result.total_items_fetched == 0 + assert result.has_more_items is False + assert result.resume_token is None diff --git a/tests/e2e/test_core_sdk.py b/tests/e2e/test_core_sdk.py index 614d6da41..7105f80e7 100644 --- a/tests/e2e/test_core_sdk.py +++ b/tests/e2e/test_core_sdk.py @@ -1,3 +1,4 @@ +import json import os import time from asyncio import gather @@ -5,17 +6,20 @@ from time import sleep import pytest -from tenacity import Retrying, stop_after_delay, wait_fixed from langfuse import Langfuse, propagate_attributes from langfuse._client.resource_manager import LangfuseResourceManager from langfuse._utils import _get_timestamp -from tests.support.api_wrapper import LangfuseAPI from tests.support.utils import ( create_uuid, get_api, - wait_for_result, - wait_for_trace, + get_observations, + get_root_observation, + get_scores, + user_metadata, + wait_for_observations, + wait_for_root_observation, + wait_for_scores, ) @@ -33,36 +37,33 @@ async def update_generation(i, langfuse: Langfuse): ) # Update generation with metadata - generation.update(metadata={"count": str(i)}) + generation.update(metadata={"count": i}) # End the generation generation.end() + return generation.trace_id + # Create Langfuse client langfuse = Langfuse() # Run concurrent operations - await gather(*(update_generation(i, langfuse) for i in range(100))) + trace_ids = await gather(*(update_generation(i, langfuse) for i in range(100))) langfuse.flush() - # Allow time for all operations to be processed - sleep(10) - # Verify that all spans were created properly - api = get_api() - for i in range(100): - # Find the observations with the expected name - observations = api.legacy.observations_v1.get_many(name=str(i)).data + for i, trace_id in enumerate(trace_ids): + observations = wait_for_observations(trace_id, min_count=2) - # Find generation observations (there should be at least one) generation_obs = [obs for obs in observations if obs.type == "GENERATION"] - assert len(generation_obs) > 0 + assert len(generation_obs) == 1 # Verify metadata observation = generation_obs[0] assert observation.name == str(i) - assert observation.metadata["count"] == i + assert user_metadata(observation)["count"] == i + assert get_root_observation(observations).trace_name == str(i) def test_flush(): @@ -80,14 +81,10 @@ def test_flush(): # Flush all pending spans to the Langfuse API langfuse.flush() - # Allow time for API to process - sleep(2) - # Verify traces were sent by checking they exist in the API - api = get_api() for i, trace_id in enumerate(trace_ids): - trace = api.trace.get(trace_id) - assert trace.name == str(i) + root = wait_for_root_observation(trace_id) + assert root.trace_name == str(i) def test_invalid_score_data_does_not_raise_exception(): @@ -152,21 +149,20 @@ def test_create_session_score(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - score = get_api().scores.get_by_id(score_id) + scores = wait_for_scores(session_id=session_id, id=score_id) - # find the score by name (server may transform the id format) - assert score is not None + assert len(scores) == 1 + score = scores[0] assert score.value == 1 assert score.data_type == "NUMERIC" - assert score.session_id == session_id + assert score.subject.kind == "session" + assert score.subject.id == session_id def test_create_numeric_score(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span and set trace properties with langfuse.start_as_current_observation(name="test-span") as span: @@ -202,22 +198,21 @@ def test_create_numeric_score(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + wait_for_observations(trace_id, min_count=2) + scores = wait_for_scores(trace_id=trace_id, name="this-is-a-score") - # Find the score by name (server may transform the ID format) - score = next((s for s in trace["scores"] if s["name"] == "this-is-a-score"), None) - assert score is not None - assert score["value"] == 1 - assert score["dataType"] == "NUMERIC" - assert score["stringValue"] is None + assert len(scores) == 1 + score = scores[0] + assert score.id == score_id + assert score.value == 1 + assert score.data_type == "NUMERIC" + assert score.subject.kind == "trace" def test_create_boolean_score(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span and set trace properties with langfuse.start_as_current_observation(name="test-span") as span: @@ -231,7 +226,7 @@ def test_create_boolean_score(): # Ensure data is sent langfuse.flush() - api_wrapper.get_trace(trace_id) + wait_for_root_observation(trace_id) # Create a boolean score score_id = create_uuid() @@ -256,27 +251,17 @@ def test_create_boolean_score(): langfuse.flush() # Retrieve and verify - trace = api_wrapper.get_trace( - trace_id, - is_result_ready=lambda trace: any( - score["name"] == "this-is-a-score" for score in trace.get("scores", []) - ), - ) + scores = wait_for_scores(trace_id=trace_id, name="this-is-a-score") - # Find the score we created by name - created_score = next( - (s for s in trace["scores"] if s["name"] == "this-is-a-score"), None - ) - assert created_score is not None, "Score not found in trace" - assert created_score["id"] == score_id - assert created_score["dataType"] == "BOOLEAN" - assert created_score["value"] == 1 - assert created_score["stringValue"] == "True" + assert len(scores) == 1, "Score not found in trace" + created_score = scores[0] + assert created_score.id == score_id + assert created_score.data_type == "BOOLEAN" + assert created_score.value is True def test_create_categorical_score(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span and set trace properties with langfuse.start_as_current_observation(name="test-span") as span: @@ -290,7 +275,7 @@ def test_create_categorical_score(): # Ensure data is sent langfuse.flush() - api_wrapper.get_trace(trace_id) + wait_for_root_observation(trace_id) # Create a categorical score score_id = create_uuid() @@ -314,27 +299,17 @@ def test_create_categorical_score(): langfuse.flush() # Retrieve and verify - trace = api_wrapper.get_trace( - trace_id, - is_result_ready=lambda trace: any( - score["name"] == "this-is-a-score" for score in trace.get("scores", []) - ), - ) + scores = wait_for_scores(trace_id=trace_id, name="this-is-a-score") - # Find the score we created by name - created_score = next( - (s for s in trace["scores"] if s["name"] == "this-is-a-score"), None - ) - assert created_score is not None, "Score not found in trace" - assert created_score["id"] == score_id - assert created_score["dataType"] == "CATEGORICAL" - assert created_score["value"] == 0 - assert created_score["stringValue"] == "high score" + assert len(scores) == 1, "Score not found in trace" + created_score = scores[0] + assert created_score.id == score_id + assert created_score.data_type == "CATEGORICAL" + assert created_score.value == "high score" def test_create_text_score(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span and set trace properties with langfuse.start_as_current_observation(name="test-span") as span: @@ -372,30 +347,28 @@ def test_create_text_score(): # Ensure data is sent langfuse.flush() - # Retrieve and verify with retry - for attempt in Retrying( - stop=stop_after_delay(10), wait=wait_fixed(0.1), reraise=True - ): - with attempt: - trace = api_wrapper.get_trace(trace_id) - - # Find the score we created by name - created_score = next( - (s for s in trace["scores"] if s["name"] == "this-is-a-score"), None - ) - assert created_score is not None, "Score not found in trace" - assert created_score["id"] == score_id - assert created_score["dataType"] == "TEXT" + # Retrieve and verify + scores = wait_for_scores(trace_id=trace_id, name="this-is-a-score") - assert ( - created_score["stringValue"] - == "This is a detailed text evaluation of the output quality." - ) + assert len(scores) == 1, "Score not found in trace" + created_score = scores[0] + assert created_score.id == score_id + assert created_score.data_type == "TEXT" + assert ( + created_score.value + == "This is a detailed text evaluation of the output quality." + ) +@pytest.mark.xfail( + strict=False, + reason=( + "Needs the Langfuse platform release that adds timestamp to POST /scores " + "(langfuse prep-js-v6-py-v5-sdk-majors). Remove this marker once it ships." + ), +) def test_create_score_with_custom_timestamp(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span and set trace properties with langfuse.start_as_current_observation(name="test-span") as span: @@ -409,7 +382,7 @@ def test_create_score_with_custom_timestamp(): # Ensure data is sent langfuse.flush() - api_wrapper.get_trace(trace_id) + wait_for_root_observation(trace_id) custom_timestamp = datetime.now(timezone.utc) - timedelta(hours=1) score_id = create_uuid() @@ -426,28 +399,16 @@ def test_create_score_with_custom_timestamp(): langfuse.flush() # Retrieve and verify - trace = api_wrapper.get_trace( - trace_id, - is_result_ready=lambda trace: any( - score["name"] == "custom-timestamp-score" - for score in trace.get("scores", []) - ), - ) + scores = wait_for_scores(trace_id=trace_id, name="custom-timestamp-score") - # Find the score we created by name - created_score = next( - (s for s in trace["scores"] if s["name"] == "custom-timestamp-score"), None - ) - assert created_score is not None, "Score not found in trace" - assert created_score["id"] == score_id - assert created_score["dataType"] == "NUMERIC" - assert created_score["value"] == 0.85 + assert len(scores) == 1, "Score not found in trace" + created_score = scores[0] + assert created_score.id == score_id + assert created_score.data_type == "NUMERIC" + assert created_score.value == 0.85 # Verify timestamp is close to our custom timestamp - # Parse the timestamp from the API response - response_timestamp = datetime.fromisoformat( - created_score["timestamp"].replace("Z", "+00:00") - ) + response_timestamp = created_score.timestamp # Check that the timestamps are within 1 second of each other # (allowing for some processing time and rounding) @@ -477,24 +438,14 @@ def test_create_trace(): langfuse.flush() # Retrieve the trace from the API - trace = LangfuseAPI().get_trace( - trace_id, - is_result_ready=lambda trace: ( - trace.get("name") == trace_name - and trace.get("userId") == "test" - and trace.get("metadata", {}).get("key") == "value" - and trace.get("tags") == ["tag1", "tag2"] - and trace.get("public") is True - ), - ) + root = wait_for_root_observation(trace_id) # Verify all trace properties - assert trace["name"] == trace_name - assert trace["userId"] == "test" - assert trace["metadata"]["key"] == "value" - assert trace["tags"] == ["tag1", "tag2"] - assert trace["public"] is True - assert True if not trace["externalId"] else False + assert root.trace_name == trace_name + assert root.user_id == "test" + assert user_metadata(root) == {"key": "value"} + assert root.tags == ["tag1", "tag2"] + assert root.public is True def test_create_update_trace(): @@ -525,23 +476,12 @@ def test_create_update_trace(): assert isinstance(trace_id, str) # Retrieve and verify trace - trace = wait_for_trace( - trace_id, - is_result_ready=lambda trace: ( - trace.name == trace_name - and trace.user_id == "test" - and trace.metadata is not None - and trace.metadata.get("key") == "value" - and trace.metadata.get("key2") == "value2" - and trace.public is True - ), - ) + root = wait_for_root_observation(trace_id) - assert trace.name == trace_name - assert trace.user_id == "test" - assert trace.metadata["key"] == "value" - assert trace.metadata["key2"] == "value2" - assert trace.public is True + assert root.trace_name == trace_name + assert root.user_id == "test" + assert user_metadata(root) == {"key": "value", "key2": "value2"} + assert root.public is True def test_create_update_current_trace(): @@ -549,14 +489,14 @@ def test_create_update_current_trace(): trace_name = create_uuid() - # Create initial span with trace properties using propagate_attributes and set_current_trace_io + # Create initial span with trace properties using propagate_attributes with langfuse.start_as_current_observation(name="test-span-current") as span: with propagate_attributes( trace_name=trace_name, user_id="test", metadata={"key": "value"}, ): - langfuse.set_current_trace_io(input="test_input") + langfuse.update_current_span(input="test_input") langfuse.set_current_trace_as_public() # Get trace ID for later reference trace_id = span.trace_id @@ -570,20 +510,18 @@ def test_create_update_current_trace(): # Ensure data is sent to the API langfuse.flush() - sleep(2) assert isinstance(trace_id, str) # Retrieve and verify trace - trace = get_api().trace.get(trace_id) + root = wait_for_root_observation(trace_id) # The 2nd update to the trace must not erase previously set attributes - assert trace.name == trace_name - assert trace.user_id == "test" - assert trace.metadata["key"] == "value" - assert trace.metadata["key2"] == "value2" - assert trace.public is True - assert trace.version == "1.0" - assert trace.input == "test_input" + assert root.trace_name == trace_name + assert root.user_id == "test" + assert user_metadata(root) == {"key": "value", "key2": "value2"} + assert root.public is True + assert root.version == "1.0" + assert root.input == "test_input" def test_create_generation(): @@ -620,19 +558,19 @@ def test_create_generation(): # Flush to ensure all data is sent langfuse.flush() - sleep(2) # Retrieve the trace from the API - trace = get_api().trace.get(trace_id) - - # Verify trace details - assert trace.name == "query-generation" - assert trace.user_id is None + observations = wait_for_observations(trace_id) - assert len(trace.observations) == 1 + assert len(observations) == 1 # Verify generation details - generation_api = trace.observations[0] + generation_api = observations[0] + + # Verify trace details + assert generation_api.is_root_observation is True + assert generation_api.trace_name == "query-generation" + assert generation_api.user_id is None assert generation_api.name == "query-generation" assert generation_api.start_time is not None @@ -707,15 +645,14 @@ def test_create_generation_complex( langfuse.flush() trace_id = generation.trace_id - trace = get_api().trace.get(trace_id) - - assert trace.name == "query-generation" - assert trace.user_id is None + observations = wait_for_observations(trace_id) - assert len(trace.observations) == 1 + assert len(observations) == 1 - generation_api = trace.observations[0] + generation_api = observations[0] + assert generation_api.trace_name == "query-generation" + assert generation_api.user_id is None assert generation_api.id == generation.id assert generation_api.name == "query-generation" assert generation_api.input == [ @@ -727,13 +664,7 @@ def test_create_generation_complex( ] assert generation_api.output == [{"foo": "bar"}] - # Check if metadata exists and has tags before asserting - if ( - hasattr(generation_api, "metadata") - and generation_api.metadata is not None - and "tags" in generation_api.metadata - ): - assert generation_api.metadata["tags"] == ["yo"] + assert user_metadata(generation_api) == {"tags": ["yo"]} assert generation_api.start_time is not None assert generation_api.usage_details == {"input": 51, "output": 0, "total": 100} @@ -759,19 +690,18 @@ def test_create_span(): # Ensure all data is sent langfuse.flush() - sleep(2) # Retrieve from API - trace = get_api().trace.get(trace_id) - - # Verify trace details - assert trace.name == "span" - assert trace.user_id is None + observations = wait_for_observations(trace_id) - assert len(trace.observations) == 1 + assert len(observations) == 1 # Verify span details - span_api = trace.observations[0] + span_api = observations[0] + + # Verify trace details + assert span_api.trace_name == "span" + assert span_api.user_id is None assert span_api.id == span_id assert span_api.name == "span" @@ -784,7 +714,6 @@ def test_create_span(): def test_score_trace(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() trace_name = create_uuid() @@ -803,20 +732,17 @@ def test_score_trace(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + assert wait_for_root_observation(trace_id).trace_name == trace_name - assert trace["name"] == trace_name - - # Find the score we created by name (server may create additional auto-scores) - score = next((s for s in trace["scores"] if s["name"] == "valuation"), None) - assert score is not None - assert score["value"] == 0.5 - assert score["comment"] == "This is a comment" - assert score["observationId"] is None - assert score["dataType"] == "NUMERIC" + scores = wait_for_scores(trace_id=trace_id, name="valuation") + assert len(scores) == 1 + score = scores[0] + assert score.value == 0.5 + assert score.comment == "This is a comment" + assert score.subject.kind == "trace" + assert score.data_type == "NUMERIC" def test_score_trace_nested_trace(): @@ -839,19 +765,16 @@ def test_score_trace_nested_trace(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) - - assert trace.name == trace_name + assert wait_for_root_observation(trace_id).trace_name == trace_name - # Find the score we created by name (server may create additional auto-scores) - score = next((s for s in trace.scores if s.name == "valuation"), None) - assert score is not None + scores = wait_for_scores(trace_id=trace_id, name="valuation") + assert len(scores) == 1 + score = scores[0] assert score.value == 0.5 assert score.comment == "This is a comment" - assert score.observation_id is None # API returns this field name + assert score.subject.kind == "trace" assert score.data_type == "NUMERIC" @@ -882,25 +805,23 @@ def test_score_trace_nested_observation(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + assert wait_for_root_observation(trace_id).trace_name == trace_name - assert trace.name == trace_name - - # Find the score we created by name (server may create additional auto-scores) - score = next((s for s in trace.scores if s.name == "valuation"), None) - assert score is not None + scores = wait_for_scores(trace_id=trace_id, name="valuation") + assert len(scores) == 1 + score = scores[0] assert score.value == 0.5 assert score.comment == "This is a comment" - assert score.observation_id == child_span_id # API returns this field name + assert score.subject.kind == "observation" + assert score.subject.id == child_span_id + assert score.subject.trace_id == trace_id assert score.data_type == "NUMERIC" def test_score_span(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span span = langfuse.start_observation( @@ -928,20 +849,19 @@ def test_score_span(): # Ensure data is sent langfuse.flush() - sleep(3) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) - - assert len(trace["observations"]) == 1 + assert len(wait_for_observations(trace_id)) == 1 - # Find the score we created by name (server may create additional auto-scores) - score = next((s for s in trace["scores"] if s["name"] == "valuation"), None) - assert score is not None - assert score["value"] == 1 - assert score["comment"] == "This is a comment" - assert score["observationId"] == span_id - assert score["dataType"] == "NUMERIC" + scores = wait_for_scores(trace_id=trace_id, observation_id=span_id) + assert len(scores) == 1 + score = scores[0] + assert score.name == "valuation" + assert score.value == 1 + assert score.comment == "This is a comment" + assert score.subject.kind == "observation" + assert score.subject.id == span_id + assert score.data_type == "NUMERIC" def test_create_trace_and_span(): @@ -963,16 +883,15 @@ def test_create_trace_and_span(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id, min_count=2) - assert trace.name == trace_name - assert len(trace.observations) == 2 # Parent span and child span + assert get_root_observation(observations).trace_name == trace_name + assert len(observations) == 2 # Parent span and child span # Find the child span - child_spans = [obs for obs in trace.observations if obs.name == "span"] + child_spans = [obs for obs in observations if obs.name == "span"] assert len(child_spans) == 1 span = child_spans[0] @@ -989,7 +908,7 @@ def test_create_trace_and_generation(): # Create parent span and set trace properties with langfuse.start_as_current_observation(name=trace_name) as parent_span: with propagate_attributes(trace_name=trace_name, session_id="test-session-id"): - parent_span.set_trace_io(input={"key": "value"}) + parent_span.update(input={"key": "value"}) # Create a generation as child generation = parent_span.start_observation( @@ -1004,35 +923,30 @@ def test_create_trace_and_generation(): # Ensure data is sent langfuse.flush() - sleep(2) - # Retrieve traces in two ways - dbTrace = get_api().trace.get(trace_id) - getTrace = get_api().trace.get( - trace_id - ) # Using API as direct getTrace not available + # Retrieve and verify + observations = wait_for_observations(trace_id, min_count=2) + root = get_root_observation(observations) # Verify trace details - assert dbTrace.name == trace_name - assert len(dbTrace.observations) == 2 # Parent span and generation - assert getTrace.name == trace_name - assert len(getTrace.observations) == 2 - assert getTrace.session_id == "test-session-id" + assert root.trace_name == trace_name + assert len(observations) == 2 # Parent span and generation + assert root.session_id == "test-session-id" # Find the generation - generations = [obs for obs in getTrace.observations if obs.name == "generation"] + generations = [obs for obs in observations if obs.name == "generation"] assert len(generations) == 1 generation = generations[0] assert generation.name == "generation" assert generation.trace_id == trace_id + assert generation.session_id == "test-session-id" assert generation.start_time is not None - assert getTrace.input == {"key": "value"} + assert root.input == {"key": "value"} def test_create_generation_and_trace(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() trace_name = create_uuid() @@ -1063,23 +977,24 @@ def test_create_generation_and_trace(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) - - assert trace["name"] == trace_name + observations = wait_for_observations(trace_id, min_count=2) # We should have 2 observations (the generation and the span for updating trace) - assert len(trace["observations"]) == 2 + assert len(observations) == 2 + + trace_update_spans = [obs for obs in observations if obs.name == "trace-update"] + assert len(trace_update_spans) == 1 + assert trace_update_spans[0].trace_name == trace_name # Find the generation - generations = [obs for obs in trace["observations"] if obs["name"] == "generation"] + generations = [obs for obs in observations if obs.name == "generation"] assert len(generations) == 1 generation_obs = generations[0] - assert generation_obs["name"] == "generation" - assert generation_obs["traceId"] == trace["id"] + assert generation_obs.name == "generation" + assert generation_obs.trace_id == trace_id def test_create_span_and_get_observation(): @@ -1096,12 +1011,12 @@ def test_create_span_and_get_observation(): # Flush and wait langfuse.flush() - sleep(2) - # Use API to fetch the observation by ID - observation = get_api().legacy.observations_v1.get(span_id) + observations = wait_for_observations(span.trace_id) # Verify observation properties + assert len(observations) == 1 + observation = observations[0] assert observation.name == "span" assert observation.id == span_id @@ -1123,20 +1038,19 @@ def test_update_generation(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id) # Verify trace properties - assert trace.name == "generation" - assert len(trace.observations) == 1 + assert len(observations) == 1 # Verify generation updates - retrieved_generation = trace.observations[0] + retrieved_generation = observations[0] + assert retrieved_generation.trace_name == "generation" assert retrieved_generation.name == "generation" assert retrieved_generation.trace_id == trace_id - assert retrieved_generation.metadata["dict"] == "value" + assert user_metadata(retrieved_generation) == {"dict": "value"} # Note: With OTEL, we can't verify exact start times from manually set timestamps, # as they are managed internally by the OTEL SDK @@ -1159,20 +1073,19 @@ def test_update_span(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id) # Verify trace properties - assert trace.name == "span" - assert len(trace.observations) == 1 + assert len(observations) == 1 # Verify span updates - retrieved_span = trace.observations[0] + retrieved_span = observations[0] + assert retrieved_span.trace_name == "span" assert retrieved_span.name == "span" assert retrieved_span.trace_id == trace_id - assert retrieved_span.metadata["dict"] == "value" + assert user_metadata(retrieved_span) == {"dict": "value"} def test_create_span_and_generation(): @@ -1197,17 +1110,16 @@ def test_create_span_and_generation(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id, min_count=2) # Verify trace details - assert len(trace.observations) == 2 + assert len(observations) == 2 # Find span and generation - spans = [obs for obs in trace.observations if obs.name == "span"] - generations = [obs for obs in trace.observations if obs.name == "generation"] + spans = [obs for obs in observations if obs.name == "span"] + generations = [obs for obs in observations if obs.name == "generation"] assert len(spans) == 1 assert len(generations) == 1 @@ -1222,7 +1134,6 @@ def test_create_span_and_generation(): def test_create_trace_with_id_and_generation(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() trace_name = create_uuid() @@ -1244,28 +1155,27 @@ def test_create_trace_with_id_and_generation(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + observations = wait_for_observations(trace_id, min_count=2) # Verify trace properties - assert trace["name"] == trace_name - assert trace["id"] == trace_id - assert len(trace["observations"]) == 2 # Parent span and generation + root = get_root_observation(observations) + assert root.trace_name == trace_name + assert root.trace_id == trace_id + assert len(observations) == 2 # Parent span and generation # Find the generation - generations = [obs for obs in trace["observations"] if obs["name"] == "generation"] + generations = [obs for obs in observations if obs.name == "generation"] assert len(generations) == 1 gen = generations[0] - assert gen["name"] == "generation" - assert gen["traceId"] == trace["id"] + assert gen.name == "generation" + assert gen.trace_id == trace_id def test_end_generation(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a generation generation = langfuse.start_observation( @@ -1294,21 +1204,14 @@ def test_end_generation(): langfuse.flush() # Retrieve and verify - trace = api_wrapper.get_trace( - trace_id, - is_result_ready=lambda trace: any( - obs["name"] == "query-generation" for obs in trace.get("observations", []) - ), - ) + observations = wait_for_observations(trace_id) # Find generation by name - generations = [ - obs for obs in trace["observations"] if obs["name"] == "query-generation" - ] + generations = [obs for obs in observations if obs.name == "query-generation"] assert len(generations) == 1 gen = generations[0] - assert gen["endTime"] is not None + assert gen.end_time is not None def test_end_generation_with_data(): @@ -1353,15 +1256,12 @@ def test_end_generation_with_data(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - fetched_trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id, min_count=2) # Find generation by name - generations = [ - obs for obs in fetched_trace.observations if obs.name == "query-generation" - ] + generations = [obs for obs in observations if obs.name == "query-generation"] assert len(generations) == 1 generation = generations[0] @@ -1371,7 +1271,7 @@ def test_end_generation_with_data(): 2023, 1, 1, 12, 3, tzinfo=timezone.utc ) assert generation.name == "query-generation" - assert generation.metadata["dict"] == "value" + assert user_metadata(generation) == {"dict": "value"} assert generation.level == "ERROR" assert generation.status_message == "Generation ended" assert generation.version == "1.0" @@ -1379,12 +1279,9 @@ def test_end_generation_with_data(): assert generation.model_parameters == {"param1": "value1", "param2": "value2"} assert generation.input == [{"test_input_key": "test_input_value"}] assert generation.output == {"test_output_key": "test_output_value"} - assert generation.usage.input == 100 - assert generation.usage.output == 200 - assert generation.usage.total == 500 - assert generation.calculated_input_cost == 111 - assert generation.calculated_output_cost == 222 - assert generation.calculated_total_cost == 444 + assert generation.usage_details == {"input": 100, "output": 200, "total": 500} + assert generation.cost_details == {"input": 111, "output": 222, "total": 444} + assert generation.total_cost == 444 def test_end_generation_with_openai_token_format(): @@ -1418,31 +1315,26 @@ def test_end_generation_with_openai_token_format(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id) # Find generation - generations = [obs for obs in trace.observations if obs.name == "query-generation"] + generations = [obs for obs in observations if obs.name == "query-generation"] assert len(generations) == 1 generation_api = generations[0] # Verify properties were converted correctly assert generation_api.end_time is not None - assert generation_api.usage.input == 100 # prompt_tokens mapped to input - assert generation_api.usage.output == 200 # completion_tokens mapped to output - assert generation_api.usage.total == 500 - assert generation_api.usage.unit == "TOKENS" # Default unit for OpenAI format - assert generation_api.calculated_input_cost == 111 - assert generation_api.calculated_output_cost == 222 - assert generation_api.calculated_total_cost == 444 + # OpenAI-style keys are mapped to input/output/total + assert generation_api.usage_details == {"input": 100, "output": 200, "total": 500} + assert generation_api.cost_details == {"input": 111, "output": 222, "total": 444} + assert generation_api.total_cost == 444 def test_end_span(): langfuse = Langfuse() - api_wrapper = LangfuseAPI() # Create a span span = langfuse.start_observation( @@ -1460,19 +1352,18 @@ def test_end_span(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = api_wrapper.get_trace(trace_id) + observations = wait_for_observations(trace_id) # Find span - spans = [obs for obs in trace["observations"] if obs["name"] == "span"] + spans = [obs for obs in observations if obs.name == "span"] assert len(spans) == 1 span_api = spans[0] # Verify end time was set - assert span_api["endTime"] is not None + assert span_api.end_time is not None def test_end_span_with_data(): @@ -1495,21 +1386,19 @@ def test_end_span_with_data(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id) # Find span - spans = [obs for obs in trace.observations if obs.name == "span"] + spans = [obs for obs in observations if obs.name == "span"] assert len(spans) == 1 span_api = spans[0] # Verify end time and metadata were updated assert span_api.end_time is not None - assert span_api.metadata["dict"] == "value" - assert span_api.metadata["interface"] == "whatsapp" + assert user_metadata(span_api) == {"dict": "value", "interface": "whatsapp"} def test_get_generations(): @@ -1535,16 +1424,15 @@ def test_get_generations(): # Ensure data is sent langfuse.flush() - sleep(3) # Fetch generations using API - generations = get_api().legacy.observations_v1.get_many(name=generation_name) + generations = wait_for_observations(name=generation_name) # Verify fetched generation matches what we created - assert len(generations.data) == 1 - assert generations.data[0].name == generation_name - assert generations.data[0].input == "great-prompt" - assert generations.data[0].output == "great-completion" + assert len(generations) == 1 + assert generations[0].name == generation_name + assert generations[0].input == "great-prompt" + assert generations[0].output == "great-completion" def test_get_generations_by_user(): @@ -1574,18 +1462,15 @@ def test_get_generations_by_user(): # Ensure data is sent langfuse.flush() - sleep(3) # Fetch generations by user ID using the API - generations = get_api().legacy.observations_v1.get_many( - user_id=user_id, type="GENERATION" - ) + generations = wait_for_observations(user_id=user_id, type="GENERATION") # Verify fetched generation matches what we created - assert len(generations.data) == 1 - assert generations.data[0].name == generation_name - assert generations.data[0].input == "great-prompt" - assert generations.data[0].output == "great-completion" + assert len(generations) == 1 + assert generations[0].name == generation_name + assert generations[0].input == "great-prompt" + assert generations[0].output == "great-completion" def test_kwargs(): @@ -1614,16 +1499,17 @@ def test_kwargs(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - observation = get_api().legacy.observations_v1.get(span_id) + observations = wait_for_observations(span.trace_id) + assert [observation.id for observation in observations] == [span_id] + observation = observations[0] # Verify kwargs were properly set as attributes assert observation.start_time is not None assert observation.input == {"key": "value"} assert observation.output == {"key": "value"} - assert observation.metadata["interface"] == "whatsapp" + assert user_metadata(observation) == {"interface": "whatsapp"} @pytest.mark.skip("Flaky") @@ -1660,16 +1546,13 @@ def test_timezone_awareness(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id, min_count=4) # Verify timestamps are in UTC regardless of local timezone - assert ( - len(trace.observations) == 4 - ) # Parent span, child span, generation, and event - for observation in trace.observations: + assert len(observations) == 4 # Parent span, child span, generation, and event + for observation in observations: # Check that start_time is within 5 seconds of current time delta = observation.start_time - utc_now assert delta.seconds < 5 @@ -1720,16 +1603,13 @@ def test_timezone_awareness_setting_timestamps(): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - trace = get_api().trace.get(trace_id) + observations = wait_for_observations(trace_id, min_count=4) # Verify timestamps are in UTC regardless of local timezone - assert ( - len(trace.observations) == 4 - ) # Parent span, child span, generation, and event - for observation in trace.observations: + assert len(observations) == 4 # Parent span, child span, generation, and event + for observation in observations: # Check that start_time is within 5 seconds of current time delta = abs((utc_now - observation.start_time).total_seconds()) assert delta < 5 @@ -1762,17 +1642,17 @@ def test_get_trace_by_session_id(): # Ensure data is sent langfuse.flush() - sleep(2) - # Retrieve the trace using the session_id - traces = get_api().trace.list(session_id=session_id) + # Retrieve the trace's observations using the session_id + observations = wait_for_observations(session_id=session_id) # Verify that the trace was retrieved correctly - assert len(traces.data) == 1 - retrieved_trace = traces.data[0] - assert retrieved_trace.name == trace_name - assert retrieved_trace.session_id == session_id - assert retrieved_trace.id == trace_id + assert len(observations) == 1 + retrieved_root = observations[0] + assert retrieved_root.is_root_observation is True + assert retrieved_root.trace_name == trace_name + assert retrieved_root.session_id == session_id + assert retrieved_root.trace_id == trace_id def test_fetch_trace(): @@ -1789,15 +1669,12 @@ def test_fetch_trace(): # Ensure data is sent langfuse.flush() - sleep(2) - # Fetch the trace using the get_api client - # Note: In the OTEL-based client, we use the API client directly - trace = get_api().trace.get(trace_id) + root = wait_for_root_observation(trace_id) # Verify trace properties - assert trace.id == trace_id - assert trace.name == name + assert root.trace_id == trace_id + assert root.trace_name == name def test_fetch_traces(): @@ -1812,7 +1689,7 @@ def test_fetch_traces(): # First trace with langfuse.start_as_current_observation(name="test1") as span: with propagate_attributes(trace_name=name, session_id="session-1"): - span.set_trace_io(input={"key": "value"}, output="output-value") + span.update(input={"key": "value"}, output="output-value") trace_ids.append(span.trace_id) sleep(1) # Ensure traces have different timestamps @@ -1820,7 +1697,7 @@ def test_fetch_traces(): # Second trace with langfuse.start_as_current_observation(name="test2") as span: with propagate_attributes(trace_name=name, session_id="session-1"): - span.set_trace_io(input={"key": "value"}, output="output-value") + span.update(input={"key": "value"}, output="output-value") trace_ids.append(span.trace_id) sleep(1) # Ensure traces have different timestamps @@ -1828,7 +1705,7 @@ def test_fetch_traces(): # Third trace with langfuse.start_as_current_observation(name="test3") as span: with propagate_attributes(trace_name=name, session_id="session-1"): - span.set_trace_io(input={"key": "value"}, output="output-value") + span.update(input={"key": "value"}, output="output-value") trace_ids.append(span.trace_id) # Ensure data is sent @@ -1836,38 +1713,50 @@ def test_fetch_traces(): expected_trace_ids = set(trace_ids) api = get_api(retry=False) + trace_name_filter = json.dumps( + [{"type": "string", "column": "traceName", "operator": "=", "value": name}] + ) - # Fetch all traces with the same name. - all_traces = wait_for_result( - lambda: api.trace.list(name=name, limit=10), - is_result_ready=lambda response: ( - {trace.id for trace in response.data} == expected_trace_ids + # Fetch the root observations of all traces with the same name. + roots = wait_for_observations( + filter=trace_name_filter, + is_result_ready=lambda observations: ( + {o.trace_id for o in observations} == expected_trace_ids ), ) # Verify we got all traces - assert len(all_traces.data) == 3 + assert len(roots) == 3 # Verify trace properties - for trace in all_traces.data: - assert trace.name == name - assert trace.session_id == "session-1" - assert trace.input == {"key": "value"} - assert trace.output == "output-value" - - # Test pagination by fetching the first three pages one at a time and - # confirming they collectively cover the created traces. - paginated_ids = set() - for page in range(1, 4): - paginated_response = wait_for_result( - lambda page=page: api.trace.list(name=name, limit=1, page=page), - is_result_ready=lambda response: ( - len(response.data) == 1 and response.data[0].id in expected_trace_ids - ), + for root in roots: + assert root.is_root_observation is True + assert root.trace_name == name + assert root.session_id == "session-1" + assert root.input == {"key": "value"} + assert root.output == "output-value" + + # Test cursor pagination by walking pages of one item and confirming they + # collectively cover the created traces. + paginated_ids = [] + cursor = None + for _ in range(3): + page = api.observations.get_many( + filter=trace_name_filter, limit=1, cursor=cursor + ) + assert len(page.data) == 1 + paginated_ids.append(page.data[0].trace_id) + cursor = page.meta.cursor + + assert set(paginated_ids) == expected_trace_ids + assert len(paginated_ids) == 3 + if cursor is not None: + assert ( + api.observations.get_many( + filter=trace_name_filter, limit=1, cursor=cursor + ).data + == [] ) - paginated_ids.add(paginated_response.data[0].id) - - assert paginated_ids == expected_trace_ids def test_get_observation(): @@ -1890,10 +1779,12 @@ def test_get_observation(): # Ensure data is sent langfuse.flush() - sleep(2) # Fetch the observation using the API - observation = get_api().legacy.observations_v1.get(generation_id) + observations = wait_for_observations(parent_span.trace_id, min_count=2) + matching = [o for o in observations if o.id == generation_id] + assert len(matching) == 1 + observation = matching[0] # Verify observation properties assert observation.id == generation_id @@ -1926,18 +1817,18 @@ def test_get_observations(): # Fetch observations using the API expected_generation_ids = {gen1_id, gen2_id} - observations = wait_for_result( - lambda: api.legacy.observations_v1.get_many(name=name, limit=10), - is_result_ready=lambda response: expected_generation_ids.issubset( - {obs.id for obs in response.data} + observations = wait_for_observations( + name=name, + is_result_ready=lambda observations: expected_generation_ids.issubset( + {obs.id for obs in observations} ), ) # Verify fetched observations - assert len(observations.data) == 2 + assert len(observations) == 2 # Filter for just the generations - generations = [obs for obs in observations.data if obs.type == "GENERATION"] + generations = [obs for obs in observations if obs.type == "GENERATION"] assert len(generations) == 2 # Verify the generation IDs match what we created @@ -1945,91 +1836,32 @@ def test_get_observations(): assert gen1_id in gen_ids assert gen2_id in gen_ids - # Test pagination by confirming both created generations can be reached - # across separate pages. - paginated_ids = set() - for page in range(1, 3): - paginated_response = wait_for_result( - lambda page=page: api.legacy.observations_v1.get_many( - name=name, limit=1, page=page - ), - is_result_ready=lambda response: ( - len(response.data) == 1 - and response.data[0].id in expected_generation_ids - ), - ) - paginated_ids.add(paginated_response.data[0].id) - - assert paginated_ids == expected_generation_ids - - -def test_get_trace_not_found(): - # Attempt to fetch a non-existent trace using the API - with pytest.raises(Exception): - get_api(retry=False).trace.get(create_uuid()) - + # Test cursor pagination by confirming both created generations can be + # reached across separate pages. + first_page = api.observations.get_many(name=name, limit=1) + assert len(first_page.data) == 1 + assert first_page.meta.cursor is not None + second_page = api.observations.get_many( + name=name, limit=1, cursor=first_page.meta.cursor + ) + assert len(second_page.data) == 1 -def test_get_observation_not_found(): - # Attempt to fetch a non-existent observation using the API - with pytest.raises(Exception): - get_api(retry=False).legacy.observations_v1.get(create_uuid()) + assert {first_page.data[0].id, second_page.data[0].id} == expected_generation_ids -def test_get_traces_empty(): - # Fetch traces with a filter that should return no results - response = get_api(retry=False).trace.list(name=create_uuid()) +def test_get_observations_for_unknown_trace_is_empty(): + response = get_api(retry=False).observations.get_many(trace_id=create_uuid()) - assert len(response.data) == 0 - assert response.meta.total_items == 0 + assert response.data == [] + assert response.meta.cursor is None def test_get_observations_empty(): # Fetch observations with a filter that should return no results - response = get_api(retry=False).legacy.observations_v1.get_many(name=create_uuid()) + response = get_api(retry=False).observations.get_many(name=create_uuid()) - assert len(response.data) == 0 - assert response.meta.total_items == 0 - - -def test_get_sessions(): - langfuse = Langfuse() - - # unique name - name = create_uuid() - session1 = create_uuid() - session2 = create_uuid() - session3 = create_uuid() - - # Create multiple traces with different session IDs - # Create first trace - with langfuse.start_as_current_observation(name=name): - with propagate_attributes(trace_name=name, session_id=session1): - pass - - # Create second trace - with langfuse.start_as_current_observation(name=name): - with propagate_attributes(trace_name=name, session_id=session2): - pass - - # Create third trace - with langfuse.start_as_current_observation(name=name): - with propagate_attributes(trace_name=name, session_id=session3): - pass - - langfuse.flush() - - # Fetch sessions - sleep(3) - response = get_api().sessions.list() - - # Assert the structure of the response, cannot check for the exact number of sessions as the table is not cleared between tests - assert hasattr(response, "data") - assert hasattr(response, "meta") - assert isinstance(response.data, list) - - # fetch only one, cannot check for the exact number of sessions as the table is not cleared between tests - response = get_api().sessions.list(limit=1, page=2) - assert len(response.data) == 1 + assert response.data == [] + assert response.meta.cursor is None @pytest.mark.skip( @@ -2037,7 +1869,6 @@ def test_get_sessions(): ) def test_create_trace_sampling_zero(): langfuse = Langfuse(sample_rate=0) - api_wrapper = LangfuseAPI() trace_name = create_uuid() # Create a span with trace properties - with sample_rate=0, this will not be sent to the API @@ -2061,12 +1892,9 @@ def test_create_trace_sampling_zero(): langfuse.flush() sleep(2) - # Try to fetch the trace - should fail as it wasn't sent to the API - fetched_trace = api_wrapper.get_trace(trace_id) - assert fetched_trace == { - "error": "LangfuseNotFoundError", - "message": f"Trace {trace_id} not found within authorized project", - } + # The trace's observations must not exist as they were never sent to the API + assert get_observations(trace_id=trace_id) == [] + assert get_scores(trace_id=trace_id) == [] def test_mask_function(request): @@ -2083,16 +1911,15 @@ def mask_func(data): return data langfuse = Langfuse(mask=mask_func) - api_wrapper = LangfuseAPI() # Create a root span with trace properties with langfuse.start_as_current_observation(name="test-span") as root_span: with propagate_attributes(trace_name="test_trace"): - root_span.set_trace_io(input={"sensitive": "data"}) + root_span.update(input={"sensitive": "data"}) # Get trace ID for later use trace_id = root_span.trace_id # Add output to the trace - root_span.set_trace_io(output={"more": "sensitive"}) + root_span.update(output={"more": "sensitive"}) # Create a generation as child gen = root_span.start_observation( @@ -2112,44 +1939,39 @@ def mask_func(data): # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - fetched_trace = api_wrapper.get_trace(trace_id) - assert fetched_trace["input"] == {"sensitive": "MASKED"} - assert fetched_trace["output"] == {"more": "MASKED"} + observations = wait_for_observations(trace_id, min_count=3) + fetched_root = get_root_observation(observations) + assert fetched_root.input == {"sensitive": "MASKED"} + assert fetched_root.output == {"more": "MASKED"} - fetched_gen = [ - o for o in fetched_trace["observations"] if o["type"] == "GENERATION" - ][0] - assert fetched_gen["input"] == {"prompt": "MASKED"} - assert fetched_gen["output"] == "MASKED" + fetched_gen = [o for o in observations if o.type == "GENERATION"][0] + assert fetched_gen.input == {"prompt": "MASKED"} + assert fetched_gen.output == "MASKED" fetched_span = [ - o - for o in fetched_trace["observations"] - if o["type"] == "SPAN" and o["name"] == "test_span" + o for o in observations if o.type == "SPAN" and o.name == "test_span" ][0] - assert fetched_span["input"] == {"data": "MASKED"} - assert fetched_span["output"] == "MASKED" + assert fetched_span.input == {"data": "MASKED"} + assert fetched_span.output == "MASKED" # Create a root span with trace properties with langfuse.start_as_current_observation(name="test-span") as root_span: with propagate_attributes(trace_name="test_trace"): - root_span.set_trace_io(input={"should_raise": "data"}) + root_span.update(input={"should_raise": "data"}) # Get trace ID for later use trace_id = root_span.trace_id # Add output to the trace - root_span.set_trace_io(output={"should_raise": "sensitive"}) + root_span.update(output={"should_raise": "sensitive"}) # Ensure data is sent langfuse.flush() - sleep(2) # Retrieve and verify - fetched_trace = api_wrapper.get_trace(trace_id) - assert fetched_trace["input"] == "" - assert fetched_trace["output"] == "" + fetched_root = wait_for_root_observation(trace_id) + assert fetched_root.input == "" + assert fetched_root.output == "" def test_get_project_id(): @@ -2218,13 +2040,11 @@ def test_start_as_current_observation_types(): pass langfuse.flush() - sleep(2) - api = get_api() - trace = api.trace.get(trace_id) + observations = wait_for_observations(trace_id, min_count=len(observation_types) + 1) # Check we have all expected observation types - found_types = {obs.type for obs in trace.observations} + found_types = {obs.type for obs in observations} expected_types = {obs_type.upper() for obs_type in observation_types} | { "SPAN" } # includes parent span @@ -2234,12 +2054,12 @@ def test_start_as_current_observation_types(): # Verify each specific observation exists for obs_type in observation_types: - observations = [ + matching = [ obs - for obs in trace.observations + for obs in observations if obs.name == f"test-{obs_type}" and obs.type == obs_type.upper() ] - assert len(observations) == 1, f"Expected one {obs_type.upper()} observation" + assert len(matching) == 1, f"Expected one {obs_type.upper()} observation" def test_that_generation_like_properties_are_actually_created(): @@ -2296,21 +2116,22 @@ def test_that_generation_like_properties_are_actually_created(): langfuse.flush() - api = get_api() - trace = api.trace.get(trace_id) + observations = wait_for_observations( + trace_id, min_count=len(generation_like_types) + 1 + ) # Verify that the properties are persisted in the API for generation-like types for obs_type in generation_like_types: - observations = [ + matching = [ obs - for obs in trace.observations + for obs in observations if obs.name == f"test-{obs_type}" and obs.type == obs_type.upper() ] - assert len(observations) == 1, ( - f"Expected one {obs_type.upper()} observation, but found {len(observations)}" + assert len(matching) == 1, ( + f"Expected one {obs_type.upper()} observation, but found {len(matching)}" ) - obs = observations[0] + obs = matching[0] assert obs.model == test_model, f"{obs_type} should have model property" assert obs.model_parameters == test_model_parameters, ( diff --git a/tests/e2e/test_decorators.py b/tests/e2e/test_decorators.py index 9e302f799..a33a4f3d5 100644 --- a/tests/e2e/test_decorators.py +++ b/tests/e2e/test_decorators.py @@ -1,6 +1,5 @@ import asyncio import os -import sys from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from time import sleep @@ -16,7 +15,11 @@ from langfuse._client.resource_manager import LangfuseResourceManager from langfuse.langchain import CallbackHandler from langfuse.media import LangfuseMedia -from tests.support.utils import get_api, wait_for_trace +from tests.support.utils import ( + get_observations, + user_metadata, + wait_for_trace_snapshot, +) mock_metadata = {"key": "metadata"} mock_deep_metadata = {"key": "mock_deep_metadata"} @@ -101,7 +104,7 @@ def level_1_function(*args, **kwargs): assert result == "level_1" # Wrapped function returns correctly # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=3) assert len(trace_data.observations) == 3 # trace parameters if set anywhere in the call stack @@ -134,7 +137,7 @@ def level_1_function(*args, **kwargs): assert level_3_observation.name == "level_3" assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.total_cost > 0 assert level_3_observation.output == "mock_output" assert level_3_observation.version == "version-1" @@ -183,7 +186,7 @@ def level_1_function(*args, **kwargs): assert result == "level_1" # Wrapped function returns correctly # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=3) assert len(trace_data.observations) == 3 # trace parameters if set anywhere in the call stack @@ -216,7 +219,7 @@ def level_1_function(*args, **kwargs): assert level_3_observation.name == "level_3" assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.total_cost > 0 assert level_3_observation.output == "mock_output" assert level_3_observation.version == "version-1" @@ -261,7 +264,7 @@ def level_1_function(*args, **kwargs): langfuse.flush() - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=3) # trace parameters if set anywhere in the call stack assert trace_data.session_id == mock_session_id @@ -350,7 +353,7 @@ def level_1_function(*args, **kwargs): langfuse.flush() for mock_id in [mock_trace_id_1, mock_trace_id_2]: - trace_data = get_api().trace.get(mock_id) + trace_data = wait_for_trace_snapshot(mock_id, min_observations=3) assert len(trace_data.observations) == 3 # ID setting for span or trace @@ -383,7 +386,7 @@ def level_1_function(*args, **kwargs): assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.total_cost > 0 def test_decorators_langchain(): @@ -428,7 +431,7 @@ def level_1_function(*args, **kwargs): langfuse.flush() - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( trace.session_id == mock_session_id @@ -523,8 +526,10 @@ def level_1_function(*args, **kwargs): assert result == "level_3" # Wrapped function returns correctly # ID setting for span or trace - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, + min_observations=3, + min_scores=3, is_result_ready=lambda trace: { "test-observation-score", "test-trace-score", @@ -554,7 +559,7 @@ def level_1_function(*args, **kwargs): assert any( [ score.name == "another-test-trace-score" - and score.string_value == "my_value" + and score.value == "my_value" and score.data_type == "CATEGORICAL" for score in trace_scores ] @@ -600,7 +605,7 @@ def function_with_circular_arg(circular_obj, *args, **kwargs): # Validate that the function executed as expected assert result == "function response" - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=1) assert ( trace_data.observations[0].input["args"][0]["reference"] == "CircularRefObject" @@ -633,7 +638,7 @@ def main(*args, **kwargs): assert result == "function response" - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) # Check that disabled capture_io doesn't capture manually set input/output assert len(trace_data.observations) == 2 @@ -703,7 +708,7 @@ def level_1_function(self, *args, **kwargs): assert result == "level_1" # Wrapped function returns correctly # ID setting for span or trace - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=4) assert len(trace_data.observations) == 4 # trace parameters if set anywhere in the call stack @@ -744,7 +749,7 @@ def level_1_function(self, *args, **kwargs): assert level_3_observation.name == "level_3_function" assert level_3_observation.metadata["key"] == mock_deep_metadata["key"] assert level_3_observation.type == "GENERATION" - assert level_3_observation.calculated_total_cost > 0 + assert level_3_observation.total_cost > 0 assert level_3_observation.output == "mock_output" @@ -780,7 +785,7 @@ def main(**kwargs): assert result == mock_output - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) # Find the main and nested observations adjacencies = defaultdict(list) @@ -834,7 +839,7 @@ async def main_async(**kwargs): assert result == mock_output - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) # Check correct nesting adjacencies = defaultdict(list) @@ -903,7 +908,7 @@ async def level_1_function(*args, **kwargs): assert result == "level_1" # Wrapped function returns correctly # ID setting for span or trace - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( trace.session_id == mock_session_id @@ -947,11 +952,11 @@ async def level_1_function(*args, **kwargs): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.usage.input is not None - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["input"] is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None print(generation) - assert generation.output == 2 + assert generation.output == "2" def test_generator_as_function_input(): @@ -983,7 +988,7 @@ def main(**kwargs): assert result == mock_output - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) nested_obs = next(o for o in trace_data.observations if o.name == "nested") @@ -1020,7 +1025,7 @@ def main(**kwargs): main(langfuse_trace_id=mock_trace_id) langfuse.flush() - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) # Find the observation with name 'nested' nested_observation = next(o for o in trace_data.observations if o.name == "nested") @@ -1053,7 +1058,7 @@ def function(): assert result == mock_output - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: any( observation.name == "function" and observation.output == mock_output @@ -1072,10 +1077,10 @@ def test_media(): media = LangfuseMedia(content_bytes=pdf_bytes, content_type="application/pdf") - @observe() + @observe(capture_input=False, capture_output=False) def main(): sleep(1) - langfuse.set_current_trace_io( + langfuse.update_current_span( input={ "context": { "nested": media, @@ -1100,7 +1105,7 @@ def main(): langfuse.flush() - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( "@@@langfuseMedia:type=application/pdf|id=" @@ -1156,19 +1161,20 @@ def main(): langfuse.flush() - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, - is_result_ready=lambda trace: ( - trace.metadata is not None - and trace.metadata.get("key1") == "value1" - and trace.metadata.get("key2") == "value2" - and trace.tags == ["tag1", "tag2"] - ), + min_observations=2, + is_result_ready=lambda trace: trace.tags == ["tag1", "tag2"], ) - assert trace_data.metadata["key1"] == "value1" - assert trace_data.metadata["key2"] == "value2" + # Trace metadata comes from the root; the nested propagation only reaches + # the nested observation. + assert trace_data.metadata == {"key1": "value1"} + nested_observation = _get_observation_by_name(trace_data, "nested") + assert user_metadata(nested_observation) == {"key1": "value1", "key2": "value2"} + assert nested_observation.tags == ["tag1", "tag2"] + assert trace_data.root.tags == ["tag1"] assert trace_data.tags == ["tag1", "tag2"] @@ -1228,7 +1234,7 @@ def level_1_function(*args, **kwargs): assert result == "level_1" # Verify trace was created properly - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( trace.name == mock_name and len(trace.observations) == 3 @@ -1288,7 +1294,7 @@ def level_1_function(*args, **kwargs): assert result == "level_4" - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( trace.name == mock_name @@ -1363,7 +1369,7 @@ def level_1_function(*args, **kwargs): assert result == "level_1" - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( trace.name == mock_name and len(trace.observations) == 2 @@ -1418,13 +1424,7 @@ def level_1_function(*args, **kwargs): # Should skip tracing entirely in multi-project setup without public key # This is expected behavior to prevent cross-project data leakage - try: - trace_data = get_api().trace.get(mock_trace_id) - # If trace is found, it should have no observations (tracing was skipped) - assert len(trace_data.observations) == 0 - except Exception: - # Trace not found is also expected - tracing was completely disabled - pass + assert get_observations(trace_id=mock_trace_id) == [] # Reset instances to not leak to other test suites removeMockResourceManagerInstances() @@ -1487,7 +1487,7 @@ async def async_level_1_function(*args, **kwargs): assert result == "async_level_3" # Verify trace was created properly - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( trace.name == mock_name @@ -1569,7 +1569,7 @@ async def async_level_1_function(*args, **kwargs): assert result == "sync_level_4" - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=4) assert len(trace_data.observations) == 4 assert trace_data.name == mock_name @@ -1648,8 +1648,8 @@ async def async_level_1_function(task_id, *args, **kwargs): assert result2 == "async_level_3_task_2" # Verify both traces were created correctly and didn't interfere - trace_data_1 = get_api().trace.get(trace_id_1) - trace_data_2 = get_api().trace.get(trace_id_2) + trace_data_1 = wait_for_trace_snapshot(trace_id_1, min_observations=3) + trace_data_2 = wait_for_trace_snapshot(trace_id_2, min_observations=3) assert trace_data_1.name == f"{mock_name}_task_1" assert trace_data_2.name == f"{mock_name}_task_2" @@ -1722,7 +1722,7 @@ async def async_consumer_function(): assert result == "Hello, Async World!" - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) assert len(trace_data.observations) == 2 assert trace_data.name == mock_name @@ -1787,7 +1787,7 @@ async def async_root_function(*args, **kwargs): assert result == "exception_handled" - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=3) assert len(trace_data.observations) == 3 assert trace_data.name == mock_name @@ -1852,11 +1852,11 @@ def root_function(): ) # Verify trace structure - trace_data = wait_for_trace( + trace_data = wait_for_trace_snapshot( mock_trace_id, is_result_ready=lambda trace: ( len(trace.observations) >= 2 - and {"parent_root", "child_stream"}.issubset( + and {"root", "sync_generator"}.issubset( { observation.name for observation in trace.observations @@ -1880,7 +1880,6 @@ def root_function(): @pytest.mark.asyncio -@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") async def test_async_generator_context_preservation(): """Test that async generators preserve context when consumed later (e.g., by streaming responses)""" langfuse = get_client() @@ -1930,7 +1929,7 @@ async def root_function(): ) # Verify trace structure - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) assert len(trace_data.observations) == 2 # Verify both observations are present @@ -1946,7 +1945,6 @@ async def root_function(): @pytest.mark.asyncio -@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") async def test_async_generator_context_preservation_with_trace_hierarchy(): """Test that async generators maintain proper parent-child span relationships""" langfuse = get_client() @@ -1999,7 +1997,7 @@ async def parent_function(): ) # Verify trace structure - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) assert len(trace_data.observations) == 2 # Check both observations exist @@ -2009,7 +2007,6 @@ async def parent_function(): @pytest.mark.asyncio -@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") async def test_async_generator_exception_handling_with_context(): """Test that exceptions in async generators are properly handled while preserving context""" langfuse = get_client() @@ -2047,7 +2044,7 @@ async def root_function(): assert items == ["first_item"] # Verify trace structure - should have both observations despite exception - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) assert len(trace_data.observations) == 2 # Check that the failing generator observation has ERROR level @@ -2087,7 +2084,7 @@ def root_function(): assert items == [] # Verify trace structure - trace_data = get_api().trace.get(mock_trace_id) + trace_data = wait_for_trace_snapshot(mock_trace_id, min_observations=2) assert len(trace_data.observations) == 2 # Verify empty generator observation diff --git a/tests/e2e/test_experiments.py b/tests/e2e/test_experiments.py index 80881f144..ed4c23182 100644 --- a/tests/e2e/test_experiments.py +++ b/tests/e2e/test_experiments.py @@ -1,20 +1,122 @@ """Comprehensive tests for Langfuse experiment functionality matching JS SDK.""" +import os import time -from typing import Any, Dict, List +from datetime import datetime, timedelta, timezone +from typing import Any, Callable, Dict, List, TypeVar +from uuid import uuid4 import pytest from opentelemetry import trace as otel_trace_api from langfuse import get_client from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse.api import LangfuseAPI from langfuse.experiment import ( Evaluation, ExperimentData, ExperimentItem, ExperimentItemResult, ) -from tests.support.utils import create_uuid, get_api, wait_for_trace +from tests.support.utils import raw_metadata_value + +T = TypeVar("T") + +READ_TIMEOUT_SECONDS = float(os.environ.get("LANGFUSE_E2E_READ_TIMEOUT_SECONDS", "30")) +READ_INTERVAL_SECONDS = float( + os.environ.get("LANGFUSE_E2E_READ_INTERVAL_SECONDS", "0.5") +) +ITEM_FIELDS = "core,dataset,io,metadata,experimentMetadata,scores" + + +def create_uuid() -> str: + return str(uuid4()) + + +def get_api() -> LangfuseAPI: + return LangfuseAPI( + username=os.environ.get("LANGFUSE_PUBLIC_KEY"), + password=os.environ.get("LANGFUSE_SECRET_KEY"), + base_url=os.environ.get("LANGFUSE_BASE_URL"), + ) + + +def _read_window_start() -> datetime: + return datetime.now(timezone.utc) - timedelta(hours=1) + + +def poll(operation: Callable[[], T], is_ready: Callable[[T], bool]) -> T: + """Re-read until ready or timeout; returns the last result for the caller to assert.""" + deadline = time.monotonic() + READ_TIMEOUT_SECONDS + while True: + result = operation() + if is_ready(result) or time.monotonic() >= deadline: + return result + time.sleep(READ_INTERVAL_SECONDS) + + +def get_experiment_items( + experiment_id: str, *, expected_count: int, with_scores: bool = False +) -> list: + api = get_api() + return poll( + lambda: ( + api.experiments.list_items( + from_start_time=_read_window_start(), + experiment_id=experiment_id, + fields=ITEM_FIELDS, + limit=100, + ).data + ), + lambda items: ( + len(items) >= expected_count + and (not with_scores or all(item.scores for item in items)) + ), + ) + + +def get_experiment( + experiment_id: str, *, is_ready: Callable[[Any], bool] = lambda _: True +): + api = get_api() + experiments = poll( + lambda: ( + api.experiments.list( + from_start_time=_read_window_start(), + id=experiment_id, + fields="core,metadata,scores", + ).data + ), + lambda data: len(data) == 1 and is_ready(data[0]), + ) + assert len(experiments) == 1, f"Experiment {experiment_id} should exist" + return experiments[0] + + +def get_observations(trace_id: str, *, is_ready: Callable[[list], bool]) -> list: + api = get_api() + return poll( + lambda: ( + api.observations.get_many( + trace_id=trace_id, + fields="core,basic,io,metadata", + from_start_time=_read_window_start(), + ).data + ), + is_ready, + ) + + +def get_trace_scores(trace_id: str, *, expected_count: int) -> list: + api = get_api() + return poll( + lambda: api.scores.get_many(trace_id=trace_id, fields="subject").data, + lambda scores: len(scores) >= expected_count, + ) + + +def has_score(name: str) -> Callable[[Any], bool]: + return lambda experiment: any(s.name == name for s in experiment.scores or []) @pytest.fixture @@ -78,63 +180,53 @@ def test_run_experiment_on_local_dataset(sample_dataset): assert len(result.item_results) == 3 assert len(result.run_evaluations) == 1 assert result.run_evaluations[0].name == "average_length" - assert result.dataset_run_id is None # No dataset_run_id for local datasets + assert len(result.experiment_id) == 16 + assert result.experiment_url is not None + assert result.experiment_url.endswith( + f"/experiments/results?baseline={result.experiment_id}" + ) + with pytest.warns(DeprecationWarning): + assert result.dataset_run_id == result.experiment_id # Validate item results structure for item_result in result.item_results: assert hasattr(item_result, "output") assert hasattr(item_result, "evaluations") assert hasattr(item_result, "trace_id") - assert ( - item_result.dataset_run_id is None - ) # No dataset_run_id for local datasets + assert item_result.experiment_id == result.experiment_id assert len(item_result.evaluations) == 2 # Both evaluators should run - # Flush and wait for server processing langfuse_client.flush() - time.sleep(2) - - # Validate traces are correctly persisted with input/output/metadata - api = get_api() - expected_inputs = ["Germany", "France", "Spain"] - expected_outputs = ["Capital of Germany", "Capital of France", "Capital of Spain"] - - for i, item_result in enumerate(result.item_results): - trace_id = item_result.trace_id - assert trace_id is not None, f"Item {i} should have a trace_id" - - # Fetch trace from API - trace = api.trace.get(trace_id) - assert trace is not None, f"Trace {trace_id} should exist" - - # Validate trace name - assert trace.name == "experiment-item-run", ( - f"Trace {trace_id} should have correct name" - ) - - # Validate trace input - should contain the experiment item - assert trace.input is not None, f"Trace {trace_id} should have input" - expected_input = expected_inputs[i] - # The input should contain the item data in some form - assert expected_input in str(trace.input), ( - f"Trace {trace_id} input should contain '{expected_input}'" - ) - # Validate trace output - should be the task result - assert trace.output is not None, f"Trace {trace_id} should have output" - expected_output = expected_outputs[i] - assert trace.output == expected_output, ( - f"Trace {trace_id} output should be '{expected_output}', got '{trace.output}'" - ) + expected = { + "Germany": ("Capital of Germany", "Berlin"), + "France": ("Capital of France", "Paris"), + "Spain": ("Capital of Spain", "Madrid"), + } + items = get_experiment_items(result.experiment_id, expected_count=3) + assert len(items) == 3 + assert {item.trace_id for item in items} == { + r.trace_id for r in result.item_results + } - # Validate trace metadata contains experiment name - assert trace.metadata is not None, f"Trace {trace_id} should have metadata" - assert "experiment_name" in trace.metadata, ( - f"Trace {trace_id} metadata should contain experiment_name" - ) - assert trace.metadata["experiment_name"] == "Euro capitals", ( - f"Trace {trace_id} metadata should have correct experiment_name" - ) + for item in items: + assert item.experiment_name == result.run_name + assert item.experiment_dataset_id is None + assert item.input in expected + expected_output, expected_answer = expected[item.input] + assert item.output == expected_output + assert item.expected_output == expected_answer + assert item.metadata is not None + assert item.metadata["experiment_name"] == raw_metadata_value("Euro capitals") + + # Run-level evaluations are persisted for local data, too + experiment = get_experiment( + result.experiment_id, is_ready=has_score("average_length") + ) + assert experiment.name == result.run_name + assert experiment.description == "Country capital experiment" + assert experiment.item_count == 3 + assert [s.name for s in experiment.scores or []] == ["average_length"] def test_run_experiment_flattens_large_metadata_for_server_ingestion(): @@ -168,21 +260,25 @@ def task_with_external_child(*, item: ExperimentItem, **kwargs: Dict[str, Any]): trace_id = result.item_results[0].trace_id assert trace_id is not None - trace = wait_for_trace( + observations = get_observations( trace_id, - is_result_ready=lambda fetched_trace: any( - observation.name == external_span_name - for observation in fetched_trace.observations + is_ready=lambda observations: any( + observation.name == external_span_name for observation in observations ), ) - assert trace.metadata is not None + root_observation = next( + observation + for observation in observations + if observation.name == "experiment-item-run" + ) + assert root_observation.metadata is not None for metadata_key, metadata_value in experiment_metadata.items(): - assert trace.metadata[metadata_key] == metadata_value + assert root_observation.metadata[metadata_key] == metadata_value external_observation = next( observation - for observation in trace.observations + for observation in observations if observation.name == external_span_name ) external_metadata = external_observation.metadata or {} @@ -227,117 +323,111 @@ def test_run_experiment_on_langfuse_dataset(): run_evaluators=[run_evaluator_average_length], ) - # Should have dataset run ID for Langfuse datasets - assert result.dataset_run_id is not None + project_id = langfuse_client._get_project_id() + assert project_id is not None + assert result.experiment_id == langfuse_client._create_experiment_id( + project_id=project_id, dataset_id=dataset.id, run_name=result.run_name + ) + assert result.experiment_url == ( + f"{os.environ['LANGFUSE_BASE_URL']}/project/{project_id}" + f"/experiments/results?baseline={result.experiment_id}" + ) assert len(result.item_results) == 2 - assert all(item.dataset_run_id is not None for item in result.item_results) + assert all( + item.experiment_id == result.experiment_id for item in result.item_results + ) - # Flush and wait for server processing langfuse_client.flush() - time.sleep(3) - - # Verify dataset run exists via API - api = get_api() - dataset_run = api.datasets.get_run( - dataset_name=dataset_name, run_name=result.run_name - ) - # Validate traces are correctly persisted with input/output/metadata expected_data = {"Germany": "Capital of Germany", "France": "Capital of France"} - dataset_run_id = result.dataset_run_id - - # Create a mapping from dataset item ID to dataset item for validation dataset_item_map = {item.id: item for item in dataset.items} - for i, item_result in enumerate(result.item_results): - trace_id = item_result.trace_id - assert trace_id is not None, f"Item {i} should have a trace_id" - - # Fetch trace from API - trace = api.trace.get(trace_id) - assert trace is not None, f"Trace {trace_id} should exist" - - # Validate trace name - assert trace.name == "experiment-item-run", ( - f"Trace {trace_id} should have correct name" + items = get_experiment_items( + result.experiment_id, expected_count=2, with_scores=True + ) + assert len(items) == 2, "Experiment should have 2 items" + assert {item.trace_id for item in items} == { + r.trace_id for r in result.item_results + } + assert {item.experiment_item_id for item in items} == set(dataset_item_map) + + for item in items: + assert item.experiment_name == result.run_name + assert item.experiment_dataset_id == dataset.id + assert item.experiment_description == "Test on Langfuse dataset" + assert item.experiment_item_version is None + + dataset_item = dataset_item_map[item.experiment_item_id] + assert item.input == dataset_item.input + assert item.output == expected_data[dataset_item.input] + assert item.expected_output == dataset_item.expected_output + + assert item.metadata is not None + assert item.metadata["experiment_name"] == raw_metadata_value(experiment_name) + assert item.metadata["dataset_id"] == raw_metadata_value(dataset.id) + assert item.metadata["dataset_item_id"] == raw_metadata_value( + item.experiment_item_id ) - # Validate trace input and output match expected pairs - assert trace.input is not None, f"Trace {trace_id} should have input" - trace_input_str = str(trace.input) + assert [s.name for s in item.scores or []] == ["factuality"] - # Find which expected input this trace corresponds to - matching_input = None - for expected_input in expected_data.keys(): - if expected_input in trace_input_str: - matching_input = expected_input - break + experiment = get_experiment( + result.experiment_id, is_ready=has_score("average_length") + ) + assert experiment.name == result.run_name + assert experiment.description == "Test on Langfuse dataset" + assert experiment.dataset_id == dataset.id + assert experiment.item_count == 2 + assert [s.name for s in experiment.scores or []] == ["average_length"] + assert experiment.scores[0].subject.kind == "experiment" + assert experiment.scores[0].subject.id == result.experiment_id - assert matching_input is not None, ( - f"Trace {trace_id} input '{trace_input_str}' should contain one of {list(expected_data.keys())}" - ) - # Validate trace output matches the expected output for this input - assert trace.output is not None, f"Trace {trace_id} should have output" - expected_output = expected_data[matching_input] - assert trace.output == expected_output, ( - f"Trace {trace_id} output should be '{expected_output}', got '{trace.output}'" - ) +def test_run_experiment_on_versioned_dataset_records_item_version(): + """Pinned dataset versions are recorded as the experiment item version.""" + langfuse_client = get_client() + dataset_name = "versioned-dataset-" + create_uuid() + langfuse_client.create_dataset(name=dataset_name) + langfuse_client.create_dataset_item( + dataset_name=dataset_name, input="Germany", expected_output="Berlin" + ) - # Validate trace metadata contains experiment and dataset info - assert trace.metadata is not None, f"Trace {trace_id} should have metadata" - assert "experiment_name" in trace.metadata, ( - f"Trace {trace_id} metadata should contain experiment_name" - ) - assert trace.metadata["experiment_name"] == experiment_name, ( - f"Trace {trace_id} metadata should have correct experiment_name" - ) + version = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(seconds=1) + time.sleep(1.5) + dataset = langfuse_client.get_dataset(dataset_name, version=version) + assert len(dataset.items) == 1 - # Validate dataset-specific metadata fields - assert "dataset_id" in trace.metadata, ( - f"Trace {trace_id} metadata should contain dataset_id" - ) - assert trace.metadata["dataset_id"] == dataset.id, ( - f"Trace {trace_id} metadata should have correct dataset_id" - ) + result = dataset.run_experiment( + name="Versioned " + create_uuid()[:8], task=mock_task + ) + langfuse_client.flush() - assert "dataset_item_id" in trace.metadata, ( - f"Trace {trace_id} metadata should contain dataset_item_id" - ) - # Get the dataset item ID from metadata and validate it exists - dataset_item_id = trace.metadata["dataset_item_id"] - assert dataset_item_id in dataset_item_map, ( - f"Trace {trace_id} metadata dataset_item_id should correspond to a valid dataset item" - ) + items = get_experiment_items(result.experiment_id, expected_count=1) + assert len(items) == 1 + assert items[0].experiment_item_version is not None + assert items[0].experiment_item_version.astimezone(timezone.utc) == version - # Validate the dataset item input matches the trace input - dataset_item = dataset_item_map[dataset_item_id] - assert dataset_item.input == matching_input, ( - f"Trace {trace_id} should correspond to dataset item with input '{matching_input}'" - ) - assert dataset_run is not None, f"Dataset run {dataset_run_id} should exist" - assert dataset_run.name == result.run_name, "Dataset run should have correct name" - assert dataset_run.description == "Test on Langfuse dataset", ( - "Dataset run should have correct description" +def test_same_run_name_on_dataset_reuses_experiment_id(): + """Re-running with the same run name on a dataset groups into one experiment.""" + langfuse_client = get_client() + dataset_name = "rerun-dataset-" + create_uuid() + langfuse_client.create_dataset(name=dataset_name) + langfuse_client.create_dataset_item( + dataset_name=dataset_name, input="Germany", expected_output="Berlin" ) + dataset = langfuse_client.get_dataset(dataset_name) + run_name = "Shared run " + create_uuid()[:8] - # Get dataset run items to verify trace linkage - dataset_run_items = api.dataset_run_items.list( - dataset_id=dataset.id, run_name=result.run_name - ) - assert len(dataset_run_items.data) == 2, "Dataset run should have 2 items" + first = dataset.run_experiment(name="Rerun", run_name=run_name, task=mock_task) + second = dataset.run_experiment(name="Rerun", run_name=run_name, task=mock_task) + langfuse_client.flush() - # Verify each dataset run item links to the correct trace - run_item_trace_ids = { - item.trace_id for item in dataset_run_items.data if item.trace_id + assert first.experiment_id == second.experiment_id + items = get_experiment_items(first.experiment_id, expected_count=2) + assert {item.trace_id for item in items} == { + r.trace_id for r in first.item_results + second.item_results } - result_trace_ids = {item.trace_id for item in result.item_results} - - assert run_item_trace_ids == result_trace_ids, ( - f"Dataset run items should link to the same traces as experiment results. " - f"Run items: {run_item_trace_ids}, Results: {result_trace_ids}" - ) # Error Handling Tests @@ -630,20 +720,21 @@ def test_run_evaluator(**kwargs): run_evaluators=[test_run_evaluator], ) - assert result.dataset_run_id is not None assert len(result.item_results) == 1 assert len(result.run_evaluations) == 1 langfuse_client.flush() - time.sleep(3) - # Verify scores are persisted via API - api = get_api() - dataset_run = api.datasets.get_run( - dataset_name=dataset_name, run_name=result.run_name + experiment = get_experiment( + result.experiment_id, is_ready=has_score("persistence_run_test") ) + assert experiment.name == "Score persistence test" + run_scores = {s.name: s for s in experiment.scores or []} + assert run_scores["persistence_run_test"].value == 0.9 - assert dataset_run.name == "Score persistence test" + trace_scores = get_trace_scores(result.item_results[0].trace_id, expected_count=1) + assert [(s.name, s.value) for s in trace_scores] == [("persistence_test", 0.85)] + assert trace_scores[0].subject.kind == "observation" def test_multiple_experiments_on_same_dataset(): @@ -688,21 +779,22 @@ def test_multiple_experiments_on_same_dataset(): ) langfuse_client.flush() - time.sleep(2) - # Both experiments should have different run IDs - assert result1.dataset_run_id is not None - assert result2.dataset_run_id is not None - assert result1.dataset_run_id != result2.dataset_run_id + assert result1.experiment_id != result2.experiment_id - # Verify both runs exist in database api = get_api() - runs = api.datasets.get_runs(dataset_name) - assert len(runs.data) >= 2 - - run_names = [run.name for run in runs.data] - assert "Experiment 1" in run_names - assert "Experiment 2" in run_names + experiments = poll( + lambda: ( + api.experiments.list( + from_start_time=_read_window_start(), dataset_id=dataset.id + ).data + ), + lambda data: len(data) >= 2, + ) + assert {e.id: e.name for e in experiments} == { + result1.experiment_id: "Experiment 1", + result2.experiment_id: "Experiment 2", + } # Result Formatting Tests @@ -840,24 +932,24 @@ def mock_task_with_boolean_results(*, item: ExperimentItem, **kwargs): assert run_eval.value is False # Spain should fail, so not all pass assert run_eval.data_type == ScoreDataType.BOOLEAN - # Flush and wait for server processing langfuse_client.flush() - time.sleep(3) # Verify scores are persisted via API with correct data types for i, item_result in enumerate(result.item_results): trace_id = item_result.trace_id assert trace_id is not None, f"Item {i} should have a trace_id" - # Fetch trace from API to verify score persistence - trace = wait_for_trace( - trace_id, - is_result_ready=lambda trace: len(trace.scores) > 0, - ) - assert trace is not None, f"Trace {trace_id} should exist" + scores = get_trace_scores(trace_id, expected_count=1) + assert len(scores) == 1 + assert scores[0].data_type == "BOOLEAN" + assert scores[0].value is expected_results[i] - for score in trace.scores: - assert score.data_type == "BOOLEAN" + experiment = get_experiment( + result.experiment_id, is_ready=has_score("all_items_pass") + ) + run_score = next(s for s in experiment.scores or [] if s.name == "all_items_pass") + assert run_score.data_type == "BOOLEAN" + assert run_score.value is False def test_experiment_composite_evaluator_weighted_average(): diff --git a/tests/e2e/test_media.py b/tests/e2e/test_media.py index d322e1788..ad8a8cb37 100644 --- a/tests/e2e/test_media.py +++ b/tests/e2e/test_media.py @@ -4,7 +4,7 @@ from langfuse._client.client import Langfuse from langfuse.media import LangfuseMedia -from tests.support.utils import wait_for_trace +from tests.support.utils import wait_for_observations def test_replace_media_reference_string_in_object(): @@ -30,25 +30,26 @@ def test_replace_media_reference_string_in_object(): langfuse.flush() - fetched_trace = wait_for_trace( + fetched_observations = wait_for_observations( span.trace_id, - is_result_ready=lambda trace: ( - bool(trace.observations) - and re.match( + is_result_ready=lambda observations: ( + re.match( r"^@@@langfuseMedia:type=audio/wav\|id=.+\|source=base64_data_uri@@@$", - trace.observations[0].metadata.get("context", {}).get("nested", ""), + observations[0].metadata.get("context", {}).get("nested", ""), ) is not None ), ) - media_ref = fetched_trace.observations[0].metadata["context"]["nested"] + assert len(fetched_observations) == 1 + fetched_observation = fetched_observations[0] + media_ref = fetched_observation.metadata["context"]["nested"] assert re.match( r"^@@@langfuseMedia:type=audio/wav\|id=.+\|source=base64_data_uri@@@$", media_ref, ) resolved_obs = langfuse.resolve_media_references( - obj=fetched_trace.observations[0], resolve_with="base64_data_uri" + obj=fetched_observation, resolve_with="base64_data_uri" ) expected_base64 = f"data:audio/wav;base64,{base64_audio}" @@ -61,15 +62,11 @@ def test_replace_media_reference_string_in_object(): langfuse.flush() - fetched_trace2 = wait_for_trace( + fetched_observations2 = wait_for_observations( span2.trace_id, - is_result_ready=lambda trace: ( - bool(trace.observations) - and trace.observations[0].metadata.get("context", {}).get("nested") - == fetched_trace.observations[0].metadata["context"]["nested"] + is_result_ready=lambda observations: ( + observations[0].metadata.get("context", {}).get("nested") == media_ref ), ) - assert ( - fetched_trace2.observations[0].metadata["context"]["nested"] - == fetched_trace.observations[0].metadata["context"]["nested"] - ) + assert len(fetched_observations2) == 1 + assert fetched_observations2[0].metadata["context"]["nested"] == media_ref diff --git a/tests/e2e/test_prompt.py b/tests/e2e/test_prompt.py index 6e113cb41..5b611d1fb 100644 --- a/tests/e2e/test_prompt.py +++ b/tests/e2e/test_prompt.py @@ -1,7 +1,7 @@ import pytest from langfuse._client.client import Langfuse -from tests.support.utils import create_uuid, get_api +from tests.support.utils import create_uuid, wait_for_observations def test_create_prompt(): @@ -429,18 +429,17 @@ def test_prompt_end_to_end(): langfuse.flush() - api = get_api() - - trace = api.trace.get(generation.trace_id) - - assert len(trace.observations) == 1 - - generation = trace.observations[0] - assert generation.prompt_id is not None + observations = wait_for_observations( + generation.trace_id, + is_result_ready=lambda observations: observations[0].prompt_id is not None, + ) - observation = api.legacy.observations_v1.get(generation.id) + assert len(observations) == 1 + observation = observations[0] assert observation.prompt_id is not None + assert observation.prompt_name == "test" + assert observation.prompt_version == prompt.version def test_do_not_return_fallback_if_fetch_success(): @@ -525,11 +524,10 @@ def test_do_not_link_observation_if_fallback(): ).end() langfuse.flush() - api = get_api() - trace = api.trace.get(generation.trace_id) + observations = wait_for_observations(generation.trace_id) - assert len(trace.observations) == 1 - assert trace.observations[0].prompt_id is None + assert len(observations) == 1 + assert observations[0].prompt_id is None def test_variable_names_on_content_with_variable_names(): diff --git a/tests/live_provider/test_langchain.py b/tests/live_provider/test_langchain.py index d04ddb81c..c1ebe8566 100644 --- a/tests/live_provider/test_langchain.py +++ b/tests/live_provider/test_langchain.py @@ -1,3 +1,4 @@ +import json import random import string import time @@ -18,7 +19,12 @@ from langfuse._client.client import Langfuse from langfuse.langchain import CallbackHandler -from tests.support.utils import create_uuid, encode_file_to_base64, get_api +from tests.support.utils import ( + create_uuid, + encode_file_to_base64, + wait_for_observations, + wait_for_trace_snapshot, +) def test_callback_generated_from_trace_chat(): @@ -44,7 +50,7 @@ def test_callback_generated_from_trace_chat(): langfuse.flush() - trace = get_api().trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) assert trace.input is None assert trace.output is None @@ -92,7 +98,7 @@ def test_callback_generated_from_lcel_chain(): langfuse.flush() - trace = get_api().trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) assert trace.input is None assert trace.output is None @@ -151,10 +157,10 @@ def test_basic_chat_openai(): # Ensure data is flushed to API sleep(2) - # Retrieve trace by name - traces = get_api().trace.list(name=test_name) - assert len(traces.data) > 0 - trace = get_api().trace.get(traces.data[0].id) + # Retrieve trace by its root observation, which carries the run name + roots = wait_for_observations(name=test_name) + assert len(roots) > 0 + trace = wait_for_trace_snapshot(roots[0].trace_id, min_observations=2) # Assertions assert trace.name == test_name @@ -193,7 +199,7 @@ def test_callback_simple_openai(): sleep(2) # Retrieve trace - trace = get_api().trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) # Assertions - add 1 for the wrapping span assert len(trace.observations) > 1 @@ -241,7 +247,7 @@ def test_callback_multiple_invocations_on_different_traces(): sleep(2) # Retrieve trace - trace = get_api().trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=3) # Add 1 to account for the wrapping span assert len(trace.observations) > 2 @@ -300,7 +306,7 @@ def test_openai_instruct_usage(): lf_handler._langfuse_client.flush() - observations = get_api().trace.get(trace_id).observations + observations = wait_for_observations(trace_id, min_count=3) assert len(observations) >= 3 assert any( @@ -318,7 +324,7 @@ def test_openai_instruct_usage(): assert observation.output != "" assert observation.input is not None assert observation.input != "" - assert observation.usage is not None + assert observation.usage_details is not None assert observation.usage_details["input"] is not None assert observation.usage_details["output"] is not None assert observation.usage_details["total"] is not None @@ -479,7 +485,7 @@ def test_link_langfuse_prompts_invoke(): langfuse_handler._langfuse_client.flush() sleep(2) - trace = get_api().trace.get(trace_id=trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=4) observations = trace.observations @@ -567,7 +573,12 @@ def test_link_langfuse_prompts_stream(): langfuse_handler._langfuse_client.flush() sleep(2) - trace = get_api().trace.get(trace_id=trace_id) + trace = wait_for_trace_snapshot( + trace_id, + is_result_ready=lambda trace: ( + len([o for o in trace.observations if o.type == "GENERATION"]) >= 4 + ), + ) observations = trace.observations @@ -653,11 +664,26 @@ def test_link_langfuse_prompts_batch(): langfuse_handler._langfuse_client.flush() - traces = get_api().trace.list(name=trace_name).data + trace_name_filter = json.dumps( + [ + { + "type": "string", + "column": "traceName", + "operator": "=", + "value": trace_name, + } + ] + ) + traced_observations = wait_for_observations(filter=trace_name_filter) - assert len(traces) == 1 + assert {o.trace_id for o in traced_observations} == {trace_id} - trace = get_api().trace.get(trace_id=trace_id) + trace = wait_for_trace_snapshot( + trace_id, + is_result_ready=lambda trace: ( + len([o for o in trace.observations if o.type == "GENERATION"]) >= 10 + ), + ) observations = trace.observations @@ -783,7 +809,7 @@ class GetWeather(BaseModel): handler._langfuse_client.flush() - trace = get_api().trace.get(trace_id=trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generations = list(filter(lambda x: x.type == "GENERATION", trace.observations)) assert len(generations) > 0 @@ -882,7 +908,7 @@ def test_multimodal(): handler._langfuse_client.flush() - trace = get_api().trace.get(trace_id=trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) assert len(trace.observations) >= 2 assert any( @@ -979,7 +1005,7 @@ def call_model(state: MessagesState): print(final_state["messages"][-1].content) handler._langfuse_client.flush() - trace = get_api().trace.get(trace_id=trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) assert len(trace.observations) > 0 @@ -1012,7 +1038,7 @@ def test_cached_token_usage(): handler._langfuse_client.flush() - trace = get_api().trace.get(handler.get_trace_id()) + trace = wait_for_trace_snapshot(handler.get_trace_id()) generation = next((o for o in trace.observations if o.type == "GENERATION")) diff --git a/tests/live_provider/test_langchain_integration.py b/tests/live_provider/test_langchain_integration.py index edb5455c4..dd6b166b2 100644 --- a/tests/live_provider/test_langchain_integration.py +++ b/tests/live_provider/test_langchain_integration.py @@ -7,7 +7,7 @@ from langfuse import Langfuse from langfuse.langchain import CallbackHandler -from tests.support.utils import create_uuid, get_api +from tests.support.utils import create_uuid, wait_for_trace_snapshot def _is_streaming_response(response): @@ -45,8 +45,7 @@ def test_stream_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -61,15 +60,15 @@ def test_stream_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output["content"] is not None assert generation.output["role"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -100,8 +99,7 @@ def test_stream_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -116,14 +114,14 @@ def test_stream_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -150,8 +148,7 @@ def test_invoke_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -165,15 +162,15 @@ def test_invoke_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output["content"] is not None assert generation.output["role"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -201,8 +198,7 @@ def test_invoke_in_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -216,14 +212,14 @@ def test_invoke_in_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert test_phrase in generation.output assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -251,8 +247,7 @@ def test_batch_in_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=3) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -266,13 +261,13 @@ def test_batch_in_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -300,8 +295,7 @@ def test_batch_in_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=3) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -314,13 +308,13 @@ def test_batch_in_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -354,8 +348,7 @@ async def test_astream_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -369,15 +362,15 @@ async def test_astream_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output["content"] is not None assert generation.output["role"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -411,8 +404,7 @@ async def test_astream_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -427,14 +419,14 @@ async def test_astream_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert test_phrase in generation.output assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -463,8 +455,7 @@ async def test_ainvoke_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -478,15 +469,15 @@ async def test_ainvoke_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output["content"] is not None assert generation.output["role"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -514,8 +505,7 @@ async def test_ainvoke_in_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=2) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -529,14 +519,14 @@ async def test_ainvoke_in_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert generation.metadata["tags"] == tags - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert test_phrase in generation.output assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -571,8 +561,7 @@ def test_chains_batch_in_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=9) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -585,13 +574,13 @@ def test_chains_batch_in_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -622,8 +611,7 @@ def test_chains_batch_in_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=9) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -636,13 +624,13 @@ def test_chains_batch_in_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -675,8 +663,7 @@ async def test_chains_abatch_in_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=9) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -689,13 +676,13 @@ async def test_chains_abatch_in_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -725,8 +712,7 @@ async def test_chains_abatch_in_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=9) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 assert len(trace.observations) == 9 @@ -738,13 +724,13 @@ async def test_chains_abatch_in_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -778,8 +764,7 @@ async def test_chains_ainvoke_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=5) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -792,15 +777,15 @@ async def test_chains_ainvoke_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output["content"] is not None assert generation.output["role"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -834,8 +819,7 @@ async def test_chains_ainvoke_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=5) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -848,13 +832,13 @@ async def test_chains_ainvoke_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -894,8 +878,7 @@ async def test_chains_astream_chat_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=5) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -910,15 +893,15 @@ async def test_chains_astream_chat_models(model_name): assert generation.model_parameters.get("max_completion_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.output["content"] is not None assert generation.output["role"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None @@ -956,8 +939,7 @@ async def test_chains_astream_completions_models(model_name): langfuse_client.flush() assert handler.runs == {} - api = get_api() - trace = api.trace.get(trace_id) + trace = wait_for_trace_snapshot(trace_id, min_observations=5) generationList = list(filter(lambda o: o.type == "GENERATION", trace.observations)) assert len(generationList) != 0 @@ -972,11 +954,11 @@ async def test_chains_astream_completions_models(model_name): assert generation.model_parameters.get("max_tokens") is not None assert generation.model_parameters.get("temperature") is not None assert all(x in generation.metadata["tags"] for x in tags) - assert generation.usage.output is not None - assert generation.usage.total is not None + assert generation.usage_details["output"] is not None + assert generation.usage_details["total"] is not None assert generation.input_price is not None assert generation.output_price is not None - assert generation.calculated_input_cost is not None - assert generation.calculated_output_cost is not None - assert generation.calculated_total_cost is not None + assert generation.cost_details["input"] is not None + assert generation.cost_details["output"] is not None + assert generation.total_cost is not None assert generation.latency is not None diff --git a/tests/live_provider/test_openai.py b/tests/live_provider/test_openai.py index 43a07be23..18dc61b81 100644 --- a/tests/live_provider/test_openai.py +++ b/tests/live_provider/test_openai.py @@ -6,7 +6,11 @@ from pydantic import BaseModel from langfuse._client.client import Langfuse -from tests.support.utils import create_uuid, encode_file_to_base64, get_api +from tests.support.utils import ( + create_uuid, + encode_file_to_base64, + wait_for_observations, +) langfuse: Langfuse | None = None @@ -58,27 +62,25 @@ def test_openai_chat_completion(openai): sleep(1) - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" assert len(completion.choices) != 0 - assert generation.data[0].input == [ + assert generation[0].input == [ { "content": "You are an expert mathematician", "role": "assistant", }, {"content": "1 + 1 = ", "role": "user"}, ] - assert generation.data[0].type == "GENERATION" - assert "gpt-3.5-turbo-0125" in generation.data[0].model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].type == "GENERATION" + assert "gpt-3.5-turbo-0125" in generation[0].model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 0, "top_p": 1, @@ -86,11 +88,11 @@ def test_openai_chat_completion(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert "2" in generation.data[0].output["content"] - assert generation.data[0].output["role"] == "assistant" + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert "2" in generation[0].output["content"] + assert generation[0].output["role"] == "assistant" def test_openai_chat_completion_stream(openai): @@ -116,21 +118,19 @@ def test_openai_chat_completion_stream(openai): langfuse.flush() sleep(3) - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert "gpt-3.5-turbo-0125" in generation.data[0].model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert "gpt-3.5-turbo-0125" in generation[0].model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 0, "top_p": 1, @@ -138,16 +138,16 @@ def test_openai_chat_completion_stream(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].output == 2 - assert generation.data[0].completion_start_time is not None + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert generation[0].output == "2" + assert generation[0].completion_start_time is not None # Completion start time for time-to-first-token - assert generation.data[0].completion_start_time is not None - assert generation.data[0].completion_start_time >= generation.data[0].start_time - assert generation.data[0].completion_start_time <= generation.data[0].end_time + assert generation[0].completion_start_time is not None + assert generation[0].completion_start_time >= generation[0].start_time + assert generation[0].completion_start_time <= generation[0].end_time def test_openai_chat_completion_stream_with_next_iteration(openai): @@ -177,21 +177,19 @@ def test_openai_chat_completion_stream_with_next_iteration(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-3.5-turbo-0125" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-3.5-turbo-0125" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 0, "top_p": 1, @@ -199,16 +197,16 @@ def test_openai_chat_completion_stream_with_next_iteration(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].output == 2 - assert generation.data[0].completion_start_time is not None + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert generation[0].output == "2" + assert generation[0].completion_start_time is not None # Completion start time for time-to-first-token - assert generation.data[0].completion_start_time is not None - assert generation.data[0].completion_start_time >= generation.data[0].start_time - assert generation.data[0].completion_start_time <= generation.data[0].end_time + assert generation[0].completion_start_time is not None + assert generation[0].completion_start_time >= generation[0].start_time + assert generation[0].completion_start_time <= generation[0].end_time def test_openai_chat_completion_stream_fail(openai): @@ -227,33 +225,28 @@ def test_openai_chat_completion_stream_fail(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "fake" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" + + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert generation[0].model == "fake" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "temperature": 0, "top_p": 1, "frequency_penalty": 0, "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].level == "ERROR" - assert generation.data[0].status_message is not None - assert generation.data[0].output is None + assert generation[0].level == "ERROR" + assert generation[0].status_message is not None + assert generation[0].output is None openai.api_key = os.environ["OPENAI_API_KEY"] @@ -277,13 +270,11 @@ def test_openai_chat_completion_with_langfuse_prompt(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert isinstance(generation.data[0].prompt_id, str) + assert len(generation) != 0 + assert generation[0].name == generation_name + assert isinstance(generation[0].prompt_id, str) def test_openai_chat_completion_fail(openai): @@ -300,29 +291,27 @@ def test_openai_chat_completion_fail(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) - - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "fake" - assert generation.data[0].level == "ERROR" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].status_message is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert generation[0].model == "fake" + assert generation[0].level == "ERROR" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].status_message is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "temperature": 0, "top_p": 1, "frequency_penalty": 0, "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].output is None + assert generation[0].output is None openai.api_key = os.environ["OPENAI_API_KEY"] @@ -360,25 +349,21 @@ def test_openai_chat_completion_two_calls(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name + assert len(generation) != 0 + assert generation[0].name == generation_name assert len(completion.choices) != 0 - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] - generation_2 = get_api().legacy.observations_v1.get_many( - name=generation_name_2, type="GENERATION" - ) + generation_2 = wait_for_observations(name=generation_name_2, type="GENERATION") - assert len(generation_2.data) != 0 - assert generation_2.data[0].name == generation_name_2 + assert len(generation_2) != 0 + assert generation_2[0].name == generation_name_2 assert len(completion_2.choices) != 0 - assert generation_2.data[0].input == [{"content": "2 + 2 = ", "role": "user"}] + assert generation_2[0].input == [{"content": "2 + 2 = ", "role": "user"}] def test_openai_chat_completion_with_seed(openai): @@ -394,11 +379,9 @@ def test_openai_chat_completion_with_seed(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert generation.data[0].model_parameters == { + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 0, "top_p": 1, @@ -423,32 +406,30 @@ def test_openai_completion(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" assert len(completion.choices) != 0 - assert completion.choices[0].text == generation.data[0].output - assert generation.data[0].input == "1 + 1 = " - assert generation.data[0].type == "GENERATION" - assert "gpt-3.5-turbo-instruct" in generation.data[0].model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert completion.choices[0].text == generation[0].output + assert generation[0].input == "1 + 1 = " + assert generation[0].type == "GENERATION" + assert "gpt-3.5-turbo-instruct" in generation[0].model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "temperature": 0, "top_p": 1, "frequency_penalty": 0, "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].output == "2\n\n1 + 2 = 3\n\n2 + 3 = " + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert generation[0].output == "2\n\n1 + 2 = 3\n\n2 + 3 = " @requires_legacy_completion_model @@ -472,37 +453,35 @@ def test_openai_completion_stream(openai): assert len(content) > 0 - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - - assert generation.data[0].input == "1 + 1 = " - assert generation.data[0].type == "GENERATION" - assert "gpt-3.5-turbo-instruct" in generation.data[0].model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" + + assert generation[0].input == "1 + 1 = " + assert generation[0].type == "GENERATION" + assert "gpt-3.5-turbo-instruct" in generation[0].model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "temperature": 0, "top_p": 1, "frequency_penalty": 0, "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].output == "2\n\n1 + 2 = 3\n\n2 + 3 = " - assert generation.data[0].completion_start_time is not None + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert generation[0].output == "2\n\n1 + 2 = 3\n\n2 + 3 = " + assert generation[0].completion_start_time is not None # Completion start time for time-to-first-token - assert generation.data[0].completion_start_time is not None - assert generation.data[0].completion_start_time >= generation.data[0].start_time - assert generation.data[0].completion_start_time <= generation.data[0].end_time + assert generation[0].completion_start_time is not None + assert generation[0].completion_start_time >= generation[0].start_time + assert generation[0].completion_start_time <= generation[0].end_time def test_openai_completion_fail(openai): @@ -521,29 +500,27 @@ def test_openai_completion_fail(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) - - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - assert generation.data[0].input == "1 + 1 = " - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "fake" - assert generation.data[0].level == "ERROR" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].status_message is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" + assert generation[0].input == "1 + 1 = " + assert generation[0].type == "GENERATION" + assert generation[0].model == "fake" + assert generation[0].level == "ERROR" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].status_message is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "temperature": 0, "top_p": 1, "frequency_penalty": 0, "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].output is None + assert generation[0].output is None openai.api_key = os.environ["OPENAI_API_KEY"] @@ -564,33 +541,28 @@ def test_openai_completion_stream_fail(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - - assert generation.data[0].input == "1 + 1 = " - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-3.5-turbo" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].input == "1 + 1 = " + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-3.5-turbo" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "temperature": 0, "top_p": 1, "frequency_penalty": 0, "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].level == "ERROR" - assert generation.data[0].status_message is not None - assert generation.data[0].output is None + assert generation[0].level == "ERROR" + assert generation[0].status_message is not None + assert generation[0].output is None openai.api_key = os.environ["OPENAI_API_KEY"] @@ -614,13 +586,11 @@ def test_openai_completion_with_langfuse_prompt(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert isinstance(generation.data[0].prompt_id, str) + assert len(generation) != 0 + assert generation[0].name == generation_name + assert isinstance(generation[0].prompt_id, str) def test_fails_wrong_name(openai): @@ -656,21 +626,19 @@ async def test_async_chat(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name + assert len(generation) != 0 + assert generation[0].name == generation_name assert len(completion.choices) != 0 - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-3.5-turbo-0125" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-3.5-turbo-0125" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 1, "top_p": 1, @@ -678,11 +646,11 @@ async def test_async_chat(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert "2" in generation.data[0].output["content"] - assert generation.data[0].output["role"] == "assistant" + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert "2" in generation[0].output["content"] + assert generation[0].output["role"] == "assistant" @pytest.mark.asyncio @@ -703,19 +671,17 @@ async def test_async_chat_stream(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) - - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-3.5-turbo-0125" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-3.5-turbo-0125" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 1, "top_p": 1, @@ -723,15 +689,15 @@ async def test_async_chat_stream(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert "2" in str(generation.data[0].output) + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert "2" in str(generation[0].output) # Completion start time for time-to-first-token - assert generation.data[0].completion_start_time is not None - assert generation.data[0].completion_start_time >= generation.data[0].start_time - assert generation.data[0].completion_start_time <= generation.data[0].end_time + assert generation[0].completion_start_time is not None + assert generation[0].completion_start_time >= generation[0].start_time + assert generation[0].completion_start_time <= generation[0].end_time @pytest.mark.asyncio @@ -762,21 +728,19 @@ async def test_async_chat_stream_with_anext(openai): print(result) - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].input == [ + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].input == [ {"content": "Give me a one-liner joke", "role": "user"} ] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-3.5-turbo-0125" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-3.5-turbo-0125" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 1, "top_p": 1, @@ -784,14 +748,14 @@ async def test_async_chat_stream_with_anext(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None # Completion start time for time-to-first-token - assert generation.data[0].completion_start_time is not None - assert generation.data[0].completion_start_time >= generation.data[0].start_time - assert generation.data[0].completion_start_time <= generation.data[0].end_time + assert generation[0].completion_start_time is not None + assert generation[0].completion_start_time >= generation[0].start_time + assert generation[0].completion_start_time <= generation[0].end_time def test_openai_function_call(openai): @@ -825,14 +789,12 @@ class StepByStepAIResponse(BaseModel): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].output is not None - assert "function_call" in generation.data[0].output + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].output is not None + assert "function_call" in generation[0].output assert output["title"] is not None @@ -869,14 +831,12 @@ class StepByStepAIResponse(BaseModel): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].output is not None - assert "function_call" in generation.data[0].output + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].output is not None + assert "function_call" in generation[0].output def test_openai_tool_call(openai): @@ -913,21 +873,17 @@ def test_openai_tool_call(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name + assert len(generation) != 0 + assert generation[0].name == generation_name assert ( - generation.data[0].output["tool_calls"][0]["function"]["name"] + generation[0].output["tool_calls"][0]["function"]["name"] == "get_current_weather" ) - assert ( - generation.data[0].output["tool_calls"][0]["function"]["arguments"] is not None - ) - assert generation.data[0].input["tools"] == tools - assert generation.data[0].input["messages"] == messages + assert generation[0].output["tool_calls"][0]["function"]["arguments"] is not None + assert generation[0].input["tools"] == tools + assert generation[0].input["messages"] == messages def test_openai_tool_call_streamed(openai): @@ -969,22 +925,18 @@ def test_openai_tool_call_streamed(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name + assert len(generation) != 0 + assert generation[0].name == generation_name assert ( - generation.data[0].output["tool_calls"][0]["function"]["name"] + generation[0].output["tool_calls"][0]["function"]["name"] == "get_current_weather" ) - assert ( - generation.data[0].output["tool_calls"][0]["function"]["arguments"] is not None - ) - assert generation.data[0].input["tools"] == tools - assert generation.data[0].input["messages"] == messages + assert generation[0].output["tool_calls"][0]["function"]["arguments"] is not None + assert generation[0].input["tools"] == tools + assert generation[0].input["messages"] == messages def test_langchain_integration(openai): @@ -1047,28 +999,28 @@ def test_structured_output_response_format_kwarg(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" + generation = wait_for_observations( + name=generation_name, type="GENERATION", expand_metadata="response_format" ) - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].metadata["someKey"] == "someResponse" - assert generation.data[0].metadata["response_format"] == { + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].metadata["someKey"] == "someResponse" + assert generation[0].metadata["response_format"] == { "type": "json_schema", "json_schema": json_schema, } - assert generation.data[0].input == [ + assert generation[0].input == [ {"role": "system", "content": "You are a helpful math tutor."}, {"content": "solve 8x + 31 = 2", "role": "user"}, ] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-4o-2024-08-06" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-4o-2024-08-06" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 1, "top_p": 1, @@ -1076,10 +1028,10 @@ def test_structured_output_response_format_kwarg(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert generation.data[0].output["role"] == "assistant" + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert generation[0].output["role"] == "assistant" def test_structured_output_beta_completions_parse(openai): @@ -1117,31 +1069,29 @@ class CalendarEvent(BaseModel): if Version(openai.__version__) >= Version("1.50.0"): # Check the trace and observation properties - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) == 1 - assert generation.data[0].name == generation_name - assert generation.data[0].type == "GENERATION" - assert "gpt-4o" in generation.data[0].model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time + assert len(generation) == 1 + assert generation[0].name == generation_name + assert generation[0].type == "GENERATION" + assert "gpt-4o" in generation[0].model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time # Check input and output - assert len(generation.data[0].input) == 2 - assert generation.data[0].input[0]["role"] == "system" - assert generation.data[0].input[1]["role"] == "user" - assert isinstance(generation.data[0].output, dict) - assert "name" in generation.data[0].output["content"] - assert "date" in generation.data[0].output["content"] - assert "participants" in generation.data[0].output["content"] + assert len(generation[0].input) == 2 + assert generation[0].input[0]["role"] == "system" + assert generation[0].input[1]["role"] == "user" + assert isinstance(generation[0].output, dict) + assert "name" in generation[0].output["content"] + assert "date" in generation[0].output["content"] + assert "participants" in generation[0].output["content"] # Check usage - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None @pytest.mark.asyncio @@ -1163,19 +1113,17 @@ async def test_close_async_stream(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) - - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].input == [{"content": "1 + 1 = ", "role": "user"}] - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == "gpt-3.5-turbo-0125" - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].model_parameters == { + generation = wait_for_observations(name=generation_name, type="GENERATION") + + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].input == [{"content": "1 + 1 = ", "role": "user"}] + assert generation[0].type == "GENERATION" + assert generation[0].model == "gpt-3.5-turbo-0125" + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].model_parameters == { "service_tier": "default", "temperature": 1, "top_p": 1, @@ -1183,15 +1131,15 @@ async def test_close_async_stream(openai): "max_tokens": "Infinity", "presence_penalty": 0, } - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert "2" in str(generation.data[0].output) + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert "2" in str(generation[0].output) # Completion start time for time-to-first-token - assert generation.data[0].completion_start_time is not None - assert generation.data[0].completion_start_time >= generation.data[0].start_time - assert generation.data[0].completion_start_time <= generation.data[0].end_time + assert generation[0].completion_start_time is not None + assert generation[0].completion_start_time >= generation[0].start_time + assert generation[0].completion_start_time <= generation[0].end_time def test_base_64_image_input(openai): @@ -1225,26 +1173,24 @@ def test_base_64_image_input(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name - assert generation.data[0].input[0]["content"][0]["text"] == "What’s in this image?" + assert len(generation) != 0 + assert generation[0].name == generation_name + assert generation[0].input[0]["content"][0]["text"] == "What’s in this image?" assert ( f"@@@langfuseMedia:type={content_type}|id=" - in generation.data[0].input[0]["content"][1]["image_url"]["url"] + in generation[0].input[0]["content"][1]["image_url"]["url"] ) - assert generation.data[0].type == "GENERATION" - assert "gpt-4o-mini" in generation.data[0].model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - assert "dog" in generation.data[0].output["content"] + assert generation[0].type == "GENERATION" + assert "gpt-4o-mini" in generation[0].model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + assert "dog" in generation[0].output["content"] def test_audio_input_and_output(openai): @@ -1277,32 +1223,28 @@ def test_audio_input_and_output(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - assert generation.data[0].name == generation_name + assert len(generation) != 0 + assert generation[0].name == generation_name assert ( - generation.data[0].input[0]["content"][0]["text"] - == "Do what this recording says." + generation[0].input[0]["content"][0]["text"] == "Do what this recording says." ) assert ( "@@@langfuseMedia:type=audio/wav|id=" - in generation.data[0].input[0]["content"][1]["input_audio"]["data"] - ) - assert generation.data[0].type == "GENERATION" - assert generation.data[0].model == model - assert generation.data[0].start_time is not None - assert generation.data[0].end_time is not None - assert generation.data[0].start_time < generation.data[0].end_time - assert generation.data[0].usage.input is not None - assert generation.data[0].usage.output is not None - assert generation.data[0].usage.total is not None - print(generation.data[0].output) + in generation[0].input[0]["content"][1]["input_audio"]["data"] + ) + assert generation[0].type == "GENERATION" + assert generation[0].model == model + assert generation[0].start_time is not None + assert generation[0].end_time is not None + assert generation[0].start_time < generation[0].end_time + assert generation[0].usage_details["input"] is not None + assert generation[0].usage_details["output"] is not None + assert generation[0].usage_details["total"] is not None + print(generation[0].output) assert ( - "@@@langfuseMedia:type=audio/wav|id=" - in generation.data[0].output["audio"]["data"] + "@@@langfuseMedia:type=audio/wav|id=" in generation[0].output["audio"]["data"] ) @@ -1317,25 +1259,22 @@ def test_response_api_text_input(openai): ) langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - generationData = generation.data[0] + assert len(generation) != 0 + generationData = generation[0] assert generationData.name == generation_name assert ( - generation.data[0].input - == "Tell me a three sentence bedtime story about a unicorn." + generation[0].input == "Tell me a three sentence bedtime story about a unicorn." ) assert generationData.type == "GENERATION" assert "gpt-4o" in generationData.model assert generationData.start_time is not None assert generationData.end_time is not None assert generationData.start_time < generationData.end_time - assert generationData.usage.input is not None - assert generationData.usage.output is not None - assert generationData.usage.total is not None + assert generationData.usage_details["input"] is not None + assert generationData.usage_details["output"] is not None + assert generationData.usage_details["total"] is not None assert generationData.output is not None @@ -1363,22 +1302,20 @@ def test_response_api_image_input(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - generationData = generation.data[0] + assert len(generation) != 0 + generationData = generation[0] assert generationData.name == generation_name - assert generation.data[0].input[0]["content"][0]["text"] == "what is in this image?" + assert generation[0].input[0]["content"][0]["text"] == "what is in this image?" assert generationData.type == "GENERATION" assert "gpt-4o" in generationData.model assert generationData.start_time is not None assert generationData.end_time is not None assert generationData.start_time < generationData.end_time - assert generationData.usage.input is not None - assert generationData.usage.output is not None - assert generationData.usage.total is not None + assert generationData.usage_details["input"] is not None + assert generationData.usage_details["output"] is not None + assert generationData.usage_details["total"] is not None assert generationData.output is not None @@ -1395,12 +1332,10 @@ def test_response_api_web_search(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - generationData = generation.data[0] + assert len(generation) != 0 + generationData = generation[0] assert generationData.name == generation_name assert generationData.input == { "input": "What was a positive news story from today?", @@ -1411,9 +1346,9 @@ def test_response_api_web_search(openai): assert generationData.start_time is not None assert generationData.end_time is not None assert generationData.start_time < generationData.end_time - assert generationData.usage.input is not None - assert generationData.usage.output is not None - assert generationData.usage.total is not None + assert generationData.usage_details["input"] is not None + assert generationData.usage_details["output"] is not None + assert generationData.usage_details["total"] is not None assert generationData.output is not None assert generationData.metadata is not None @@ -1435,14 +1370,12 @@ def test_response_api_streaming(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - generationData = generation.data[0] + assert len(generation) != 0 + generationData = generation[0] assert generationData.name == generation_name - assert generation.data[0].input == [ + assert generation[0].input == [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"}, ] @@ -1451,9 +1384,9 @@ def test_response_api_streaming(openai): assert generationData.start_time is not None assert generationData.end_time is not None assert generationData.start_time < generationData.end_time - assert generationData.usage.input is not None - assert generationData.usage.output is not None - assert generationData.usage.total is not None + assert generationData.usage_details["input"] is not None + assert generationData.usage_details["output"] is not None + assert generationData.usage_details["total"] is not None assert generationData.output is not None assert generationData.metadata is not None assert generationData.metadata["instructions"] == "You are a helpful assistant." @@ -1492,14 +1425,12 @@ def test_response_api_functions(openai): langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - generationData = generation.data[0] + assert len(generation) != 0 + generationData = generation[0] assert generationData.name == generation_name - assert generation.data[0].input == { + assert generation[0].input == { "input": "What is the weather like in Boston today?", "tools": tools, "tool_choice": "auto", @@ -1509,9 +1440,9 @@ def test_response_api_functions(openai): assert generationData.start_time is not None assert generationData.end_time is not None assert generationData.start_time < generationData.end_time - assert generationData.usage.input is not None - assert generationData.usage.output is not None - assert generationData.usage.total is not None + assert generationData.usage_details["input"] is not None + assert generationData.usage_details["output"] is not None + assert generationData.usage_details["total"] is not None assert generationData.output is not None assert generationData.metadata is not None @@ -1528,22 +1459,20 @@ def test_response_api_reasoning(openai): ) langfuse.flush() - generation = get_api().legacy.observations_v1.get_many( - name=generation_name, type="GENERATION" - ) + generation = wait_for_observations(name=generation_name, type="GENERATION") - assert len(generation.data) != 0 - generationData = generation.data[0] + assert len(generation) != 0 + generationData = generation[0] assert generationData.name == generation_name - assert generation.data[0].input == "How much wood would a woodchuck chuck?" + assert generation[0].input == "How much wood would a woodchuck chuck?" assert generationData.type == "GENERATION" assert "o3-mini" in generationData.model assert generationData.start_time is not None assert generationData.end_time is not None assert generationData.start_time < generationData.end_time - assert generationData.usage.input is not None - assert generationData.usage.output is not None - assert generationData.usage.total is not None + assert generationData.usage_details["input"] is not None + assert generationData.usage_details["output"] is not None + assert generationData.usage_details["total"] is not None assert generationData.output is not None assert generationData.metadata is not None @@ -1560,12 +1489,10 @@ def test_openai_embeddings(openai): langfuse.flush() sleep(1) - embedding = get_api().legacy.observations_v1.get_many( - name=embedding_name, type="EMBEDDING" - ) + embedding = wait_for_observations(name=embedding_name, type="EMBEDDING") - assert len(embedding.data) != 0 - embedding_data = embedding.data[0] + assert len(embedding) != 0 + embedding_data = embedding[0] assert embedding_data.name == embedding_name assert embedding_data.metadata["test_key"] == "test_value" assert embedding_data.input == "The quick brown fox jumps over the lazy dog" @@ -1574,8 +1501,8 @@ def test_openai_embeddings(openai): assert embedding_data.start_time is not None assert embedding_data.end_time is not None assert embedding_data.start_time < embedding_data.end_time - assert embedding_data.usage.input is not None - assert embedding_data.usage.total is not None + assert embedding_data.usage_details["input"] is not None + assert embedding_data.usage_details["total"] is not None assert embedding_data.output is not None assert "dimensions" in embedding_data.output assert "count" in embedding_data.output @@ -1596,18 +1523,16 @@ def test_openai_embeddings_multiple_inputs(openai): langfuse.flush() sleep(1) - embedding = get_api().legacy.observations_v1.get_many( - name=embedding_name, type="EMBEDDING" - ) + embedding = wait_for_observations(name=embedding_name, type="EMBEDDING") - assert len(embedding.data) != 0 - embedding_data = embedding.data[0] + assert len(embedding) != 0 + embedding_data = embedding[0] assert embedding_data.name == embedding_name assert embedding_data.input == inputs assert embedding_data.type == "EMBEDDING" assert "text-embedding-ada-002" in embedding_data.model - assert embedding_data.usage.input is not None - assert embedding_data.usage.total is not None + assert embedding_data.usage_details["input"] is not None + assert embedding_data.usage_details["total"] is not None assert embedding_data.output["count"] == len(inputs) @@ -1629,16 +1554,14 @@ async def test_async_openai_embeddings(openai): langfuse.flush() sleep(1) - embedding = get_api().legacy.observations_v1.get_many( - name=embedding_name, type="EMBEDDING" - ) + embedding = wait_for_observations(name=embedding_name, type="EMBEDDING") - assert len(embedding.data) != 0 - embedding_data = embedding.data[0] + assert len(embedding) != 0 + embedding_data = embedding[0] assert embedding_data.name == embedding_name assert embedding_data.input == "Async embedding test" assert embedding_data.type == "EMBEDDING" assert "text-embedding-ada-002" in embedding_data.model assert embedding_data.metadata["async"] is True - assert embedding_data.usage.input is not None - assert embedding_data.usage.total is not None + assert embedding_data.usage_details["input"] is not None + assert embedding_data.usage_details["total"] is not None diff --git a/tests/support/api_wrapper.py b/tests/support/api_wrapper.py deleted file mode 100644 index c4519252f..000000000 --- a/tests/support/api_wrapper.py +++ /dev/null @@ -1,130 +0,0 @@ -import os - -import httpx - -from langfuse.api.commons.errors.not_found_error import NotFoundError -from tests.support.retry import ( - DEFAULT_RETRY_INTERVAL_SECONDS, - DEFAULT_RETRY_TIMEOUT_SECONDS, - is_not_found_payload, - retry_until_ready, -) - - -class LangfuseAPI: - def __init__(self, username=None, password=None, base_url=None): - username = username if username else os.environ["LANGFUSE_PUBLIC_KEY"] - password = password if password else os.environ["LANGFUSE_SECRET_KEY"] - self.auth = (username, password) - self.BASE_URL = base_url if base_url else os.environ["LANGFUSE_BASE_URL"] - - def _get_json( - self, - url, - params=None, - *, - retry=True, - is_result_ready=None, - timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, - interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, - ): - def _request(): - response = httpx.get(url, params=params, auth=self.auth) - payload = response.json() - - if response.status_code == 404 and is_not_found_payload(payload): - raise NotFoundError(body=payload, headers=dict(response.headers)) - - return payload - - if not retry: - return _request() - - return retry_until_ready( - _request, - is_result_ready=is_result_ready, - timeout_seconds=timeout_seconds, - interval_seconds=interval_seconds, - ) - - def get_observation( - self, - observation_id, - *, - retry=True, - is_result_ready=None, - timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, - interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, - ): - url = f"{self.BASE_URL}/api/public/observations/{observation_id}" - return self._get_json( - url, - retry=retry, - is_result_ready=is_result_ready, - timeout_seconds=timeout_seconds, - interval_seconds=interval_seconds, - ) - - def get_scores( - self, - page=None, - limit=None, - user_id=None, - name=None, - *, - retry=True, - is_result_ready=None, - timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, - interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, - ): - params = {"page": page, "limit": limit, "userId": user_id, "name": name} - url = f"{self.BASE_URL}/api/public/scores" - return self._get_json( - url, - params=params, - retry=retry, - is_result_ready=is_result_ready, - timeout_seconds=timeout_seconds, - interval_seconds=interval_seconds, - ) - - def get_traces( - self, - page=None, - limit=None, - user_id=None, - name=None, - *, - retry=True, - is_result_ready=None, - timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, - interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, - ): - params = {"page": page, "limit": limit, "userId": user_id, "name": name} - url = f"{self.BASE_URL}/api/public/traces" - return self._get_json( - url, - params=params, - retry=retry, - is_result_ready=is_result_ready, - timeout_seconds=timeout_seconds, - interval_seconds=interval_seconds, - ) - - def get_trace( - self, - trace_id, - *, - retry=True, - is_result_ready=None, - timeout_seconds=DEFAULT_RETRY_TIMEOUT_SECONDS, - interval_seconds=DEFAULT_RETRY_INTERVAL_SECONDS, - ): - url = f"{self.BASE_URL}/api/public/traces/{trace_id}" - return self._get_json( - url, - retry=retry, - is_result_ready=is_result_ready, - timeout_seconds=timeout_seconds, - interval_seconds=interval_seconds, - ) diff --git a/tests/support/utils.py b/tests/support/utils.py index a29274d3a..fc827eacb 100644 --- a/tests/support/utils.py +++ b/tests/support/utils.py @@ -1,19 +1,56 @@ import base64 +import json import os -from typing import Any, Callable, TypeVar +from dataclasses import dataclass +from typing import Any, Callable, Sequence, TypeVar from uuid import uuid4 -from langfuse.api import LangfuseAPI +from langfuse._version import __version__ as langfuse_version +from langfuse.api import LangfuseAPI, ObservationV2, ScoreV3 from tests.support.retry import ( DEFAULT_RETRY_INTERVAL_SECONDS, DEFAULT_RETRY_TIMEOUT_SECONDS, retry_until_ready, ) -READ_METHOD_NAMES = {"get", "get_by_id", "get_many", "get_run", "list"} -PAGINATION_ARGUMENTS = {"limit", "page"} +READ_METHOD_NAMES = {"get", "get_by_id", "get_many", "get_many_v3", "get_run", "list"} +PAGINATION_ARGUMENTS = {"limit", "page", "cursor", "fields", "expand_metadata"} T = TypeVar("T") +ALL_OBSERVATION_FIELDS = ( + "core,basic,time,io,metadata,model,usage,prompt,metrics,trace_context" +) +SCORE_FIELDS = "details,subject" + +# The v2 observations API returns "" instead of null for unset string fields. +_EMPTY_AS_NONE_FIELDS = ( + "name", + "status_message", + "version", + "user_id", + "session_id", + "model", + "internal_model_id", + "prompt_id", + "prompt_name", + "trace_name", + "release", +) +_SDK_METADATA_KEY_PREFIXES = ("scope.", "resourceAttributes.") + +# The server JSON-decodes per-key observation metadata values only for Python +# SDK major >= 5 (langfuse/langfuse#18436). Older SDK versions read back the raw +# JSON string on endpoints that do not parse metadata values on read. +_SERVER_DECODES_METADATA_VALUES = int(langfuse_version.split(".")[0]) >= 5 + + +def raw_metadata_value(value: Any) -> Any: + """Per-key metadata value as stored by the server for this SDK version.""" + if _SERVER_DECODES_METADATA_VALUES: + return value + + return json.dumps(value, separators=(",", ":"), ensure_ascii=False) + def _has_filters(kwargs: dict[str, Any]) -> bool: return any( @@ -51,7 +88,9 @@ def _call(*args: Any, **kwargs: Any) -> Any: def _result_ready(method_name: str, kwargs: dict[str, Any]): - if method_name not in {"get_many", "list"} or not _has_filters(kwargs): + if method_name not in {"get_many", "get_many_v3", "list"} or not _has_filters( + kwargs + ): return None def _has_data(result: Any) -> bool: @@ -89,17 +128,315 @@ def wait_for_result( ) -def wait_for_trace( +def _parse_json_string(value: Any) -> Any: + # Only objects and arrays: the SDK sends string IO unquoted, so text such as + # "2" or "true" must stay a string. + if not isinstance(value, str) or not value.lstrip().startswith(("{", "[")): + return value + + try: + return json.loads(value) + except ValueError: + return value + + +def normalize_observation(observation: ObservationV2) -> ObservationV2: + """Map a v2 observation to the shape the SDK sent. + + The v2 API returns input/output as raw strings and unset string fields as + "". This parses JSON object/array input/output and maps "" to None. + """ + update: dict[str, Any] = { + "input": _parse_json_string(observation.input), + "output": _parse_json_string(observation.output), + } + for field in _EMPTY_AS_NONE_FIELDS: + if getattr(observation, field, None) == "": + update[field] = None + + return observation.model_copy(update=update) + + +def user_metadata(observation: ObservationV2) -> dict[str, Any]: + """Observation metadata without the scope/resource keys the server adds.""" + metadata = observation.metadata or {} + assert isinstance(metadata, dict), metadata + + return { + key: value + for key, value in metadata.items() + if not key.startswith(_SDK_METADATA_KEY_PREFIXES) + } + + +def get_observations( + *, + fields: str = ALL_OBSERVATION_FIELDS, + api: Any = None, + **filters: Any, +) -> list[ObservationV2]: + """Fetch all observations matching `filters` (all pages), oldest first.""" + api = api or get_api(retry=False) + observations: list[ObservationV2] = [] + cursor = None + + while True: + response = api.observations.get_many( + fields=fields, limit=1000, cursor=cursor, **filters + ) + observations.extend(response.data) + cursor = response.meta.cursor + if not cursor or not response.data: + break + + # The events table can briefly return several rows for one span until + # ClickHouse merges them; keep the most recently updated row per id. + latest_by_id: dict[str, ObservationV2] = {} + for observation in observations: + current = latest_by_id.get(observation.id) + if current is None or (observation.updated_at or observation.start_time) >= ( + current.updated_at or current.start_time + ): + latest_by_id[observation.id] = observation + + return sorted( + (normalize_observation(observation) for observation in latest_by_id.values()), + key=lambda observation: observation.start_time, + ) + + +def wait_for_observations( + trace_id: str | None = None, + *, + min_count: int = 1, + is_result_ready: Callable[[list[ObservationV2]], bool] | None = None, + fields: str = ALL_OBSERVATION_FIELDS, + timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, + **filters: Any, +) -> list[ObservationV2]: + """Poll the v2 observations API until at least `min_count` observations + match (and `is_result_ready` holds), then return them oldest first.""" + if trace_id is not None: + filters["trace_id"] = trace_id + assert filters, "wait_for_observations needs at least one filter" + + def _ready(observations: list[ObservationV2]) -> bool: + return len(observations) >= min_count and ( + is_result_ready is None or is_result_ready(observations) + ) + + return wait_for_result( + lambda: get_observations(fields=fields, **filters), + is_result_ready=_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + +def get_root_observation(observations: Sequence[ObservationV2]) -> ObservationV2: + """Return the single root observation, which carries the trace-level + attributes (trace name, user, session, tags, public) and trace IO.""" + roots = [ + observation for observation in observations if observation.is_root_observation + ] + assert len(roots) == 1, ( + f"expected exactly one root observation, got {[r.name for r in roots]}" + ) + + return roots[0] + + +def wait_for_root_observation( + trace_id: str, + *, + min_count: int = 1, + is_result_ready: Callable[[ObservationV2], bool] | None = None, + timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, +) -> ObservationV2: + def _ready(observations: list[ObservationV2]) -> bool: + roots = [o for o in observations if o.is_root_observation] + return len(roots) == 1 and ( + is_result_ready is None or is_result_ready(roots[0]) + ) + + return get_root_observation( + wait_for_observations( + trace_id, + min_count=min_count, + is_result_ready=_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + ) + + +@dataclass(frozen=True) +class TraceSnapshot: + """A trace as v4 exposes it: its observations plus (optionally) scores. + + Mirrors how the platform aggregates events into a trace: input, output + and metadata come from the root observation; name, user, session, + version, release and environment are the latest non-empty value across + all observations; tags are the union; public is true if any observation + is public. + """ + + id: str + observations: list[ObservationV2] + scores: list[ScoreV3] | None = None + + @property + def root(self) -> ObservationV2: + return get_root_observation(self.observations) + + def _latest(self, field: str) -> Any: + values = [ + getattr(observation, field) + for observation in self.observations + if getattr(observation, field) + ] + return values[-1] if values else None + + @property + def name(self) -> str | None: + # The API falls back to the root's own name when no trace name was + # set, so a root whose trace_name equals its name carries no signal. + explicit_names = [ + observation.trace_name + for observation in self.observations + if observation.trace_name + and not ( + observation.is_root_observation + and observation.trace_name == observation.name + ) + ] + return explicit_names[-1] if explicit_names else self.root.trace_name + + @property + def user_id(self) -> str | None: + return self._latest("user_id") + + @property + def session_id(self) -> str | None: + return self._latest("session_id") + + @property + def tags(self) -> list[str]: + return sorted( + {tag for observation in self.observations for tag in observation.tags or []} + ) + + @property + def public(self) -> bool: + return any(observation.public for observation in self.observations) + + @property + def version(self) -> str | None: + return self._latest("version") + + @property + def release(self) -> str | None: + return self._latest("release") + + @property + def environment(self) -> str | None: + return self._latest("environment") + + @property + def input(self) -> Any: + return self.root.input + + @property + def output(self) -> Any: + return self.root.output + + @property + def metadata(self) -> dict[str, Any]: + return user_metadata(self.root) + + +def wait_for_trace_snapshot( trace_id: str, *, - is_result_ready: Callable[[Any], bool] | None = None, + min_observations: int = 1, + min_scores: int | None = None, + is_result_ready: Callable[[TraceSnapshot], bool] | None = None, timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, -): - api = get_api(retry=False) +) -> TraceSnapshot: + """Poll until the trace has `min_observations` observations (and + `min_scores` scores, which are only fetched when given).""" + + def _fetch() -> TraceSnapshot: + return TraceSnapshot( + id=trace_id, + observations=get_observations(trace_id=trace_id), + scores=None if min_scores is None else get_scores(trace_id=trace_id), + ) + + def _ready(snapshot: TraceSnapshot) -> bool: + if len(snapshot.observations) < min_observations: + return False + if min_scores is not None and len(snapshot.scores or []) < min_scores: + return False + if is_result_ready is None: + return True + try: + return is_result_ready(snapshot) + except AssertionError: + # Root-derived attributes are unavailable until the root arrives. + return False + return wait_for_result( - lambda: api.trace.get(trace_id), - is_result_ready=is_result_ready, + _fetch, + is_result_ready=_ready, + timeout_seconds=timeout_seconds, + interval_seconds=interval_seconds, + ) + + +def get_scores( + *, fields: str = SCORE_FIELDS, api: Any = None, **filters: Any +) -> list[ScoreV3]: + api = api or get_api(retry=False) + scores: list[ScoreV3] = [] + cursor = None + + while True: + response = api.scores.get_many( + fields=fields, limit=100, cursor=cursor, **filters + ) + scores.extend(response.data) + cursor = response.meta.cursor + if not cursor or not response.data: + break + + return scores + + +def wait_for_scores( + *, + min_count: int = 1, + is_result_ready: Callable[[list[ScoreV3]], bool] | None = None, + fields: str = SCORE_FIELDS, + timeout_seconds: float = DEFAULT_RETRY_TIMEOUT_SECONDS, + interval_seconds: float = DEFAULT_RETRY_INTERVAL_SECONDS, + **filters: Any, +) -> list[ScoreV3]: + """Poll the v3 scores API (filters: trace_id, session_id, observation_id, + name, ...) until at least `min_count` scores match.""" + assert filters, "wait_for_scores needs at least one filter" + + def _ready(scores: list[ScoreV3]) -> bool: + return len(scores) >= min_count and ( + is_result_ready is None or is_result_ready(scores) + ) + + return wait_for_result( + lambda: get_scores(fields=fields, **filters), + is_result_ready=_ready, timeout_seconds=timeout_seconds, interval_seconds=interval_seconds, ) diff --git a/tests/unit/test_additional_headers_simple.py b/tests/unit/test_additional_headers_simple.py index 7837c5c3e..d05475a3c 100644 --- a/tests/unit/test_additional_headers_simple.py +++ b/tests/unit/test_additional_headers_simple.py @@ -210,6 +210,41 @@ def test_span_processor_none_additional_headers_works(self): assert "authorization" in exporter._client._headers assert "x-langfuse-sdk-name" in exporter._client._headers assert "x-langfuse-public-key" in exporter._client._headers + assert exporter._client._headers["x-langfuse-ingestion-version"] == "4" + + def test_span_processor_additional_headers_override_ingestion_version(self): + """Test that additional headers can override the default ingestion version.""" + from langfuse._client.span_processor import LangfuseSpanProcessor + + processor = LangfuseSpanProcessor( + public_key="test-public-key", + secret_key="test-secret-key", + base_url="https://mock-host.com", + additional_headers={"x-langfuse-ingestion-version": "3"}, + ) + + exporter = processor.span_exporter + + assert exporter._client._headers["x-langfuse-ingestion-version"] == "3" + + def test_span_processor_mixed_case_override_replaces_ingestion_version(self): + """Test that a differently cased override replaces the default header.""" + from langfuse._client.span_processor import LangfuseSpanProcessor + + processor = LangfuseSpanProcessor( + public_key="test-public-key", + secret_key="test-secret-key", + base_url="https://mock-host.com", + additional_headers={"X-Langfuse-Ingestion-Version": "3"}, + ) + + ingestion_headers = { + key: value + for key, value in processor.span_exporter._client._headers.items() + if key.lower() == "x-langfuse-ingestion-version" + } + + assert ingestion_headers == {"x-langfuse-ingestion-version": "3"} def test_span_processor_uses_custom_span_exporter_when_provided(self): """Test that a custom exporter bypasses the default OTLP exporter construction.""" diff --git a/tests/unit/test_api_metadata_parsing.py b/tests/unit/test_api_metadata_parsing.py index 6be15dfba..2c8fe8eb9 100644 --- a/tests/unit/test_api_metadata_parsing.py +++ b/tests/unit/test_api_metadata_parsing.py @@ -10,16 +10,7 @@ import pydantic import pytest -from langfuse.api import ( - BaseScore, - BaseScoreV1, - GetScoresResponseData, - Observation, - ObservationV2, - Score, - ScoreV1, - Trace, -) +from langfuse.api import BaseScore, ObservationV2, Score from langfuse.api.core import parse_obj_as TIMESTAMP = "2026-01-01T00:00:00.000Z" @@ -33,7 +24,6 @@ "updatedAt": TIMESTAMP, "environment": "default", } -SCORE_V1_BASE = {**SCORE_BASE, "traceId": "trace-1"} SCORE_VARIANTS: typing.Dict[str, typing.Dict[str, typing.Any]] = { "NUMERIC": {"value": 0.5}, @@ -42,7 +32,6 @@ "CORRECTION": {"value": 0, "stringValue": "corrected output"}, "TEXT": {"stringValue": "free text"}, } -SCORE_V1_DATA_TYPES = ["NUMERIC", "CATEGORICAL", "BOOLEAN", "TEXT"] def _score_payload( @@ -52,34 +41,6 @@ def _score_payload( CASES: typing.List[typing.Any] = [ - pytest.param( - Trace, - { - "id": "trace-1", - "timestamp": TIMESTAMP, - "tags": [], - "public": False, - "environment": "default", - }, - id="Trace", - ), - pytest.param( - Observation, - { - "id": "obs-1", - "type": "SPAN", - "startTime": TIMESTAMP, - "modelParameters": {}, - "input": None, - "output": None, - "usage": {"input": 0, "output": 0, "total": 0}, - "level": "DEFAULT", - "usageDetails": {}, - "costDetails": {}, - "environment": "default", - }, - id="Observation", - ), pytest.param( ObservationV2, { @@ -91,23 +52,10 @@ def _score_payload( id="ObservationV2", ), pytest.param(BaseScore, SCORE_BASE, id="BaseScore"), - pytest.param(BaseScoreV1, SCORE_V1_BASE, id="BaseScoreV1"), *[ pytest.param(Score, _score_payload(SCORE_BASE, dt), id=f"Score-{dt}") for dt in SCORE_VARIANTS ], - *[ - pytest.param( - GetScoresResponseData, - _score_payload(SCORE_BASE, dt), - id=f"GetScoresResponseData-{dt}", - ) - for dt in SCORE_VARIANTS - ], - *[ - pytest.param(ScoreV1, _score_payload(SCORE_V1_BASE, dt), id=f"ScoreV1-{dt}") - for dt in SCORE_V1_DATA_TYPES - ], ] NESTED_METADATA = { diff --git a/tests/unit/test_app_root_detection.py b/tests/unit/test_app_root_detection.py index 7fa346d1e..fdf19639a 100644 --- a/tests/unit/test_app_root_detection.py +++ b/tests/unit/test_app_root_detection.py @@ -252,12 +252,15 @@ def test_active_langfuse_scope_sets_baggage_after_root_start( assert "langfuse.trace.metadata.trace_id" not in spans["child"].attributes -def test_blocked_instrumentation_scope_parent_marks_child_as_app_root( +def test_filtered_scope_parent_marks_child_as_app_root( memory_exporter, ): tracer_provider, processor = _create_processor( memory_exporter, - blocked_instrumentation_scopes=["blocked.scope"], + should_export_span=lambda span: ( + span.instrumentation_scope is None + or span.instrumentation_scope.name != "blocked.scope" + ), ) blocked_tracer = tracer_provider.get_tracer("blocked.scope") langfuse_tracer = _langfuse_tracer(tracer_provider) diff --git a/tests/unit/test_batch_evaluation.py b/tests/unit/test_batch_evaluation.py new file mode 100644 index 000000000..f6c29f4f6 --- /dev/null +++ b/tests/unit/test_batch_evaluation.py @@ -0,0 +1,412 @@ +"""Unit tests for batch evaluation on the v2 observations API.""" + +import inspect +import json +from datetime import datetime, timedelta, timezone +from types import SimpleNamespace +from typing import Any, Dict, List, Optional +from unittest.mock import MagicMock + +import pytest + +from langfuse import Langfuse +from langfuse.api import ObservationsV2Response, ObservationV2 +from langfuse.batch_evaluation import ( + DEFAULT_BATCH_EVALUATION_FIELDS, + BatchEvaluationResumeToken, + BatchEvaluationRunner, + EvaluatorInputs, +) +from langfuse.experiment import Evaluation + +BASE_TIME = datetime(2026, 1, 1, tzinfo=timezone.utc) + + +def make_observation( + index: int, + *, + trace_id: Optional[str] = "default", + is_root: bool = False, + start_time: Optional[datetime] = None, + observation_id: Optional[str] = None, + parent_observation_id: Optional[str] = "default", +) -> ObservationV2: + return ObservationV2( + id=observation_id or f"obs-{index}", + trace_id=f"trace-{index}" if trace_id == "default" else trace_id, + start_time=start_time or BASE_TIME + timedelta(seconds=index), + end_time=None, + project_id="project", + parent_observation_id=( + (None if is_root else f"parent-{index}") + if parent_observation_id == "default" + else parent_observation_id + ), + type="SPAN", + is_root_observation=is_root, + input=json.dumps({"question": index}), + output=f"answer {index}", + ) + + +class FakeObservationsApi: + """Serves observations newest first with an index cursor, like the v2 API.""" + + def __init__(self, observations: List[ObservationV2]): + self.observations = sorted( + observations, key=lambda o: (o.start_time, o.id), reverse=True + ) + self.calls: List[Dict[str, Any]] = [] + self.fail_on_call: Optional[int] = None + + def get_many(self, **kwargs: Any) -> ObservationsV2Response: + self.calls.append(kwargs) + if self.fail_on_call is not None and len(self.calls) == self.fail_on_call: + raise ConnectionError("fetch failed") + + matching = [o for o in self.observations if self._matches(o, kwargs["filter"])] + start = int(kwargs["cursor"]) if kwargs["cursor"] else 0 + page = matching[start : start + kwargs["limit"]] + has_more = start + kwargs["limit"] < len(matching) + meta = {"cursor": str(start + kwargs["limit"])} if has_more else {} + return ObservationsV2Response(data=page, meta=meta) + + @staticmethod + def _matches(observation: ObservationV2, filter_json: Optional[str]) -> bool: + for condition in json.loads(filter_json) if filter_json else []: + if condition["column"] == "isRootObservation": + if observation.is_root_observation is not condition["value"]: + return False + elif condition["column"] == "startTime": + assert condition["operator"] == "<=" + if observation.start_time > datetime.fromisoformat(condition["value"]): + return False + return True + + +def make_runner(observations: List[ObservationV2]): + api = FakeObservationsApi(observations) + client = SimpleNamespace( + api=SimpleNamespace(observations=api), + create_score=MagicMock(), + flush=MagicMock(), + ) + return BatchEvaluationRunner(client), api, client # type: ignore[arg-type] + + +def mapper(*, item: ObservationV2) -> EvaluatorInputs: + return EvaluatorInputs(input=item.input, output=item.output, metadata={}) + + +def length_evaluator(*, input, output, **kwargs): + return Evaluation(name="length", value=len(output)) + + +async def run(runner: BatchEvaluationRunner, **kwargs: Any): + kwargs.setdefault("mapper", mapper) + kwargs.setdefault("evaluators", [length_evaluator]) + return await runner.run_async(**kwargs) + + +@pytest.mark.asyncio +async def test_paginates_with_cursor_and_scores_observations(): + runner, api, client = make_runner([make_observation(i) for i in range(5)]) + + result = await run(runner, fetch_batch_size=2) + + assert [call["cursor"] for call in api.calls] == [None, "2", "4"] + assert all("page" not in call for call in api.calls) + assert all( + call["fields"] == f"{DEFAULT_BATCH_EVALUATION_FIELDS},time" + and call["filter"] is None + for call in api.calls + ) + assert result.completed is True + assert result.has_more_items is False + assert result.resume_token is None + assert result.total_items_fetched == 5 + assert result.total_items_processed == 5 + assert result.total_scores_created == 5 + assert set(result.item_evaluations) == {f"obs-{i}" for i in range(5)} + client.create_score.assert_any_call( + observation_id="obs-3", + trace_id="trace-3", + name="length", + value=len("answer 3"), + comment=None, + metadata={}, + data_type=None, + config_id=None, + ) + client.flush.assert_called_once() + + +@pytest.mark.asyncio +async def test_mapper_receives_raw_string_io_and_requested_fields(): + runner, api, _ = make_runner([make_observation(1)]) + seen: List[ObservationV2] = [] + + def recording_mapper(*, item): + seen.append(item) + return mapper(item=item) + + await run(runner, mapper=recording_mapper, fields="core,io") + + assert api.calls[0]["fields"] == "core,io,time" + assert isinstance(seen[0], ObservationV2) + assert seen[0].input == '{"question": 1}' + + +@pytest.mark.asyncio +@pytest.mark.parametrize("fields", [None, ""]) +async def test_unset_fields_request_the_api_defaults_plus_time(fields): + runner, api, _ = make_runner([make_observation(1)]) + + await run(runner, fields=fields) + + assert api.calls[0]["fields"] == "core,basic,time" + + +def test_client_and_runner_share_default_fields(): + client_default = ( + inspect.signature(Langfuse.run_batched_evaluation).parameters["fields"].default + ) + runner_default = ( + inspect.signature(BatchEvaluationRunner.run_async).parameters["fields"].default + ) + + assert client_default == runner_default == DEFAULT_BATCH_EVALUATION_FIELDS + assert "io" in DEFAULT_BATCH_EVALUATION_FIELDS.split(",") + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("kwargs", "message"), + [ + ({"filter": "not json"}, "JSON array"), + ({"filter": '{"tags": ["a"]}'}, "JSON array"), + ], +) +async def test_invalid_arguments_raise_before_fetching(kwargs, message): + runner, api, _ = make_runner([make_observation(1)]) + + with pytest.raises(ValueError, match=message): + await run(runner, **kwargs) + + assert api.calls == [] + + +@pytest.mark.asyncio +async def test_max_items_aligns_pages_and_resume_continues_without_gaps(): + # Shared start times exercise ties that a timestamp-only resume would skip. + observations = [ + make_observation(i, start_time=BASE_TIME + timedelta(seconds=i // 3)) + for i in range(10) + ] + runner, api, _ = make_runner(observations) + + first = await run(runner, max_items=5, fetch_batch_size=3) + + assert [call["limit"] for call in api.calls] == [3, 2] + assert first.completed is True + assert first.has_more_items is True + assert first.total_items_fetched == 5 + token = first.resume_token + assert token is not None + assert token.cursor == "5" + assert token.items_processed == 5 + + second = await run(runner, resume_from=token, fetch_batch_size=3) + + assert second.resume_token is None + assert second.has_more_items is False + assert set(first.item_evaluations).isdisjoint(second.item_evaluations) + assert set(first.item_evaluations) | set(second.item_evaluations) == { + f"obs-{i}" for i in range(10) + } + + +@pytest.mark.asyncio +async def test_max_items_reached_on_last_page_has_no_resume_token(): + runner, _, _ = make_runner([make_observation(i) for i in range(4)]) + + result = await run(runner, max_items=4, fetch_batch_size=2) + + assert result.has_more_items is False + assert result.resume_token is None + + +@pytest.mark.asyncio +async def test_fetch_failure_returns_resume_token_for_failed_page(): + runner, api, _ = make_runner([make_observation(i) for i in range(6)]) + api.fail_on_call = 2 + + failed = await run(runner, fetch_batch_size=2, max_retries=0) + + assert failed.completed is False + assert failed.total_items_processed == 2 + token = failed.resume_token + assert token is not None + assert token.cursor == "2" + assert token.last_processed_id == "obs-4" + + api.fail_on_call = None + resumed = await run(runner, fetch_batch_size=2, resume_from=token) + + assert resumed.completed is True + assert set(resumed.item_evaluations) == {"obs-3", "obs-2", "obs-1", "obs-0"} + assert api.calls[-1]["request_options"] == {"max_retries": 3} + + +@pytest.mark.asyncio +async def test_resume_without_cursor_falls_back_to_start_time_bound(): + tied_time = BASE_TIME + timedelta(seconds=3) + runner, api, _ = make_runner( + [make_observation(i) for i in range(5)] + + [make_observation(9, observation_id="obs-tied", start_time=tied_time)] + ) + token = BatchEvaluationResumeToken( + filter=None, + last_processed_timestamp=(BASE_TIME + timedelta(seconds=3)).isoformat(), + last_processed_id="obs-3", + items_processed=2, + ) + + result = await run(runner, resume_from=token) + + assert json.loads(api.calls[0]["filter"]) == [ + { + "type": "datetime", + "column": "startTime", + "operator": "<=", + "value": token.last_processed_timestamp, + } + ] + assert api.calls[0]["cursor"] is None + assert set(result.item_evaluations) == {"obs-tied", "obs-2", "obs-1", "obs-0"} + + +@pytest.mark.asyncio +async def test_resume_reuses_the_token_filter_and_rejects_a_different_one(): + runner, api, _ = make_runner([make_observation(i) for i in range(4)]) + token_filter = json.dumps( + [{"type": "string", "column": "name", "operator": "=", "value": "x"}] + ) + token = BatchEvaluationResumeToken( + filter=token_filter, + cursor="2", + last_processed_timestamp=BASE_TIME.isoformat(), + last_processed_id="obs-2", + items_processed=2, + ) + api._matches = lambda observation, filter_json: True # type: ignore[method-assign] + + await run(runner, resume_from=token) + assert json.loads(api.calls[0]["filter"]) == json.loads(token_filter) + + with pytest.raises(ValueError, match="different filter"): + await run(runner, resume_from=token, filter="[]") + + +@pytest.mark.asyncio +async def test_observation_scores_can_be_duplicated_onto_trace(): + runner, _, client = make_runner([make_observation(1)]) + + result = await run(runner, _add_observation_scores_to_trace=True) + + assert result.total_scores_created == 2 + targets = [ + (call.kwargs.get("observation_id"), call.kwargs["trace_id"]) + for call in client.create_score.call_args_list + ] + assert targets == [("obs-1", "trace-1"), (None, "trace-1")] + + +@pytest.mark.asyncio +async def test_item_failures_and_evaluator_failures_are_tracked(): + observations = [make_observation(1), make_observation(2, trace_id=None)] + runner, _, client = make_runner(observations) + + def failing_evaluator(**kwargs): + raise RuntimeError("boom") + + def composite(*, evaluations, **kwargs): + return Evaluation(name="composite", value=len(evaluations)) + + result = await run( + runner, + evaluators=[length_evaluator, failing_evaluator], + composite_evaluator=composite, + ) + + assert result.total_items_processed == 1 + assert result.failed_item_ids == ["obs-2"] + assert result.error_summary == {"ValueError": 1} + assert result.total_evaluations_failed == 1 + assert result.total_composite_scores_created == 1 + assert [e.name for e in result.item_evaluations["obs-1"]] == [ + "length", + "composite", + ] + stats = {s.name: s for s in result.evaluator_stats} + assert stats["failing_evaluator"].failed_runs == 1 + assert stats["length_evaluator"].successful_runs == 1 + assert client.create_score.call_count == 2 + + +@pytest.mark.asyncio +async def test_async_mapper_and_evaluator_are_awaited(): + runner, _, _ = make_runner([make_observation(1)]) + + async def async_mapper(*, item): + return mapper(item=item) + + async def async_evaluator(*, output, **kwargs): + return [Evaluation(name="a", value=1), Evaluation(name="b", value=2)] + + result = await run(runner, mapper=async_mapper, evaluators=[async_evaluator]) + + assert result.total_scores_created == 2 + + +@pytest.mark.asyncio +async def test_duplicate_rows_for_one_observation_are_evaluated_once(): + # The events table can briefly return several rows for the same observation. + stale = make_observation(1) + fresh = make_observation(1).model_copy( + update={"output": "fresh answer", "updated_at": BASE_TIME + timedelta(hours=1)} + ) + runner, _, client = make_runner([make_observation(0), stale, fresh]) + seen: List[ObservationV2] = [] + + def recording_mapper(*, item): + seen.append(item) + return mapper(item=item) + + result = await run(runner, mapper=recording_mapper, fetch_batch_size=2) + + assert sorted(item.id for item in seen) == ["obs-0", "obs-1"] + assert [item.output for item in seen if item.id == "obs-1"] == ["fresh answer"] + assert result.total_items_processed == 2 + assert client.create_score.call_count == 2 + + +@pytest.mark.asyncio +async def test_resume_skips_duplicate_rows_split_across_the_page_boundary(): + duplicate = make_observation(2).model_copy( + update={"updated_at": BASE_TIME + timedelta(hours=1)} + ) + runner, _, client = make_runner( + [make_observation(i) for i in range(5)] + [duplicate] + ) + + first = await run(runner, max_items=3, fetch_batch_size=3) + assert first.resume_token is not None + assert first.resume_token.last_page_ids == ["obs-2", "obs-3", "obs-4"] + + await run(runner, resume_from=first.resume_token, fetch_batch_size=3) + + scored_ids = [ + call.kwargs["observation_id"] for call in client.create_score.call_args_list + ] + assert sorted(scored_ids) == [f"obs-{i}" for i in range(5)] diff --git a/tests/unit/test_e2e_support.py b/tests/unit/test_e2e_support.py index 8320bd2fe..b27e5aa0d 100644 --- a/tests/unit/test_e2e_support.py +++ b/tests/unit/test_e2e_support.py @@ -1,144 +1,260 @@ +from datetime import datetime, timedelta, timezone from types import SimpleNamespace +from langfuse.api import ObservationV2 from langfuse.api.commons.errors.not_found_error import NotFoundError -from tests.support.api_wrapper import LangfuseAPI as SupportLangfuseAPI from tests.support.retry import retry_until_ready -from tests.support.utils import get_api, wait_for_trace +from tests.support.utils import ( + TraceSnapshot, + get_api, + get_observations, + normalize_observation, + user_metadata, + wait_for_observations, + wait_for_scores, + wait_for_trace_snapshot, +) +START = datetime(2024, 1, 1, tzinfo=timezone.utc) -def test_get_api_retries_not_found(monkeypatch): - monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) - attempts = {"count": 0} +def _observation(index: int = 0, **fields) -> ObservationV2: + defaults = { + "id": f"obs-{index}", + "trace_id": "trace-123", + "start_time": START + timedelta(seconds=index), + "project_id": "project", + "type": "SPAN", + "is_root_observation": False, + } + return ObservationV2(**{**defaults, **fields}) - class FakeTraceService: - def get(self, trace_id): - attempts["count"] += 1 - if attempts["count"] < 3: - raise NotFoundError( - body={ - "error": "LangfuseNotFoundError", - "message": f"Trace {trace_id} not found within authorized project", - } - ) +def _page(data, cursor=None): + return SimpleNamespace(data=data, meta=SimpleNamespace(cursor=cursor)) - return {"id": trace_id} - class FakeClient: - trace = FakeTraceService() +def _install_client(monkeypatch, **services): + monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + client = SimpleNamespace(**services) + monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: client) - monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) - trace = get_api().trace.get("trace-123") +def test_get_api_retries_not_found(monkeypatch): + attempts = {"count": 0} - assert trace == {"id": "trace-123"} - assert attempts["count"] == 3 + def get_many(**kwargs): + attempts["count"] += 1 + if attempts["count"] < 3: + raise NotFoundError( + body={ + "error": "LangfuseNotFoundError", + "message": "Observations not found within authorized project", + } + ) -def test_get_api_retries_filtered_lists(monkeypatch): - monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) + return _page([kwargs["trace_id"]]) - attempts = {"count": 0} + _install_client(monkeypatch, observations=SimpleNamespace(get_many=get_many)) - class FakeTraceService: - def list(self, **kwargs): - attempts["count"] += 1 + response = get_api().observations.get_many(trace_id="trace-123") - if attempts["count"] < 3: - return SimpleNamespace(data=[]) + assert response.data == ["trace-123"] + assert attempts["count"] == 3 - return SimpleNamespace(data=[kwargs["name"]]) - class FakeClient: - trace = FakeTraceService() +def test_get_api_retries_filtered_lists(monkeypatch): + attempts = {"count": 0} - monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) + def get_many(**kwargs): + attempts["count"] += 1 + return _page([] if attempts["count"] < 3 else [kwargs["name"]]) - response = get_api().trace.list(name="ready-trace") + _install_client(monkeypatch, observations=SimpleNamespace(get_many=get_many)) - assert response.data == ["ready-trace"] + response = get_api().observations.get_many(name="ready-observation") + + assert response.data == ["ready-observation"] assert attempts["count"] == 3 def test_get_api_retry_can_be_disabled(monkeypatch): attempts = {"count": 0} - class FakeTraceService: - def list(self, **kwargs): - attempts["count"] += 1 - return SimpleNamespace(data=[]) - - class FakeClient: - trace = FakeTraceService() + def get_many(**kwargs): + attempts["count"] += 1 + return _page([]) - monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) + _install_client(monkeypatch, observations=SimpleNamespace(get_many=get_many)) - response = get_api(retry=False).trace.list(name="missing-trace") + response = get_api(retry=False).observations.get_many(name="missing") assert response.data == [] assert attempts["count"] == 1 -def test_raw_api_wrapper_retries_not_found_payload(monkeypatch): - monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) +def test_normalize_observation_parses_io_and_maps_empty_strings_to_none(): + observation = normalize_observation( + _observation( + input='{"question": "hi"}', + output="plain text", + name="", + session_id="", + user_id="user-1", + ) + ) - attempts = {"count": 0} + assert observation.input == {"question": "hi"} + assert observation.output == "plain text" + assert observation.name is None + assert observation.session_id is None + assert observation.user_id == "user-1" - class FakeResponse: - def __init__(self, status_code, payload): - self.status_code = status_code - self._payload = payload - self.headers = {} - def json(self): - return self._payload +def test_user_metadata_drops_server_added_keys(): + observation = _observation( + metadata={ + "key": "value", + "scope.name": "langfuse-sdk", + "resourceAttributes.service.name": "test", + } + ) - def fake_get(*args, **kwargs): - attempts["count"] += 1 + assert user_metadata(observation) == {"key": "value"} - if attempts["count"] < 3: - return FakeResponse( - 404, - { - "error": "LangfuseNotFoundError", - "message": "Trace trace-123 not found within authorized project", - }, - ) - return FakeResponse(200, {"id": "trace-123", "observations": []}) +def test_get_observations_follows_cursor_and_sorts_by_start_time(monkeypatch): + calls = [] + pages = { + None: _page([_observation(2), _observation(0)], cursor="next"), + "next": _page([_observation(1)]), + } - monkeypatch.setattr("tests.support.api_wrapper.httpx.get", fake_get) + def get_many(**kwargs): + calls.append(kwargs) + return pages[kwargs["cursor"]] - api = SupportLangfuseAPI(username="user", password="pass", base_url="http://test") - trace = api.get_trace("trace-123") + _install_client(monkeypatch, observations=SimpleNamespace(get_many=get_many)) - assert trace["id"] == "trace-123" - assert attempts["count"] == 3 + observations = get_observations(trace_id="trace-123") + assert [o.id for o in observations] == ["obs-0", "obs-1", "obs-2"] + assert [call["cursor"] for call in calls] == [None, "next"] + assert all(call["trace_id"] == "trace-123" for call in calls) -def test_wait_for_trace_retries_until_predicate_matches(monkeypatch): - monkeypatch.setattr("tests.support.retry.sleep", lambda _: None) +def test_wait_for_observations_polls_until_min_count(monkeypatch): attempts = {"count": 0} - class FakeTraceService: - def get(self, trace_id): - attempts["count"] += 1 - return {"id": trace_id, "observations": [1] * attempts["count"]} + def get_many(**kwargs): + attempts["count"] += 1 + return _page([_observation(i) for i in range(attempts["count"])]) + + _install_client(monkeypatch, observations=SimpleNamespace(get_many=get_many)) - class FakeClient: - trace = FakeTraceService() + observations = wait_for_observations("trace-123", min_count=3) + + assert len(observations) == 3 + assert attempts["count"] == 3 - monkeypatch.setattr("tests.support.utils.LangfuseAPI", lambda **_: FakeClient()) - trace = wait_for_trace( - "trace-123", is_result_ready=lambda trace: len(trace["observations"]) == 3 +def test_wait_for_trace_snapshot_waits_for_root_and_scores(monkeypatch): + attempts = {"observations": 0, "scores": 0} + + def get_many(**kwargs): + attempts["observations"] += 1 + observations = [_observation(1, name="child")] + if attempts["observations"] >= 2: + observations.append(_observation(0, name="root", is_root_observation=True)) + return _page(observations) + + def get_many_v3(**kwargs): + attempts["scores"] += 1 + return _page(["score"] if attempts["scores"] >= 3 else []) + + _install_client( + monkeypatch, + observations=SimpleNamespace(get_many=get_many), + scores=SimpleNamespace(get_many=get_many_v3), ) - assert trace["id"] == "trace-123" - assert len(trace["observations"]) == 3 - assert attempts["count"] == 3 + snapshot = wait_for_trace_snapshot( + "trace-123", + min_scores=1, + is_result_ready=lambda trace: trace.root.name == "root", + ) + + assert snapshot.root.name == "root" + assert snapshot.scores == ["score"] + assert attempts["scores"] == 3 + + +def test_trace_snapshot_aggregates_trace_attributes_like_the_platform(): + snapshot = TraceSnapshot( + id="trace-123", + observations=[ + _observation( + 0, + name="root", + trace_name="root", + is_root_observation=True, + input={"q": 1}, + metadata={"root_key": "root", "scope.name": "sdk"}, + tags=["b"], + ), + _observation( + 1, + name="child", + trace_name="explicit-name", + session_id="session-1", + user_id="user-1", + tags=["a"], + public=True, + ), + _observation(2, name="grandchild", session_id="session-2"), + ], + ) + + assert snapshot.name == "explicit-name" + assert snapshot.session_id == "session-2" + assert snapshot.user_id == "user-1" + assert snapshot.tags == ["a", "b"] + assert snapshot.public is True + assert snapshot.input == {"q": 1} + assert snapshot.metadata == {"root_key": "root"} + + +def test_trace_snapshot_name_falls_back_to_root_trace_name(): + snapshot = TraceSnapshot( + id="trace-123", + observations=[ + _observation(0, name="root", trace_name="root", is_root_observation=True), + _observation(1, name="child", trace_name="root"), + ], + ) + + assert snapshot.name == "root" + assert snapshot.session_id is None + assert snapshot.public is False + + +def test_wait_for_scores_follows_cursor_and_polls(monkeypatch): + attempts = {"count": 0} + + def get_many_v3(**kwargs): + attempts["count"] += 1 + if attempts["count"] < 2: + return _page([]) + if kwargs["cursor"] is None: + return _page(["score-1"], cursor="next") + return _page(["score-2"]) + + _install_client(monkeypatch, scores=SimpleNamespace(get_many=get_many_v3)) + + scores = wait_for_scores(min_count=2, trace_id="trace-123") + + assert scores == ["score-1", "score-2"] def test_retry_until_ready_clears_stale_error_after_success(monkeypatch): @@ -171,3 +287,37 @@ def operation(): assert trace["id"] == "trace-123" assert trace["attempt"] == 3 + + +def test_normalize_observation_keeps_scalar_text_io_as_strings(): + observation = normalize_observation( + ObservationV2( + id="obs", + trace_id="trace", + start_time=datetime(2026, 1, 1, tzinfo=timezone.utc), + project_id="project", + type="SPAN", + input="2", + output="true", + ) + ) + + assert observation.input == "2" + assert observation.output == "true" + + +def test_get_observations_keeps_the_latest_row_per_observation_id(monkeypatch): + stale = _observation(0, name="stale", updated_at=START) + fresh = _observation(0, name="fresh", updated_at=START + timedelta(seconds=1)) + + def get_many(**kwargs): + return _page([fresh, stale, _observation(1)]) + + _install_client(monkeypatch, observations=SimpleNamespace(get_many=get_many)) + + observations = get_observations(trace_id="trace-123") + + assert [(o.id, o.name) for o in observations] == [ + ("obs-0", "fresh"), + ("obs-1", None), + ] diff --git a/tests/unit/test_error_parsing.py b/tests/unit/test_error_parsing.py index 36a7decbf..d71678873 100644 --- a/tests/unit/test_error_parsing.py +++ b/tests/unit/test_error_parsing.py @@ -1,10 +1,6 @@ """@private""" -from langfuse._utils.parse_error import ( - generate_error_message, - generate_error_message_fern, -) -from langfuse._utils.request import APIError, APIErrors +from langfuse._utils.parse_error import generate_error_message_fern from langfuse.api import ( AccessDeniedError, MethodNotAllowedError, @@ -15,32 +11,6 @@ from langfuse.api.core import ApiError -def test_generate_error_message_api_error(): - exception = APIError(message="Test API error", status="500") - expected_message = "API error occurred: Internal server error occurred. For help, please contact support: https://langfuse.com/support" - assert expected_message in generate_error_message(exception) - - -def test_generate_error_message_api_errors(): - errors = [ - APIError(status=400, message="Bad request", details="Invalid input"), - APIError(status=401, message="Unauthorized", details="Invalid credentials"), - ] - exception = APIErrors(errors) - expected_message = ( - "API errors occurred: " - "Bad request. Please check your request for any missing or incorrect parameters. Refer to our API docs: https://api.reference.langfuse.com for details.\n" - "Unauthorized. Please check your public/private host settings. Refer to our installation and setup guide: https://langfuse.com/docs/sdk/typescript/guide for details on SDK configuration." - ) - assert expected_message in generate_error_message(exception) - - -def test_generate_error_message_generic_exception(): - exception = Exception("Generic error") - expected_message = "Unexpected error occurred. Please check your request and contact support: https://langfuse.com/support." - assert generate_error_message(exception) == expected_message - - def test_generate_error_message_access_denied_error(): exception = AccessDeniedError(body={}) expected_message = "Forbidden. Please check your access control settings. Refer to our RBAC docs: https://langfuse.com/docs/rbac for details." diff --git a/tests/unit/test_experiment.py b/tests/unit/test_experiment.py index 3eb55ad40..fea3587b2 100644 --- a/tests/unit/test_experiment.py +++ b/tests/unit/test_experiment.py @@ -1,8 +1,9 @@ """Tests for ``langfuse.experiment`` — ``RunnerContext`` and ``RegressionError``.""" import inspect +import json import typing -from datetime import datetime +from datetime import datetime, timezone from typing import get_type_hints from unittest.mock import MagicMock @@ -12,7 +13,9 @@ from langfuse import Evaluation, RegressionError, RunnerContext from langfuse._client.attributes import LangfuseOtelSpanAttributes from langfuse._client.client import Langfuse +from langfuse.api import DatasetItem, DatasetStatus from langfuse.batch_evaluation import CompositeEvaluatorFunction +from langfuse.experiment import ExperimentItemResult, ExperimentResult def _noop_task(*, item, **kwargs): # pragma: no cover - never invoked via mock @@ -273,16 +276,12 @@ def failing_task(**kwargs): item_run.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_NAME] == result.run_name ) - assert ( - item_run.attributes[f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.run"] - == "metadata" - ) - assert ( - item_run.attributes[ - f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.item" - ] - == "metadata" - ) + assert json.loads( + item_run.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_METADATA] + ) == {"run": "metadata"} + assert json.loads( + item_run.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA] + ) == {"item": "metadata"} assert ( item_run.attributes[ LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID @@ -524,3 +523,259 @@ def failing_evaluator(**kwargs): "failed_evaluator_count": 1, "skipped_evaluator_count": 1, } + + +def _dataset_item(*, item_id: str, dataset_id: str, input: str) -> DatasetItem: + return DatasetItem( + id=item_id, + status=DatasetStatus.ACTIVE, + input=input, + expected_output=f"expected {input}", + metadata=None, + source_trace_id=None, + source_observation_id=None, + dataset_id=dataset_id, + dataset_name="dataset", + media_references=[], + created_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + updated_at=datetime(2026, 1, 1, tzinfo=timezone.utc), + ) + + +def _run_scores(create_score: MagicMock) -> list: + return [ + call.kwargs + for call in create_score.call_args_list + if call.kwargs.get("dataset_run_id") is not None + ] + + +class TestExperimentRunIdentity: + @pytest.mark.parametrize( + ("project_id", "dataset_id", "run_name", "expected"), + [ + ("p", "d", "r", "eab007015b1c6f77"), + ("proj-ü", "ds", 'Run – ✓ "q"', "8d4425c67052ced3"), + # Shared with the JS SDK and the platform; keep these in sync. + ( + "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a", + "cm9x1dataset0000000000001", + "my-run", + "a1164c0f238e4173", + ), + ( + "7a88fb47-b4e2-43b8-a06c-a5ce950dc53a", + "cm9x1dataset0000000000001", + 'Läufe "v2" 🚀 – 2026-10-06T12:00:00.000Z', + "dce21641128b88a8", + ), + ], + ) + def test_dataset_experiment_id_matches_server_derivation( + self, langfuse_memory_client, project_id, dataset_id, run_name, expected + ): + assert ( + langfuse_memory_client._create_experiment_id( + project_id=project_id, dataset_id=dataset_id, run_name=run_name + ) + == expected + ) + + def test_dataset_experiment_without_project_id_uses_random_id( + self, langfuse_memory_client, caplog + ): + ids = { + langfuse_memory_client._create_experiment_id( + project_id=None, dataset_id="d", run_name="r" + ) + for _ in range(2) + } + + assert len(ids) == 2 + assert all(len(i) == 16 for i in ids) + assert "random experiment id" in caplog.text + + def test_dataset_run_uses_stable_id_url_and_version( + self, langfuse_memory_client, find_spans, monkeypatch + ): + create_score = MagicMock() + monkeypatch.setattr(langfuse_memory_client, "create_score", create_score) + monkeypatch.setattr(langfuse_memory_client, "_get_project_id", lambda: "p") + version = datetime(2026, 2, 3, 4, 5, 6, tzinfo=timezone.utc) + + result = langfuse_memory_client.run_experiment( + name="exp", + run_name="r", + data=[ + _dataset_item(item_id="i1", dataset_id="d", input="a"), + _dataset_item(item_id="i2", dataset_id="d", input="b"), + ], + task=lambda *, item, **kwargs: item.input, + run_evaluators=[lambda **kwargs: Evaluation(name="run", value=1.0)], + _dataset_version=version, + ) + langfuse_memory_client.flush() + + assert result.experiment_id == "eab007015b1c6f77" + assert ( + result.experiment_url + == "http://test-host/project/p/experiments/results?baseline=eab007015b1c6f77" + ) + assert {r.experiment_id for r in result.item_results} == {result.experiment_id} + + spans = find_spans("experiment-item-run") + find_spans("experiment-item-task") + assert len(spans) == 4 + for span in spans: + assert ( + span.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ID] + == result.experiment_id + ) + assert ( + span.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_VERSION] + == "2026-02-03T04:05:06Z" + ) + + run_scores = _run_scores(create_score) + assert len(run_scores) == 1 + assert run_scores[0]["dataset_run_id"] == result.experiment_id + assert run_scores[0]["name"] == "run" + assert "trace_id" not in run_scores[0] + assert "observation_id" not in run_scores[0] + assert "session_id" not in run_scores[0] + + def test_local_run_persists_run_scores_without_item_version( + self, langfuse_memory_client, find_spans, monkeypatch + ): + create_score = MagicMock() + monkeypatch.setattr(langfuse_memory_client, "create_score", create_score) + monkeypatch.setattr(langfuse_memory_client, "_get_project_id", lambda: "p") + + result = langfuse_memory_client.run_experiment( + name="exp", + run_name="r", + data=[{"input": "a"}], + task=lambda *, item, **kwargs: item["input"], + run_evaluators=[lambda **kwargs: Evaluation(name="run", value=1.0)], + _dataset_version=datetime(2026, 2, 3, tzinfo=timezone.utc), + ) + langfuse_memory_client.flush() + + assert len(result.experiment_id) == 16 + assert result.experiment_id != "eab007015b1c6f77" + assert result.experiment_url == ( + f"http://test-host/project/p/experiments/results?baseline={result.experiment_id}" + ) + assert [s["dataset_run_id"] for s in _run_scores(create_score)] == [ + result.experiment_id + ] + for span in find_spans("experiment-item-run") + find_spans( + "experiment-item-task" + ): + assert LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_VERSION not in ( + span.attributes or {} + ) + + def test_unresolvable_project_id_yields_no_url( + self, langfuse_memory_client, monkeypatch + ): + def fail(): + raise RuntimeError("no network") + + monkeypatch.setattr(langfuse_memory_client, "create_score", MagicMock()) + monkeypatch.setattr(langfuse_memory_client, "_get_project_id", fail) + + result = langfuse_memory_client.run_experiment( + name="exp", + data=[_dataset_item(item_id="i1", dataset_id="d", input="a")], + task=lambda *, item, **kwargs: item.input, + ) + + assert len(result.item_results) == 1 + assert len(result.experiment_id) == 16 + assert result.experiment_url is None + + +class TestExperimentResultAliases: + def test_dataset_run_fields_are_deprecated_aliases(self): + item_result = ExperimentItemResult( + item={"input": "a"}, + output="a", + evaluations=[], + trace_id="t", + experiment_id="e", + ) + result = ExperimentResult( + name="exp", + run_name="r", + description=None, + item_results=[item_result], + run_evaluations=[], + experiment_id="e", + experiment_url="http://host/project/p/experiments/results?baseline=e", + ) + + with pytest.warns(DeprecationWarning, match="experiment_id"): + assert result.dataset_run_id == "e" + with pytest.warns(DeprecationWarning, match="experiment_url"): + assert result.dataset_run_url == result.experiment_url + with pytest.warns(DeprecationWarning, match="experiment_id"): + assert item_result.dataset_run_id == "e" + assert f"Experiment:\n {result.experiment_url}" in result.format() + + def test_legacy_constructor_arguments_still_populate_new_fields(self): + item_result = ExperimentItemResult( + item={"input": "a"}, + output="a", + evaluations=[], + trace_id="t", + dataset_run_id="legacy", + ) + result = ExperimentResult( + name="exp", + run_name="r", + description=None, + item_results=[item_result], + run_evaluations=[], + experiment_id="e", + dataset_run_url="http://legacy", + ) + + assert item_result.experiment_id == "legacy" + assert result.experiment_url == "http://legacy" + + +class TestExperimentRunScoreGating: + def test_run_scores_kept_when_the_task_fails_after_experiment_attributes( + self, langfuse_memory_client, monkeypatch + ): + def failing_task(*, item, **kwargs): + raise RuntimeError("boom") + + create_score = MagicMock() + monkeypatch.setattr(langfuse_memory_client, "create_score", create_score) + monkeypatch.setattr(langfuse_memory_client, "_get_project_id", lambda: "p") + + langfuse_memory_client.run_experiment( + name="exp", + data=[{"input": "a"}], + task=failing_task, + run_evaluators=[lambda **kwargs: Evaluation(name="run", value=1.0)], + ) + + assert len(_run_scores(create_score)) == 1 + + def test_tracing_disabled_skips_project_id_lookup(self, monkeypatch): + client = Langfuse( + public_key="pk", secret_key="sk", base_url="http://x", tracing_enabled=False + ) + get_project_id = MagicMock(return_value="p") + monkeypatch.setattr(client, "_get_project_id", get_project_id) + + result = client.run_experiment( + name="exp", + data=[{"input": "a"}], + task=lambda *, item, **kwargs: item["input"], + ) + + get_project_id.assert_not_called() + assert result.experiment_url is None diff --git a/tests/unit/test_initialization.py b/tests/unit/test_initialization.py index 7181ae45e..1f682d3fe 100644 --- a/tests/unit/test_initialization.py +++ b/tests/unit/test_initialization.py @@ -100,6 +100,16 @@ def test_env_host_fallback(self, cleanup_env_vars): assert client._base_url == "http://env-host.com" + def test_env_host_fallback_logs_no_warning(self, cleanup_env_vars, caplog): + """Test that LANGFUSE_HOST keeps working without a deprecation warning.""" + caplog.set_level("WARNING", logger="langfuse") + os.environ["LANGFUSE_HOST"] = "http://env-host.com" + + client = Langfuse(public_key="test_pk", secret_key="test_sk") + + assert client._base_url == "http://env-host.com" + assert not any("deprecated" in record.message for record in caplog.records) + def test_default_base_url(self, cleanup_env_vars): """Test that default base_url is used when nothing is set.""" client = Langfuse( diff --git a/tests/unit/test_json.py b/tests/unit/test_json.py index 1f7ef6ece..e818f54a5 100644 --- a/tests/unit/test_json.py +++ b/tests/unit/test_json.py @@ -7,10 +7,10 @@ from unittest.mock import patch import pytest -from langchain.messages import HumanMessage +from langchain_core.messages import HumanMessage from pydantic import BaseModel -import langfuse +import langfuse._utils.serializer as serializer_module from langfuse._utils.serializer import EventSerializer from langfuse.api import ObservationLevel @@ -61,41 +61,39 @@ def test_json_decoder_without_langchain_serializer(): @pytest.fixture -def hide_available_langchain(monkeypatch): +def serializer_without_langchain(monkeypatch): import_orig = builtins.__import__ def mocked_import(name, *args, **kwargs): - if name == "langchain" or name == "langchain.load.serializable": + if name == "langchain_core" or name.startswith("langchain_core."): raise ImportError() return import_orig(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", mocked_import) + yield importlib.reload(serializer_module) + monkeypatch.undo() + importlib.reload(serializer_module) -@pytest.mark.usefixtures("hide_available_langchain") -def test_json_decoder_without_langchain_serializer_with_langchain_message(): - with pytest.raises(ImportError): - import langchain # noqa +def test_json_decoder_without_langchain_serializer_with_langchain_message( + serializer_without_langchain, +): with pytest.raises(ImportError): - from langchain.load.serializable import Serializable # noqa + from langchain_core.load.serializable import Serializable # noqa - importlib.reload(langfuse) obj = TestModel(foo="bar", bar=datetime(2021, 1, 1, 0, 0, 0, tzinfo=timezone.utc)) - result = json.dumps(obj, cls=EventSerializer) + result = json.dumps(obj, cls=serializer_without_langchain.EventSerializer) assert result == '{"foo": "bar", "bar": "2021-01-01T00:00:00Z"}' -@pytest.mark.usefixtures("hide_available_langchain") -def test_json_decoder_without_langchain_serializer_with_none(): - with pytest.raises(ImportError): - import langchain # noqa - +def test_json_decoder_without_langchain_serializer_with_none( + serializer_without_langchain, +): with pytest.raises(ImportError): - from langchain.load.serializable import Serializable # noqa + from langchain_core.load.serializable import Serializable # noqa - importlib.reload(langfuse) - result = json.dumps(None, cls=EventSerializer) + result = json.dumps(None, cls=serializer_without_langchain.EventSerializer) default = json.dumps(None) assert result == "null" assert result == default diff --git a/tests/unit/test_langchain.py b/tests/unit/test_langchain.py index 0e63fb6b1..12639f0bb 100644 --- a/tests/unit/test_langchain.py +++ b/tests/unit/test_langchain.py @@ -4,8 +4,7 @@ from uuid import uuid4 import pytest -from langchain.messages import HumanMessage -from langchain_core.messages import AIMessage, ToolMessage +from langchain_core.messages import AIMessage, HumanMessage, ToolMessage from langchain_core.output_parsers import StrOutputParser from langchain_core.outputs import ChatGeneration, ChatResult, Generation, LLMResult from langchain_core.prompts import ChatPromptTemplate @@ -845,7 +844,7 @@ def test_tool_when_structured_inputs_only_store_in_inputs_attribute_not_metadata metadata_prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA - assert span.attributes[f"{metadata_prefix}.custom_key"] == "custom_value" + assert span.attributes[f"{metadata_prefix}.custom_key"] == '"custom_value"' assert f"{metadata_prefix}.inputs" not in span.attributes @@ -924,6 +923,7 @@ class DummyControlFlowError(RuntimeError): def test_graphbubbleup_import_is_independent_from_command_import(): + pytest.importorskip("langgraph.errors") real_import = __import__ def import_without_langgraph_command( diff --git a/tests/unit/test_mask_otel_spans.py b/tests/unit/test_mask_otel_spans.py index 5d1d52ee4..d6ad7ec9a 100644 --- a/tests/unit/test_mask_otel_spans.py +++ b/tests/unit/test_mask_otel_spans.py @@ -21,6 +21,11 @@ from opentelemetry.trace import SpanContext, TraceFlags, TraceState import langfuse._client.span_exporter as span_exporter_module +from langfuse._client.attributes import ( + LangfuseOtelSpanAttributes, + _flatten_and_serialize_metadata, + _serialize_metadata_value, +) from langfuse._client.constants import LANGFUSE_TRACER_NAME from langfuse._client.span_processor import LangfuseSpanProcessor from langfuse._task_manager.media_manager import MediaManager @@ -250,6 +255,112 @@ def test_export_stage_media_processes_direct_data_uri_string(): assert not media_queue.empty() +def test_export_stage_media_processes_json_encoded_metadata_data_uri(): + exporter = InMemorySpanExporter() + media_manager, media_queue = _media_manager() + image_base64 = base64.b64encode(b"image-bytes").decode("utf-8") + data_uri = f"data:image/jpeg;base64,{image_base64}" + + provider = _tracer_provider(exporter=exporter, media_manager=media_manager) + tracer = provider.get_tracer("openinference.instrumentation.openai") + + observation_key = f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.image" + trace_key = f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.image" + + with tracer.start_as_current_span("json-metadata-media-span") as span: + span.set_attributes( + { + **_flatten_and_serialize_metadata({"image": data_uri}, "observation"), + trace_key: _serialize_metadata_value(data_uri), + # A quoted data URI outside Langfuse metadata is left as is. + "custom.payload": _serialize_metadata_value(data_uri), + } + ) + + provider.force_flush() + + attributes = exporter.get_finished_spans()[0].attributes + + for key in (observation_key, trace_key): + assert attributes[key].startswith('"@@@langfuseMedia:') + assert json.loads(attributes[key]).startswith("@@@langfuseMedia:") + assert image_base64 not in attributes[key] + assert attributes["custom.payload"] == json.dumps(data_uri) + assert media_queue.qsize() == 2 + + +def test_export_stage_media_extracts_json_encoded_metadata_data_uri_in_place( + monkeypatch, +): + media_manager, media_queue = _media_manager() + image_base64 = base64.b64encode(b"image-bytes").decode("utf-8") + json_load_calls = 0 + original_json_loads = json.loads + + def count_json_loads(*args, **kwargs): + nonlocal json_load_calls + json_load_calls += 1 + return original_json_loads(*args, **kwargs) + + monkeypatch.setattr(span_exporter_module.json, "loads", count_json_loads) + + transforming_exporter = span_exporter_module.LangfuseTransformingSpanExporter( + exporter=InMemorySpanExporter(), + media_manager=media_manager, + mask_otel_spans=None, + ) + span = SimpleNamespace( + name="json-metadata-media-in-place", + context=SimpleNamespace(trace_id=1, span_id=2), + ) + + result = transforming_exporter._process_media_string( + span=span, + attribute_key=f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.image", + value=_serialize_metadata_value(f"data:image/jpeg;base64,{image_base64}"), + ) + + assert json_load_calls == 0 + assert result.startswith('"@@@langfuseMedia:type=image/jpeg|') + assert original_json_loads(result).startswith("@@@langfuseMedia:") + assert media_queue.qsize() == 1 + + +@pytest.mark.parametrize( + "value", + [ + # Valid JSON for a data URI, but with the "/" characters escaped. + '"data:image/jpeg;base64,' + + base64.b64encode(b"\xff\xff\xff").decode().replace("/", "\\/") + + '"', + # Not a valid JSON string: an unescaped quote inside the value. + '"data:image/jpeg;base64,' + base64.b64encode(b"img").decode() + '" "x"', + # The ";base64" header is not within the first 256 characters. + '"data:image/jpeg;' + "p=1;" * 64 + 'base64,aW1n"', + ], +) +def test_export_stage_media_leaves_escaped_json_encoded_metadata_unchanged(value): + media_manager, media_queue = _media_manager() + transforming_exporter = span_exporter_module.LangfuseTransformingSpanExporter( + exporter=InMemorySpanExporter(), + media_manager=media_manager, + mask_otel_spans=None, + ) + span = SimpleNamespace( + name="json-metadata-escaped-media", + context=SimpleNamespace(trace_id=1, span_id=2), + ) + + result = transforming_exporter._process_media_string( + span=span, + attribute_key=f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.image", + value=value, + ) + + assert result == value + assert media_queue.empty() + + def test_export_stage_media_processes_string_sequence_attributes(): exporter = InMemorySpanExporter() media_manager, media_queue = _media_manager() @@ -293,7 +404,6 @@ def test_export_stage_media_processes_string_sequence_attributes(): @pytest.mark.parametrize( ("attribute_key", "expected_field"), [ - ("langfuse.trace.input", "input"), ("langfuse.observation.input", "input"), ("ai.prompt.messages", "input"), ("gcp.vertex.agent.tool_call_args", "input"), @@ -302,7 +412,6 @@ def test_export_stage_media_processes_string_sequence_attributes(): ("gen_ai.input.messages", "input"), ("gen_ai.prompt.0.content", "input"), ("llm.input_messages.0.message.content", "input"), - ("langfuse.trace.output", "output"), ("langfuse.observation.output", "output"), ("ai.response.toolCalls", "output"), ("gcp.vertex.agent.tool_response", "output"), diff --git a/tests/unit/test_media.py b/tests/unit/test_media.py index 387eae745..f222df576 100644 --- a/tests/unit/test_media.py +++ b/tests/unit/test_media.py @@ -263,6 +263,7 @@ def test_resolve_media_references_uses_configured_httpx_client(): "https://example.com/test.jpg", timeout=fetch_timeout_seconds ) + def test_init_with_urlsafe_base64_data_uri(): original_bytes = b"\xfb\xff" urlsafe_base64 = base64.urlsafe_b64encode(original_bytes).decode() @@ -274,4 +275,3 @@ def test_init_with_urlsafe_base64_data_uri(): assert media._source == "base64_data_uri" assert media._content_type == "application/octet-stream" assert media._content_bytes == original_bytes - diff --git a/tests/unit/test_metadata_attribute_limit.py b/tests/unit/test_metadata_attribute_limit.py new file mode 100644 index 000000000..54bb919a4 --- /dev/null +++ b/tests/unit/test_metadata_attribute_limit.py @@ -0,0 +1,638 @@ +"""Langfuse writes must stay within the OTel span attribute limit. + +The Python OTel SDK evicts the oldest attribute once a span holds more than +``SpanLimits.max_span_attributes`` attributes, which used to drop Langfuse core +attributes written first. The SDK now drops the newest attributes instead, like +OTel JS: excess new metadata keys go first, then any other new keys that still +do not fit. Metadata also leaves room for the reserved observation attributes +that later updates may write. +""" + +import json +import logging +from datetime import datetime +from types import SimpleNamespace + +import pytest +from opentelemetry.trace import format_span_id + +from langfuse import propagate_attributes +from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse._client.span import ( + _RESERVED_OBSERVATION_ATTRIBUTE_KEYS, + _RESERVED_SPAN_ATTRIBUTE_KEYS, + _drop_attributes_over_span_limit, + _reserved_attribute_keys, +) +from langfuse.api import DatasetItem, DatasetStatus + +METADATA_PREFIX = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + "." + + +def _metadata_keys(span): + return [key for key in span.attributes if key.startswith(METADATA_PREFIX)] + + +def _missing_reserved_keys(span): + observation_type = span.attributes.get(LangfuseOtelSpanAttributes.OBSERVATION_TYPE) + return [ + key + for key in _reserved_attribute_keys(observation_type) + if key not in span.attributes + ] + + +def _limit_warnings(caplog): + return [ + record + for record in caplog.records + if record.name == "langfuse" + and record.levelno == logging.WARNING + and "attribute limit" in record.getMessage() + ] + + +@pytest.fixture(autouse=True) +def default_attribute_limit(monkeypatch): + # Pin the OTel default so a custom limit in the environment cannot break + # the assertions below that expect 128. + monkeypatch.delenv("OTEL_ATTRIBUTE_COUNT_LIMIT", raising=False) + monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "128") + + +@pytest.fixture +def small_limit_client(monkeypatch, request): + monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT", "40") + return request.getfixturevalue("langfuse_memory_client") + + +def test_metadata_over_limit_at_start_keeps_core_attributes( + langfuse_memory_client, get_span, caplog +): + metadata = {f"key_{i}": f"value_{i}" for i in range(130)} + + generation = langfuse_memory_client.start_observation( + name="big-generation", + as_type="generation", + input="the input", + model="gpt-4o", + version="v1", + metadata=metadata, + ) + generation.end() + langfuse_memory_client.flush() + + span = get_span("big-generation") + attributes = span.attributes + + assert len(attributes) <= 128 + assert span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "generation" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o" + assert attributes[LangfuseOtelSpanAttributes.VERSION] == "v1" + + # Kept metadata keys are the first ones in insertion order. + kept = _metadata_keys(span) + assert kept == [f"{METADATA_PREFIX}key_{i}" for i in range(len(kept))] + assert 0 < len(kept) < 130 + + warnings = _limit_warnings(caplog) + assert len(warnings) == 1 + message = warnings[0].getMessage() + assert str(130 - len(kept)) in message + assert "128" in message + assert "big-generation" in message + assert f"key_{len(kept)}" in message + + +def test_update_over_limit_keeps_earlier_attributes( + langfuse_memory_client, get_span, caplog +): + span_wrapper = langfuse_memory_client.start_observation( + name="updated-span", + input="the input", + metadata={f"first_{i}": i for i in range(100)}, + ) + assert _limit_warnings(caplog) == [] + + span_wrapper.update( + output="the output", metadata={f"second_{i}": i for i in range(50)} + ) + span_wrapper.end() + langfuse_memory_client.flush() + + span = get_span("updated-span") + attributes = span.attributes + + assert len(attributes) + len(_missing_reserved_keys(span)) == 128 + assert span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "the output" + for i in range(100): + assert attributes[f"{METADATA_PREFIX}first_{i}"] == str(i) + + second = [key for key in _metadata_keys(span) if ".second_" in key] + assert second == [f"{METADATA_PREFIX}second_{i}" for i in range(len(second))] + assert len(second) < 50 + + assert len(_limit_warnings(caplog)) == 1 + + +def test_update_current_span_over_limit_keeps_earlier_attributes( + langfuse_memory_client, get_span, caplog +): + with langfuse_memory_client.start_as_current_observation( + name="current-span", input="the input" + ): + langfuse_memory_client.update_current_span( + metadata={f"key_{i}": i for i in range(200)} + ) + langfuse_memory_client.flush() + + span = get_span("current-span") + assert span.dropped_attributes == 0 + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert len(_limit_warnings(caplog)) == 1 + + +def test_overwriting_existing_metadata_keys_at_limit_is_allowed( + small_limit_client, get_span, caplog +): + span_wrapper = small_limit_client.start_observation( + name="overwrite-span", metadata={f"key_{i}": "old" for i in range(40)} + ) + span_at_start = span_wrapper._otel_span + assert ( + len(span_at_start.attributes) + len(_missing_reserved_keys(span_at_start)) == 40 + ) + kept = _metadata_keys(span_at_start) + caplog.clear() + + span_wrapper.update(metadata={key[len(METADATA_PREFIX) :]: "new" for key in kept}) + span_wrapper.end() + small_limit_client.flush() + + span = get_span("overwrite-span") + assert span.dropped_attributes == 0 + assert len(span.attributes) == len(span_at_start.attributes) + for key in kept: + assert span.attributes[key] == '"new"' + assert _limit_warnings(caplog) == [] + + +def test_custom_smaller_span_limit_is_respected(small_limit_client, get_span, caplog): + span_wrapper = small_limit_client.start_observation( + name="small-limit-span", + input="the input", + metadata={f"key_{i}": i for i in range(40)}, + ) + span_wrapper.end() + small_limit_client.flush() + + span = get_span("small-limit-span") + assert len(span.attributes) + len(_missing_reserved_keys(span)) == 40 + assert span.dropped_attributes == 0 + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "span" + + warnings = _limit_warnings(caplog) + assert len(warnings) == 1 + assert "40" in warnings[0].getMessage() + + +def test_propagated_trace_attributes_count_toward_limit( + small_limit_client, get_span, caplog +): + with propagate_attributes( + user_id="user-1", + session_id="session-1", + metadata={f"trace_{i}": str(i) for i in range(5)}, + ): + span_wrapper = small_limit_client.start_observation( + name="propagated-span", + input="the input", + metadata={f"key_{i}": i for i in range(30)}, + ) + span_wrapper.end() + small_limit_client.flush() + + span = get_span("propagated-span") + attributes = span.attributes + assert len(attributes) + len(_missing_reserved_keys(span)) == 40 + assert span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.TRACE_USER_ID] == "user-1" + assert attributes[LangfuseOtelSpanAttributes.TRACE_SESSION_ID] == "session-1" + for i in range(5): + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.trace_{i}"] + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert len(_limit_warnings(caplog)) == 1 + + +def test_propagated_metadata_over_limit_keeps_core_attributes( + small_limit_client, get_span +): + with propagate_attributes( + metadata={f"trace_{i}": str(i) for i in range(60)}, + ): + generation = small_limit_client.start_observation( + name="propagated-generation", + as_type="generation", + input="the input", + model="gpt-4o", + ) + generation.update(output="the output") + generation.end() + small_limit_client.flush() + + span = get_span("propagated-generation") + attributes = span.attributes + assert span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] == "generation" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "the output" + assert any( + key.startswith(LangfuseOtelSpanAttributes.TRACE_METADATA + ".") + for key in attributes + ) + + +def test_metadata_under_limit_is_unchanged(langfuse_memory_client, get_span, caplog): + metadata = {f"key_{i}": i for i in range(50)} + span_wrapper = langfuse_memory_client.start_observation( + name="small-span", input="the input", metadata=metadata + ) + span_wrapper.update(metadata={"extra": "value"}) + span_wrapper.end() + langfuse_memory_client.flush() + + span = get_span("small-span") + assert span.dropped_attributes == 0 + assert len(_metadata_keys(span)) == 51 + for i in range(50): + assert span.attributes[f"{METADATA_PREFIX}key_{i}"] == str(i) + assert span.attributes[f"{METADATA_PREFIX}extra"] == '"value"' + assert _limit_warnings(caplog) == [] + + +def test_later_updates_do_not_evict_attributes_after_metadata_cap( + langfuse_memory_client, get_span, caplog +): + generation = langfuse_memory_client.start_observation( + name="capped-generation", + as_type="generation", + input="the input", + model="gpt-4o", + metadata={f"key_{i}": i for i in range(130)}, + ) + generation.update(output="the output") + generation.update( + usage_details={"input": 10, "output": 20}, + cost_details={"input": 0.1, "output": 0.2}, + ) + generation.end() + langfuse_memory_client.flush() + + span = get_span("capped-generation") + attributes = span.attributes + + assert span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "the output" + assert LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS in attributes + assert LangfuseOtelSpanAttributes.OBSERVATION_COST_DETAILS in attributes + assert len(attributes) + len(_missing_reserved_keys(span)) == 128 + assert len(_limit_warnings(caplog)) == 1 + + +def test_reserved_keys_already_written_cost_no_extra_slot(small_limit_client, get_span): + metadata = {f"key_{i}": i for i in range(50)} + reserved_values = { + "input": "the input", + "output": "the output", + "version": "v1", + "level": "WARNING", + "status_message": "careful", + } + + small_limit_client.start_observation(name="metadata-only", metadata=metadata).end() + small_limit_client.start_observation( + name="reserved-in-same-write", metadata=metadata, **reserved_values + ).end() + small_limit_client.start_observation( + name="reserved-already-on-span", **reserved_values + ).update(metadata=metadata).end() + small_limit_client.flush() + + kept_counts = { + name: len(_metadata_keys(get_span(name))) + for name in ( + "metadata-only", + "reserved-in-same-write", + "reserved-already-on-span", + ) + } + assert 0 < kept_counts["metadata-only"] < 50 + assert len(set(kept_counts.values())) == 1, kept_counts + + +def test_reserved_keys_depend_on_observation_type(langfuse_memory_client, get_span): + assert len(_RESERVED_SPAN_ATTRIBUTE_KEYS) == 7 + assert len(_RESERVED_OBSERVATION_ATTRIBUTE_KEYS) == 14 + for observation_type in ("generation", "embedding", None): + assert ( + _reserved_attribute_keys(observation_type) + == _RESERVED_OBSERVATION_ATTRIBUTE_KEYS + ) + for observation_type in ( + "span", + "agent", + "tool", + "chain", + "retriever", + "evaluator", + "guardrail", + "event", + ): + assert _reserved_attribute_keys(observation_type) == ( + _RESERVED_SPAN_ATTRIBUTE_KEYS + ) + + metadata = {f"key_{i}": i for i in range(130)} + for as_type in ("span", "generation"): + langfuse_memory_client.start_observation( + name=f"typed-{as_type}", + as_type=as_type, + input="the input", + metadata=metadata, + ).end() + langfuse_memory_client.flush() + + span = get_span("typed-span") + generation = get_span("typed-generation") + span_count = len(_metadata_keys(span)) + generation_count = len(_metadata_keys(generation)) + # The span reserves 5 open slots (level, status message, version, output, + # plus environment when unset) instead of 12 for the generation. + assert span_count - generation_count == 7 + assert span.dropped_attributes == 0 + assert generation.dropped_attributes == 0 + assert len(span.attributes) + len(_missing_reserved_keys(span)) == 128 + assert len(generation.attributes) + len(_missing_reserved_keys(generation)) == 128 + + +def test_generation_keeps_room_for_model_usage_and_output( + small_limit_client, get_span, caplog +): + generation = small_limit_client.start_observation( + name="roomy-generation", + as_type="generation", + input="the input", + metadata={f"key_{i}": i for i in range(40)}, + ) + caplog.clear() + generation.update( + output="the output", + model="gpt-4o", + usage_details={"input": 1}, + cost_details={"input": 0.1}, + model_parameters={"temperature": 0}, + ) + generation.end() + small_limit_client.flush() + + span = get_span("roomy-generation") + attributes = span.attributes + assert span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "the output" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o" + assert LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS in attributes + assert LangfuseOtelSpanAttributes.OBSERVATION_COST_DETAILS in attributes + assert LangfuseOtelSpanAttributes.OBSERVATION_MODEL_PARAMETERS in attributes + assert _limit_warnings(caplog) == [] + + +def test_warning_message_format(small_limit_client, caplog): + small_limit_client.start_observation( + name="warned-span", metadata={f"key_{i}": i for i in range(40)} + ).end() + + warnings = _limit_warnings(caplog) + assert len(warnings) == 1 + message = warnings[0].getMessage() + assert message.startswith("Dropped ") + assert ( + " metadata key(s) from observation 'warned-span' to stay within the span " + "attribute limit of 40 (SpanLimits.max_span_attributes / " + "OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT). Dropped keys include: " + ) in message + dropped_keys = message.split("Dropped keys include: ")[1].split(", ") + assert len(dropped_keys) == 5 + assert all(key.startswith("key_") for key in dropped_keys) + + +def test_propagated_metadata_on_full_span_drops_newest_instead_of_evicting( + small_limit_client, get_span, caplog +): + with small_limit_client.start_as_current_observation( + name="full-generation", + as_type="generation", + input="the input", + model="gpt-4o", + metadata={f"key_{i}": i for i in range(40)}, + ) as generation: + generation.update(output="the output", usage_details={"input": 1}) + otel_span = generation._otel_span + free_slots = 40 - len(otel_span.attributes) + assert 0 < free_slots < 20 + # The free slots are the ones reserved for later updates, so propagated + # metadata gets none of them. + metadata_slots = max(0, free_slots - len(_missing_reserved_keys(otel_span))) + attributes_before = dict(otel_span.attributes) + caplog.clear() + + with propagate_attributes( + metadata={f"trace_{i:02d}": str(i) for i in range(20)} + ): + pass + small_limit_client.flush() + + span = get_span("full-generation") + attributes = span.attributes + + assert span.dropped_attributes == 0 + assert len(attributes) == 40 - free_slots + metadata_slots + for key, value in attributes_before.items(): + assert attributes[key] == value + assert attributes[LangfuseOtelSpanAttributes.IS_APP_ROOT] is True + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "the input" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "the output" + + trace_prefix = LangfuseOtelSpanAttributes.TRACE_METADATA + "." + kept_trace_keys = [key for key in attributes if key.startswith(trace_prefix)] + assert kept_trace_keys == [ + f"{trace_prefix}trace_{i:02d}" for i in range(metadata_slots) + ] + + warnings = _limit_warnings(caplog) + assert len(warnings) == 1 + message = warnings[0].getMessage() + assert f"Dropped {20 - metadata_slots} metadata key(s) from" in message + assert f"{trace_prefix}trace_{metadata_slots:02d}" in message + + +def _fake_span(limit, existing): + return SimpleNamespace( + name="fake", + attributes=dict(existing), + _limits=SimpleNamespace(max_span_attributes=limit), + ) + + +def test_hard_guard_keeps_overwrites_and_drops_new_tail(caplog): + span = _fake_span(5, {f"a_{i}": i for i in range(4)}) + + result = _drop_attributes_over_span_limit( + span, + { + "a_0": "overwritten", + "b_0": 0, + f"{METADATA_PREFIX}m": 1, + "b_1": 1, + "b_2": 2, + }, + ) + + assert result == {"a_0": "overwritten", "b_0": 0} + message = _limit_warnings(caplog)[0].getMessage() + assert "1 metadata key(s) and 2 other attribute(s)" in message + assert "Dropped keys include: m, b_1, b_2" in message + + +def test_hard_guard_never_raises(): + class BrokenAttributes: + def __contains__(self, key): + raise RuntimeError("boom") + + def __len__(self): + return 1 + + span = SimpleNamespace( + attributes=BrokenAttributes(), + _limits=SimpleNamespace(max_span_attributes=1), + ) + attributes = {"key": "value"} + + assert _drop_attributes_over_span_limit(span, attributes) is attributes + + +def test_experiment_with_large_metadata_keeps_output_and_experiment_attributes( + langfuse_memory_client, get_span, find_spans, caplog +): + item_metadata = { + "experiment_name": "user-value", + "nested": {"a": 1, "b": {"c": "d"}}, + **{f"item_{i}": str(i) for i in range(150)}, + } + item = DatasetItem( + id="item-1", + status=DatasetStatus.ACTIVE, + input="question", + expected_output="answer", + metadata=item_metadata, + source_trace_id=None, + source_observation_id=None, + dataset_id="dataset-1", + dataset_name="Dataset", + created_at=datetime.now(), + updated_at=datetime.now(), + media_references=[], + ) + + def task(**kwargs): + langfuse_memory_client.start_observation(name="task-child").end() + return "the answer" + + result = langfuse_memory_client.run_experiment( + name="big-metadata-experiment", + data=[item], + task=task, + metadata={"run": "metadata"}, + max_concurrency=1, + ) + langfuse_memory_client.flush() + + task_span = get_span("experiment-item-task") + task_span_id = format_span_id(task_span.context.span_id) + attributes = task_span.attributes + assert task_span.dropped_attributes == 0 + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] == "question" + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "the answer" + + # Copied experiment metadata is one JSON attribute each, never trimmed. + assert ( + json.loads(attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA]) + == item_metadata + ) + assert json.loads(attributes[LangfuseOtelSpanAttributes.EXPERIMENT_METADATA]) == { + "run": "metadata" + } + assert not [ + key + for key in attributes + if key.startswith(LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA + ".") + ] + assert attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ID] == result.experiment_id + assert attributes[LangfuseOtelSpanAttributes.EXPERIMENT_NAME] == result.run_name + assert attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID] == "item-1" + assert ( + attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID] + == task_span_id + ) + + # Observation metadata is trimmed, but the run keys survive and the run + # value wins over the user key of the same name. + assert 0 < len(_metadata_keys(task_span)) < len(item_metadata) + 4 + assert attributes[f"{METADATA_PREFIX}experiment_name"] == json.dumps( + "big-metadata-experiment" + ) + assert attributes[f"{METADATA_PREFIX}experiment_run_name"] == json.dumps( + result.run_name + ) + assert attributes[f"{METADATA_PREFIX}dataset_id"] == '"dataset-1"' + assert attributes[f"{METADATA_PREFIX}dataset_item_id"] == '"item-1"' + assert f"{METADATA_PREFIX}item_149" not in attributes + + item_run = get_span("experiment-item-run") + assert item_run.dropped_attributes == 0 + assert item_run.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == ( + "the answer" + ) + assert item_run.attributes[f"{METADATA_PREFIX}experiment_run_name"] == json.dumps( + result.run_name + ) + assert LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA in item_run.attributes + + (child,) = find_spans("task-child") + assert child.dropped_attributes == 0 + assert len(child.attributes) < 30 + assert ( + json.loads( + child.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA] + ) + == item_metadata + ) + assert ( + child.attributes[LangfuseOtelSpanAttributes.EXPERIMENT_METADATA] + == attributes[LangfuseOtelSpanAttributes.EXPERIMENT_METADATA] + ) + + # Only the observation metadata of the task and item-run spans is trimmed. + warnings = _limit_warnings(caplog) + assert warnings + assert all("other attribute(s)" not in w.getMessage() for w in warnings) diff --git a/tests/unit/test_observe.py b/tests/unit/test_observe.py index f2ff11789..21ff1418d 100644 --- a/tests/unit/test_observe.py +++ b/tests/unit/test_observe.py @@ -3,13 +3,11 @@ import gc import inspect import json -import sys from typing import Any, AsyncGenerator, Generator, cast import pytest from langfuse import observe -from langfuse._client import observe as observe_module from langfuse._client.attributes import LangfuseOtelSpanAttributes from langfuse._client.observe import ( _ContextPreservedAsyncGeneratorWrapper, @@ -96,7 +94,6 @@ def body() -> Generator[str, None, None]: @pytest.mark.asyncio -@pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11 or higher") async def test_streaming_response_preserves_context_without_output_capture( langfuse_memory_client: Any, memory_exporter: Any ) -> None: @@ -479,41 +476,6 @@ async def generator() -> AsyncGenerator[str, None]: assert span.updates[-1] == {"level": "ERROR", "status_message": "cleanup failed"} -@pytest.mark.asyncio -async def test_async_generator_wrapper_fallback_preserves_context( - monkeypatch: pytest.MonkeyPatch, -) -> None: - marker = contextvars.ContextVar("marker", default="ambient") - seen: list[str] = [] - monkeypatch.setattr(observe_module, "_ASYNCIO_CREATE_TASK_SUPPORTS_CONTEXT", False) - - async def generator() -> AsyncGenerator[str, None]: - try: - yield marker.get() - yield "item_1" - finally: - seen.append(marker.get()) - - span = SpanRecorder() - context = contextvars.copy_context() - context.run(marker.set, "preserved") - wrapper = _ContextPreservedAsyncGeneratorWrapper( - generator(), - context, - cast(Any, span), - False, - None, - ) - - assert await wrapper.__anext__() == "preserved" - marker.set("ambient-now") - - await wrapper.aclose() - - assert seen == ["preserved"] - assert span.ended == 1 - - @pytest.mark.asyncio async def test_async_generator_wrapper_del_ends_span_when_abandoned() -> None: async def generator() -> AsyncGenerator[str, None]: @@ -537,3 +499,27 @@ async def generator() -> AsyncGenerator[str, None]: assert span.ended == 1 assert span.updates == [] + + +@pytest.mark.asyncio +async def test_observe_treats_legacy_marked_coroutine_function_as_async( + langfuse_memory_client: Any, memory_exporter: Any +) -> None: + async def work() -> str: + await asyncio.sleep(0) + return "done" + + def marked() -> Any: + return work() + + # The marker asgiref's markcoroutinefunction sets on Python < 3.12. + cast(Any, marked)._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore[attr-defined] + + observed = observe(name="marked")(marked) + + assert await observed() == "done" + + langfuse_memory_client.flush() + + span = _finished_spans_by_name(memory_exporter, "marked")[0] + assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "done" diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py index cacdae5c2..b688fc6f1 100644 --- a/tests/unit/test_openai.py +++ b/tests/unit/test_openai.py @@ -289,7 +289,7 @@ def test_chat_completion_exports_generation_span( assert ( span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "gpt-4o-mini" ) - assert span.attributes["langfuse.observation.metadata.suite"] == "unit" + assert span.attributes["langfuse.observation.metadata.suite"] == '"unit"' assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_INPUT) == [ {"role": "user", "content": "1 + 1 = ?"} ] @@ -371,7 +371,7 @@ def test_openai_stream_with_none_choices_chunk_does_not_crash( span = get_span("unit-openai-stream-none-choices") assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" - assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == '"stop"' def test_streaming_chat_completion_preserves_tool_calls_after_content(): @@ -507,7 +507,7 @@ def fake_stream(): span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] is not None ) - assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == '"stop"' assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { "prompt_tokens": 3, "completion_tokens": 1, @@ -575,7 +575,7 @@ def test_openai_stream_preserves_original_stream_contract( span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] is not None ) - assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == '"stop"' assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { "prompt_tokens": 3, "completion_tokens": 1, @@ -610,7 +610,7 @@ def test_openai_stream_handles_trailing_azure_content_filter_chunk( span = get_span("unit-openai-native-stream-azure-filter") assert span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] == "2" - assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == '"stop"' assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { "prompt_tokens": 3, "completion_tokens": 1, @@ -734,7 +734,7 @@ async def test_openai_async_stream_preserves_original_stream_contract( span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] is not None ) - assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == '"stop"' assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { "prompt_tokens": 3, "completion_tokens": 1, @@ -777,7 +777,7 @@ async def test_openai_async_stream_supports_anext( span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_COMPLETION_START_TIME] is not None ) - assert span.attributes["langfuse.observation.metadata.finish_reason"] == "stop" + assert span.attributes["langfuse.observation.metadata.finish_reason"] == '"stop"' assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { "prompt_tokens": 3, "completion_tokens": 1, diff --git a/tests/unit/test_otel.py b/tests/unit/test_otel.py index 46a085a71..dfe1336fc 100644 --- a/tests/unit/test_otel.py +++ b/tests/unit/test_otel.py @@ -1,4 +1,5 @@ import json +import logging from datetime import datetime from hashlib import sha256 from typing import List, Sequence @@ -120,10 +121,6 @@ def mock_init(self, **kwargs): from langfuse._client.span_filter import is_default_export_span self.public_key = kwargs.get("public_key", "test-key") - blocked_scopes = kwargs.get("blocked_instrumentation_scopes") - self.blocked_instrumentation_scopes = ( - blocked_scopes if blocked_scopes is not None else [] - ) self._should_export_span = ( kwargs.get("should_export_span") or is_default_export_span ) @@ -418,7 +415,7 @@ def test_span_attributes(self, langfuse_client, memory_exporter): # Verify attribute values assert input_data == {"prompt": "Test prompt"} assert output_data == {"response": "Updated response"} - assert metadata_data == "test-session" + assert metadata_data == '"test-session"' assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_LEVEL] == "INFO" assert ( attributes[LangfuseOtelSpanAttributes.OBSERVATION_STATUS_MESSAGE] @@ -502,8 +499,8 @@ def test_generation_name_update(self, langfuse_client, memory_exporter): def test_trace_update(self, langfuse_client, memory_exporter): """Test updating trace level attributes.""" - # Create a span and set trace attributes using propagate_attributes and set_trace_io - with langfuse_client.start_as_current_observation(name="trace-span") as span: + # Create a span and set trace attributes using propagate_attributes + with langfuse_client.start_as_current_observation(name="trace-span"): with propagate_attributes( trace_name="updated-trace-name", user_id="test-user", @@ -511,7 +508,7 @@ def test_trace_update(self, langfuse_client, memory_exporter): tags=["tag1", "tag2"], metadata={"trace-meta": "data"}, ): - span.set_trace_io(input={"trace-input": "value"}) + pass # Get the span data spans = self.get_spans_by_name(memory_exporter, "trace-span") @@ -530,13 +527,11 @@ def test_trace_update(self, langfuse_client, memory_exporter): else: tags = list(attributes[LangfuseOtelSpanAttributes.TRACE_TAGS]) - input_data = json.loads(attributes[LangfuseOtelSpanAttributes.TRACE_INPUT]) metadata = attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.trace-meta"] # Check attribute values assert sorted(tags) == sorted(["tag1", "tag2"]) - assert input_data == {"trace-input": "value"} - assert metadata == "data" + assert metadata == '"data"' def test_complex_scenario(self, langfuse_client, memory_exporter): """Test a more complex scenario with multiple operations and nesting.""" @@ -614,7 +609,7 @@ def test_complex_scenario(self, langfuse_client, memory_exporter): proc_metadata = proc["attributes"][ f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.step" ] - assert proc_metadata == "processing" + assert proc_metadata == '"processing"' # Parse input/output JSON llm_input = json.loads( @@ -1551,17 +1546,15 @@ class TestMetadataHandling(TestOTelBase): def test_complex_metadata_serialization(self): """Test the _flatten_and_serialize_metadata function directly.""" - from langfuse._client.attributes import ( - _flatten_and_serialize_metadata, - _serialize, - ) + from langfuse._client.attributes import _flatten_and_serialize_metadata # Test case 1: Non-dict metadata non_dict_result = _flatten_and_serialize_metadata("string-value", "observation") assert LangfuseOtelSpanAttributes.OBSERVATION_METADATA in non_dict_result - assert non_dict_result[ - LangfuseOtelSpanAttributes.OBSERVATION_METADATA - ] == _serialize("string-value") + assert ( + non_dict_result[LangfuseOtelSpanAttributes.OBSERVATION_METADATA] + == '"string-value"' + ) # Test case 2: Simple dict simple_dict = {"key1": "value1", "key2": 123} @@ -1574,11 +1567,11 @@ def test_complex_metadata_serialization(self): ) assert ( simple_result[f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.key1"] - == "value1" + == '"value1"' ) assert ( simple_result[f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.key2"] - == 123 + == "123" ) # Test case 3: Nested dict (will be flattened in current implementation) @@ -1631,7 +1624,7 @@ def test_complex_metadata_serialization(self): # The nested structures are serialized as JSON strings assert json.loads(complex_result[level1_key]) == complex_dict["level1"] - assert complex_result[sibling_key] == "value" + assert complex_result[sibling_key] == '"value"' def test_nested_metadata_updates(self): """Test that nested metadata updates don't overwrite unrelated keys.""" @@ -1986,6 +1979,364 @@ def update_random_metadata(thread_id): assert "version" in system_data assert "features" in system_data + def test_metadata_values_are_json_encoded(self, langfuse_client, memory_exporter): + """Every per-key metadata value is sent as a JSON-encoded string.""" + from datetime import timezone + + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + with langfuse_client.start_as_current_observation( + name="json-metadata-span", + metadata={ + "string": "hello", + "numeric_string": "123", + "bool_string": "true", + "int": 5, + "bool": True, + "float": 1.5, + "big_int": 2**70, + "nan": float("nan"), + "list": [1, "a", None], + "nested": {"a": {"b": [1, 2]}, "c": "d"}, + "datetime": datetime(2024, 1, 2, 3, 4, 5, tzinfo=timezone.utc), + }, + ): + pass + + attributes = self.get_spans_by_name(memory_exporter, "json-metadata-span")[0][ + "attributes" + ] + + expected = { + "string": '"hello"', + "numeric_string": '"123"', + "bool_string": '"true"', + "int": "5", + "bool": "true", + "float": "1.5", + "big_int": '"1180591620717411303424"', + "nan": '"NaN"', + "list": '[1,"a",null]', + "nested": '{"a":{"b":[1,2]},"c":"d"}', + "datetime": '"2024-01-02T03:04:05Z"', + } + for key, value in expected.items(): + assert attributes[f"{prefix}.{key}"] == value, key + + # Decoding restores the original type, so strings and numbers stay distinct. + assert json.loads(attributes[f"{prefix}.numeric_string"]) == "123" + assert json.loads(attributes[f"{prefix}.int"]) == 5 + assert json.loads(attributes[f"{prefix}.bool"]) is True + + def test_big_int_metadata_value_survives_otlp_encoding( + self, langfuse_client, memory_exporter + ): + """Ints beyond int64 must not make the OTLP encoder drop the key.""" + from opentelemetry.exporter.otlp.proto.common.trace_encoder import ( + encode_spans, + ) + + with langfuse_client.start_as_current_observation( + name="big-int-span", metadata={"big": 2**70, "small": 1} + ): + pass + + spans = [ + s for s in memory_exporter.get_finished_spans() if s.name == "big-int-span" + ] + request = encode_spans(spans) + encoded = { + kv.key: kv.value.string_value + for kv in request.resource_spans[0].scope_spans[0].spans[0].attributes + } + + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + assert encoded[f"{prefix}.big"] == '"1180591620717411303424"' + assert encoded[f"{prefix}.small"] == "1" + + def test_none_metadata_value_keeps_earlier_value_on_update( + self, langfuse_client, memory_exporter + ): + """None values are skipped, so they don't overwrite earlier keys.""" + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + with langfuse_client.start_as_current_observation( + name="none-metadata-span", metadata={"keep": "first", "other": 1} + ) as span: + span.update(metadata={"keep": None, "other": 2}) + + attributes = self.get_spans_by_name(memory_exporter, "none-metadata-span")[0][ + "attributes" + ] + + assert attributes[f"{prefix}.keep"] == '"first"' + assert attributes[f"{prefix}.other"] == "2" + + def test_none_metadata_value_is_debug_logged(self, caplog): + """Skipped None values are logged at debug level, non-None keys are not.""" + from langfuse._client.attributes import _flatten_and_serialize_metadata + + caplog.set_level(logging.DEBUG, logger="langfuse") + + _flatten_and_serialize_metadata( + {"keep": None, "other": 2, "text": "x"}, "observation" + ) + + messages = [ + record.getMessage() + for record in caplog.records + if record.name == "langfuse" + ] + assert messages == [ + 'Observation metadata key "keep" was not written because its value is None' + ] + + @pytest.mark.parametrize( + "metadata, expected", + [("foo", '"foo"'), (5, "5"), ([1, "a", None], '[1,"a",null]')], + ) + def test_non_dict_metadata_is_json_encoded( + self, langfuse_client, memory_exporter, metadata, expected + ): + """Non-dict metadata is sent JSON-encoded on the bare key, strings included.""" + with langfuse_client.start_as_current_observation( + name="non-dict-metadata-span", metadata=metadata + ): + pass + + attributes = self.get_spans_by_name(memory_exporter, "non-dict-metadata-span")[ + 0 + ]["attributes"] + + assert attributes[LangfuseOtelSpanAttributes.OBSERVATION_METADATA] == expected + + def test_flatten_metadata_trace_prefix_uses_same_encoding(self): + """The trace prefix variant encodes values the same way.""" + from langfuse._client.attributes import _flatten_and_serialize_metadata + + result = _flatten_and_serialize_metadata( + {"s": "x", "i": 1, "none": None}, "trace" + ) + + prefix = LangfuseOtelSpanAttributes.TRACE_METADATA + assert result == {f"{prefix}.s": '"x"', f"{prefix}.i": "1"} + + def test_metadata_json_matches_js_json_stringify(self): + """Values use compact separators and keep non-ASCII, like JSON.stringify.""" + from langfuse._client.attributes import _flatten_and_serialize_metadata + + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + result = _flatten_and_serialize_metadata( + {"umlaut": "ü", "nested": {"a": {"b": [1, "x"]}}}, "observation" + ) + + assert result == { + f"{prefix}.umlaut": '"ü"', + f"{prefix}.nested": '{"a":{"b":[1,"x"]}}', + } + + non_dict_result = _flatten_and_serialize_metadata([1, "ü"], "observation") + assert non_dict_result == {prefix: '[1,"ü"]'} + + # Expected values are the output of JSON.stringify in Node. + @pytest.mark.parametrize( + "value, js_json_stringify", + [ + ("prod", '"prod"'), + ("123", '"123"'), + ("", '""'), + ('a"b\\c', '"a\\"b\\\\c"'), + ("line\nbreak\ttab\b\f\r", '"line\\nbreak\\ttab\\b\\f\\r"'), + ("\x00\x1f\x7f", '"\\u0000\\u001f\x7f"'), + ("\u2028\u2029", '"\u2028\u2029"'), + ("ü Läufe 🚀 日本", '"ü Läufe 🚀 日本"'), + ("", '""'), + ], + ) + def test_string_fast_path_matches_event_serializer_and_js( + self, value, js_json_stringify + ): + """Plain strings skip EventSerializer but must encode to the same bytes.""" + from langfuse._client.attributes import _serialize_metadata_value + from langfuse._utils.serializer import EventSerializer + + via_event_serializer = json.dumps( + value, cls=EventSerializer, separators=(",", ":"), ensure_ascii=False + ) + + assert _serialize_metadata_value(value) == via_event_serializer + assert _serialize_metadata_value(value) == js_json_stringify + + def test_str_subclass_metadata_goes_through_event_serializer(self): + """str subclasses keep EventSerializer handling, e.g. str enums.""" + from enum import Enum + + from langfuse._client.attributes import _serialize_metadata_value + + class Stage(str, Enum): + PROD = "prod" + + assert _serialize_metadata_value(Stage.PROD) == '"prod"' + + MASK_FALLBACK = "" + # Metadata attributes carry the fallback JSON-encoded like any other value. + ENCODED_MASK_FALLBACK = json.dumps(MASK_FALLBACK) + + def get_metadata_attributes(self, span_data: dict) -> dict: + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + return { + key: value + for key, value in span_data["attributes"].items() + if key == prefix or key.startswith(f"{prefix}.") + } + + def test_failed_mask_on_update_masks_each_metadata_key( + self, configurable_langfuse_client, memory_exporter + ): + def mask(*, data, **kwargs): + if isinstance(data, dict) and "secret" in data: + raise ValueError("mask failed") + return data + + langfuse_client = configurable_langfuse_client(mask=mask) + span = langfuse_client.start_observation(name="mask-fail", metadata={"a": 1}) + span.update(metadata={"secret": "pw", "b": 2}) + span.end() + + span_data = self.get_spans_by_name(memory_exporter, "mask-fail")[0] + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + assert self.get_metadata_attributes(span_data) == { + f"{prefix}.a": "1", + f"{prefix}.secret": self.ENCODED_MASK_FALLBACK, + f"{prefix}.b": self.ENCODED_MASK_FALLBACK, + } + + def test_failed_mask_on_start_masks_each_metadata_key( + self, configurable_langfuse_client, memory_exporter + ): + def mask(*, data, **kwargs): + if isinstance(data, dict): + raise ValueError("mask failed") + return data + + langfuse_client = configurable_langfuse_client(mask=mask) + span = langfuse_client.start_observation( + name="mask-fail-start", metadata={"secret": "pw", "b": 2} + ) + span.end() + + span_data = self.get_spans_by_name(memory_exporter, "mask-fail-start")[0] + prefix = LangfuseOtelSpanAttributes.OBSERVATION_METADATA + assert self.get_metadata_attributes(span_data) == { + f"{prefix}.secret": self.ENCODED_MASK_FALLBACK, + f"{prefix}.b": self.ENCODED_MASK_FALLBACK, + } + + def test_failed_mask_keeps_string_fallback_for_empty_dict_metadata( + self, configurable_langfuse_client, memory_exporter + ): + def mask(*, data, **kwargs): + if isinstance(data, dict): + raise ValueError("mask failed") + return data + + langfuse_client = configurable_langfuse_client(mask=mask) + span = langfuse_client.start_observation( + name="mask-fail-empty-dict", metadata={} + ) + span.end() + + span_data = self.get_spans_by_name(memory_exporter, "mask-fail-empty-dict")[0] + assert self.get_metadata_attributes(span_data) == { + LangfuseOtelSpanAttributes.OBSERVATION_METADATA: self.ENCODED_MASK_FALLBACK, + } + + def test_failed_mask_keeps_string_fallback_for_non_dict_values( + self, configurable_langfuse_client, memory_exporter + ): + def mask(*, data, **kwargs): + if data is not None: + raise ValueError("mask failed") + return data + + langfuse_client = configurable_langfuse_client(mask=mask) + span = langfuse_client.start_observation( + name="mask-fail-non-dict", + input={"secret": "pw"}, + output={"secret": "pw"}, + metadata="plain-string", + ) + span.end() + + span_data = self.get_spans_by_name(memory_exporter, "mask-fail-non-dict")[0] + attributes = span_data["attributes"] + assert self.get_metadata_attributes(span_data) == { + LangfuseOtelSpanAttributes.OBSERVATION_METADATA: self.ENCODED_MASK_FALLBACK, + } + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_INPUT] + == self.MASK_FALLBACK + ) + assert ( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] + == self.MASK_FALLBACK + ) + + def test_unset_attributes_are_never_passed_to_the_mask( + self, configurable_langfuse_client, memory_exporter + ): + """Only attributes the caller set may reach the mask function. + + A mask that assumes it always receives a dict (the common shape) must not + trigger the fallback for attributes the caller never provided. Otherwise + phantom `input`/`metadata` attributes get written to the span. + """ + seen = [] + + def mask(*, data, **kwargs): + seen.append(data) + return {**data, "email": "***"} + + langfuse_client = configurable_langfuse_client(mask=mask) + span = langfuse_client.start_observation(name="mask-unset") + span.update(output={"answer": "42"}) + span.end() + + span_data = self.get_spans_by_name(memory_exporter, "mask-unset")[0] + attributes = span_data["attributes"] + + assert seen == [{"answer": "42"}] + assert json.loads( + attributes[LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT] + ) == {"answer": "42", "email": "***"} + assert LangfuseOtelSpanAttributes.OBSERVATION_INPUT not in attributes + assert self.get_metadata_attributes(span_data) == {} + + def test_unset_attributes_stay_unset_when_created_with_metadata_only( + self, configurable_langfuse_client, memory_exporter + ): + """Creating a span with only metadata must not mask a missing input/output.""" + seen = [] + + def mask(*, data, **kwargs): + seen.append(data) + return {**data, "email": "***"} + + langfuse_client = configurable_langfuse_client(mask=mask) + span = langfuse_client.start_observation( + name="mask-unset-start", metadata={"k": 1} + ) + span.end() + + span_data = self.get_spans_by_name(memory_exporter, "mask-unset-start")[0] + attributes = span_data["attributes"] + + assert seen == [{"k": 1}] + assert self.get_metadata_attributes(span_data) == { + f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.k": "1", + f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.email": '"***"', + } + assert LangfuseOtelSpanAttributes.OBSERVATION_INPUT not in attributes + assert LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT not in attributes + class TestMultiProjectSetup(TestOTelBase): """Tests for multi-project setup within the same process. @@ -2408,10 +2759,6 @@ def mock_processor_init(self, **kwargs): from langfuse._client.span_filter import is_default_export_span self.public_key = kwargs.get("public_key", "test-key") - blocked_scopes = kwargs.get("blocked_instrumentation_scopes") - self.blocked_instrumentation_scopes = ( - blocked_scopes if blocked_scopes is not None else [] - ) self._should_export_span = ( kwargs.get("should_export_span") or is_default_export_span ) @@ -2446,16 +2793,13 @@ def mock_initialize(self, **kwargs): # Call original_initialize to set up all the necessary attributes original_initialize(self, **kwargs) - # Now create our custom LangfuseSpanProcessor with the actual blocked_instrumentation_scopes + # Now create our custom LangfuseSpanProcessor with the actual should_export_span from langfuse._client.span_processor import LangfuseSpanProcessor processor = LangfuseSpanProcessor( public_key=self.public_key, secret_key=self.secret_key, base_url=self.base_url, - blocked_instrumentation_scopes=kwargs.get( - "blocked_instrumentation_scopes" - ), should_export_span=kwargs.get("should_export_span"), ) # Replace its exporter with our test exporter @@ -2661,38 +3005,6 @@ def test_custom_should_export_span_with_composition( assert "known-span" in exported_span_names assert "unknown-span" not in exported_span_names - def test_blocked_scopes_override_should_export( - self, instrumentation_filtering_setup - ): - """Test that blocked scopes are dropped even when callback allows all.""" - with pytest.warns(DeprecationWarning, match="blocked_instrumentation_scopes"): - Langfuse( - public_key=instrumentation_filtering_setup["test_key"], - secret_key="test-secret-key", - base_url="http://localhost:3000", - blocked_instrumentation_scopes=["my-framework.worker"], - should_export_span=lambda span: True, - ) - - tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] - blocked_tracer = tracer_provider.get_tracer("my-framework.worker") - allowed_tracer = tracer_provider.get_tracer("custom.allowed") - - blocked_span = blocked_tracer.start_span("blocked-span") - blocked_span.end() - allowed_span = allowed_tracer.start_span("allowed-span") - allowed_span.end() - tracer_provider.force_flush() - - exported_span_names = [ - span.name - for span in instrumentation_filtering_setup[ - "blocked_exporter" - ].get_finished_spans() - ] - assert "blocked-span" not in exported_span_names - assert "allowed-span" in exported_span_names - def test_should_export_span_with_none_uses_default( self, instrumentation_filtering_setup ): @@ -2758,34 +3070,6 @@ def _failing_filter(_span): for record in caplog.records ) - def test_blocked_scope_drop_logs_scope_name( - self, instrumentation_filtering_setup, caplog - ): - """Test that blocked scope drops include scope names in debug logs.""" - caplog.set_level("DEBUG", logger="langfuse") - - with pytest.warns(DeprecationWarning, match="blocked_instrumentation_scopes"): - Langfuse( - public_key=instrumentation_filtering_setup["test_key"], - secret_key="test-secret-key", - base_url="http://localhost:3000", - blocked_instrumentation_scopes=["my.blocked.scope"], - should_export_span=lambda span: True, - ) - - tracer_provider = instrumentation_filtering_setup["test_tracer_provider"] - blocked_tracer = tracer_provider.get_tracer("my.blocked.scope") - - span = blocked_tracer.start_span("blocked-debug-span") - span.end() - tracer_provider.force_flush() - - assert any( - "Dropping span due to blocked instrumentation scope" in record.message - and "my.blocked.scope" in record.message - for record in caplog.records - ) - class TestConcurrencyAndAsync(TestOTelBase): """Tests for asynchronous and concurrent span operations.""" diff --git a/tests/unit/test_patch_generated_score_compat.py b/tests/unit/test_patch_generated_score_compat.py deleted file mode 100644 index 384abc4be..000000000 --- a/tests/unit/test_patch_generated_score_compat.py +++ /dev/null @@ -1,159 +0,0 @@ -from __future__ import annotations - -import subprocess -import sys -import typing -from pathlib import Path -from runpy import run_path -from types import SimpleNamespace - - -def _write_generated_fixture(api_root: Path, *, canonical_has_create: bool) -> None: - scores_dir = api_root / "scores" - legacy_dir = api_root / "legacy" / "score_v1" - legacy_types_dir = legacy_dir / "types" - scores_dir.mkdir(parents=True) - legacy_types_dir.mkdir(parents=True) - - canonical_methods = ( - "\n def create(self):\n pass\n" - "\n async def create(self):\n pass\n" - if canonical_has_create - else "" - ) - canonical_body = canonical_methods or " pass\n" - (scores_dir / "client.py").write_text(f"class ScoresClient:{canonical_body}") - (legacy_dir / "client.py").write_text( - "import typing\n" - "from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper\n\n" - "class ScoreV1Client:\n" - " def delete(self):\n" - " pass\n\n" - "class AsyncScoreV1Client:\n" - " async def delete(self):\n" - " pass\n" - ) - (legacy_dir / "__init__.py").write_text( - "# This file was auto-generated by Fern from our API Definition.\n" - ) - (legacy_dir.parent / "__init__.py").write_text("__all__ = []\n") - (legacy_types_dir / "__init__.py").write_text("__all__ = []\n") - - -def _run_postprocessor(api_root: Path) -> subprocess.CompletedProcess[str]: - return subprocess.run( - [ - sys.executable, - "scripts/patch_generated_score_compat.py", - "--api-root", - str(api_root), - ], - check=False, - capture_output=True, - text=True, - ) - - -def test_postprocessor_adds_delegating_methods_and_type_aliases(tmp_path: Path) -> None: - api_root = tmp_path / "api" - _write_generated_fixture(api_root, canonical_has_create=True) - - result = _run_postprocessor(api_root) - - assert result.returncode == 0, result.stderr - client = (api_root / "legacy" / "score_v1" / "client.py").read_text() - assert "def create(" in client - assert "async def create(" in client - assert "CanonicalScoresClient(" in client - assert "CanonicalAsyncScoresClient(" in client - assert "client.scores.create" in client - compile(client, "", "exec") - - response_alias = ( - api_root / "legacy" / "score_v1" / "types" / "create_score_response.py" - ).read_text() - assert ( - "from ....scores.types.create_score_response import CreateScoreResponse" - in response_alias - ) - assert ( - "CreateScoreResponse" - in (api_root / "legacy" / "score_v1" / "__init__.py").read_text() - ) - legacy_package = (api_root / "legacy" / "score_v1" / "__init__.py").read_text() - assert "__all__ = [" in legacy_package - assert "[*__all__" not in legacy_package - assert "CreateScoreResponse" in (api_root / "legacy" / "__init__.py").read_text() - - second_result = _run_postprocessor(api_root) - assert second_result.returncode == 0, second_result.stderr - assert second_result.stdout.strip() == "No patch needed." - - -def test_postprocessor_fails_when_no_generated_create_exists(tmp_path: Path) -> None: - api_root = tmp_path / "api" - _write_generated_fixture(api_root, canonical_has_create=False) - - result = _run_postprocessor(api_root) - - assert result.returncode != 0 - assert "Neither generated score client defines" in result.stderr - - -def test_generated_sync_alias_delegates_to_canonical_client() -> None: - postprocessor = run_path("scripts/patch_generated_score_compat.py") - sync_create_method = postprocessor["SYNC_CREATE_METHOD"] - calls: dict[str, object] = {} - omitted = object() - - class FakeCanonicalScoresClient: - def __init__(self, *, client_wrapper: object): - calls["client_wrapper"] = client_wrapper - - def create(self, **kwargs: object) -> str: - calls["kwargs"] = kwargs - return "created" - - namespace = { - "CanonicalScoresClient": FakeCanonicalScoresClient, - "CreateScoreResponse": str, - "CreateScoreSource": object, - "CreateScoreValue": object, - "OMIT": omitted, - "RequestOptions": object, - "ScoreDataType": object, - "typing": typing, - } - exec( - "class LegacyScoreClient:\n" - " def __init__(self, client_wrapper):\n" - " self._raw_client = SimpleNamespace(" - "_client_wrapper=client_wrapper)\n" - f"{sync_create_method}", - {**namespace, "SimpleNamespace": SimpleNamespace}, - namespace, - ) - - client_wrapper = object() - legacy_client = namespace["LegacyScoreClient"](client_wrapper) - response = legacy_client.create(name="quality", value=0.9, trace_id="trace-id") - - assert response == "created" - assert calls["client_wrapper"] is client_wrapper - assert calls["kwargs"] == { - "name": "quality", - "value": 0.9, - "id": omitted, - "trace_id": "trace-id", - "session_id": omitted, - "observation_id": omitted, - "dataset_run_id": omitted, - "comment": omitted, - "metadata": omitted, - "environment": omitted, - "queue_id": omitted, - "data_type": omitted, - "config_id": omitted, - "source": omitted, - "request_options": None, - } diff --git a/tests/unit/test_propagate_attributes.py b/tests/unit/test_propagate_attributes.py index cdf4f9351..c1b5947fe 100644 --- a/tests/unit/test_propagate_attributes.py +++ b/tests/unit/test_propagate_attributes.py @@ -6,6 +6,7 @@ """ import concurrent.futures +import json from datetime import datetime import pytest @@ -19,6 +20,14 @@ from tests.unit.test_otel import TestOTelBase +def _assert_json_attribute(span_data, key, expected): + """Assert a metadata attribute is one JSON string, not one attribute per key.""" + attributes = span_data["attributes"] + assert key in attributes, f"Attribute {key} not found in {attributes}" + assert json.loads(attributes[key]) == expected + assert not [k for k in attributes if k.startswith(f"{key}.")] + + class TestPropagateAttributesBase(TestOTelBase): """Base class for propagate_attributes tests with shared helper methods.""" @@ -147,24 +156,24 @@ def test_metadata_propagates_to_child_spans(self, langfuse_client, memory_export self.verify_span_attribute( child1_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", - "variant_a", + '"variant_a"', ) self.verify_span_attribute( child1_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", - "1.0", + '"1.0"', ) child2_span = self.get_span_by_name(memory_exporter, "child-span-2") self.verify_span_attribute( child2_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", - "variant_a", + '"variant_a"', ) self.verify_span_attribute( child2_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", - "1.0", + '"1.0"', ) def test_all_attributes_propagate_together(self, langfuse_client, memory_exporter): @@ -189,12 +198,12 @@ def test_all_attributes_propagate_together(self, langfuse_client, memory_exporte self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", - "test", + '"test"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "prod", + '"prod"', ) @@ -461,10 +470,10 @@ def test_non_string_user_id_dropped(self, langfuse_client, memory_exporter): child_span, LangfuseOtelSpanAttributes.TRACE_USER_ID ) - def test_non_string_metadata_values_coerced( + def test_non_string_metadata_values_json_serialized( self, langfuse_client, memory_exporter, caplog ): - """Verify non-string metadata values are coerced instead of dropped.""" + """Verify non-string metadata values are JSON-serialized instead of dropped.""" caplog.set_level("WARNING", logger="langfuse") metadata = { @@ -472,6 +481,21 @@ def test_non_string_metadata_values_coerced( "langgraph_triggers": ["branch:agent"], "langgraph_path": ("root", "agent"), "max_search_results": 5, + "is_cached": True, + "ratio": 0.5, + "config": {"model": "gpt-4o", "nested": {"b": [1, None], "a": "ü"}}, + "label": ["Läufe", "🚀"], + } + # Byte-identical to JSON.stringify in the JS SDK for the same values. + expected = { + "langgraph_step": "1", + "langgraph_triggers": '["branch:agent"]', + "langgraph_path": '["root","agent"]', + "max_search_results": "5", + "is_cached": "true", + "ratio": "0.5", + "config": '{"model":"gpt-4o","nested":{"b":[1,null],"a":"ü"}}', + "label": '["Läufe","🚀"]', } with langfuse_client.start_as_current_observation(name="parent-span"): @@ -481,15 +505,170 @@ def test_non_string_metadata_values_coerced( child_span = self.get_span_by_name(memory_exporter, "child-span") - for key, value in metadata.items(): + for key, value in expected.items(): self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.{key}", - str(value), + value, ) assert "value is not a string. Dropping value." not in caplog.text + def test_none_metadata_value_is_dropped_with_warning( + self, langfuse_client, memory_exporter, caplog + ): + """Verify None values are dropped and don't overwrite an outer value.""" + caplog.set_level("WARNING", logger="langfuse") + prefix = LangfuseOtelSpanAttributes.TRACE_METADATA + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"outer": "kept"}): + with propagate_attributes( + metadata={"outer": None, "empty": None, "other": "x"} + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute(child_span, f"{prefix}.outer", '"kept"') + self.verify_span_attribute(child_span, f"{prefix}.other", '"x"') + self.verify_missing_attribute(child_span, f"{prefix}.empty") + assert ( + "Propagated attribute 'metadata.empty' is None. Dropping value." + in caplog.text + ) + assert ( + "Propagated attribute 'metadata.outer' is None. Dropping value." + in caplog.text + ) + + def test_large_integer_metadata_becomes_json_string( + self, langfuse_client, memory_exporter + ): + """Verify integers beyond JS's safe range become JSON strings, like observation metadata.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata={"snowflake_id": 9007199254740993}): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.snowflake_id", + '"9007199254740993"', + ) + + def test_nested_large_integer_metadata_becomes_json_string( + self, langfuse_client, memory_exporter + ): + """Verify nested integers beyond JS's safe range become JSON strings.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "ids": [9007199254740993, 1], + "ref": {"snowflake_id": 9007199254740993}, + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.ids", + '["9007199254740993",1]', + ) + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.ref", + '{"snowflake_id":"9007199254740993"}', + ) + + def test_non_finite_number_metadata_becomes_json_string( + self, langfuse_client, memory_exporter + ): + """Verify NaN and Infinity become JSON strings, like observation metadata.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={ + "kept": 1.5, + "nan": float("nan"), + "inf": float("-inf"), + "nested": {"scores": [1.0, float("inf")]}, + } + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + prefix = LangfuseOtelSpanAttributes.TRACE_METADATA + self.verify_span_attribute(child_span, f"{prefix}.kept", "1.5") + self.verify_span_attribute(child_span, f"{prefix}.nan", '"NaN"') + self.verify_span_attribute(child_span, f"{prefix}.inf", '"-Infinity"') + self.verify_span_attribute( + child_span, f"{prefix}.nested", '{"scores":[1.0,"Infinity"]}' + ) + + def test_string_metadata_is_json_encoded_like_observation_metadata( + self, langfuse_client, memory_exporter + ): + """Verify strings are JSON-encoded so "123" stays a string on the server.""" + metadata = { + "plain": "prod", + "numeric_string": "123", + "bool_string": "true", + "null_string": "null", + "json_string": '{"a":1}', + "umlaut": "ü", + } + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes(metadata=metadata): + child = langfuse_client.start_observation( + name="child-span", metadata=metadata + ) + child.end() + + attributes = self.get_span_by_name(memory_exporter, "child-span")["attributes"] + for key, value in metadata.items(): + trace_value = attributes[ + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.{key}" + ] + assert ( + trace_value + == attributes[ + f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.{key}" + ] + ) + assert json.loads(trace_value) == value + + assert ( + attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.numeric_string"] + == '"123"' + ) + assert ( + attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.umlaut"] == '"ü"' + ) + + def test_metadata_length_limit_applies_after_json_encoding( + self, langfuse_client, memory_exporter + ): + """Verify the 200 character limit counts the quotes added by JSON encoding.""" + with langfuse_client.start_as_current_observation(name="parent-span"): + with propagate_attributes( + metadata={"fits": "a" * 198, "too_long": "b" * 199} + ): + child = langfuse_client.start_observation(name="child-span") + child.end() + + child_span = self.get_span_by_name(memory_exporter, "child-span") + self.verify_span_attribute( + child_span, + f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.fits", + '"' + "a" * 198 + '"', + ) + self.verify_missing_attribute( + child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.too_long" + ) + def test_mixed_valid_invalid_metadata(self, langfuse_client, memory_exporter): """Verify mixed valid/invalid metadata - valid entries kept, invalid dropped.""" with langfuse_client.start_as_current_observation(name="parent-span"): @@ -508,12 +687,12 @@ def test_mixed_valid_invalid_metadata(self, langfuse_client, memory_exporter): self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.valid_key", - "valid_value", + '"valid_value"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.another_valid", - "ok", + '"ok"', ) self.verify_missing_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.invalid_key" @@ -622,12 +801,12 @@ def test_nested_metadata_merges_additively(self, langfuse_client, memory_exporte self.verify_span_attribute( outer_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "prod", + '"prod"', ) self.verify_span_attribute( outer_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-east", + '"us-east"', ) self.verify_missing_attribute( outer_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment" @@ -638,22 +817,22 @@ def test_nested_metadata_merges_additively(self, langfuse_client, memory_exporte self.verify_span_attribute( inner_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "prod", + '"prod"', ) self.verify_span_attribute( inner_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-east", + '"us-east"', ) self.verify_span_attribute( inner_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", - "A", + '"A"', ) self.verify_span_attribute( inner_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", - "2.0", + '"2.0"', ) # Verify: after span has only outer metadata (inner context exited) @@ -661,12 +840,12 @@ def test_nested_metadata_merges_additively(self, langfuse_client, memory_exporte self.verify_span_attribute( after_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "prod", + '"prod"', ) self.verify_span_attribute( after_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-east", + '"us-east"', ) self.verify_missing_attribute( after_span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment" @@ -694,26 +873,26 @@ def test_nested_metadata_inner_overwrites_conflicting_keys( self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "production", # Inner value wins + '"production"', # Inner value wins ) # Preserved keys from outer self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", - "1.0", # From outer + '"1.0"', # From outer ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-west", # From outer + '"us-west"', # From outer ) # New key from inner self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", - "B", # From inner + '"B"', # From inner ) def test_triple_nested_metadata_accumulates(self, langfuse_client, memory_exporter): @@ -732,24 +911,24 @@ def test_triple_nested_metadata_accumulates(self, langfuse_client, memory_export self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.level", - "3", + '"3"', ) # Unique keys from each level self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.a", - "outer", + '"outer"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.b", - "middle", + '"middle"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.c", - "inner", + '"inner"', ) def test_metadata_merge_with_empty_inner(self, langfuse_client, memory_exporter): @@ -766,12 +945,12 @@ def test_metadata_merge_with_empty_inner(self, langfuse_client, memory_exporter) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key1", - "value1", + '"value1"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.key2", - "value2", + '"value2"', ) def test_metadata_merge_preserves_user_session( @@ -799,12 +978,12 @@ def test_metadata_merge_preserves_user_session( self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.outer", - "value", + '"value"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.inner", - "value", + '"value"', ) @@ -955,9 +1134,9 @@ def test_multiple_metadata_keys_independent(self, langfuse_client, memory_export attributes = child_span["attributes"] # Verify all three are separate attributes with correct values - assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k1"] == "v1" - assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k2"] == "v2" - assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k3"] == "v3" + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k1"] == '"v1"' + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k2"] == '"v2"' + assert attributes[f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.k3"] == '"v3"' class TestPropagateAttributesThreading(TestPropagateAttributesBase): @@ -1266,7 +1445,7 @@ def test_nested_spans_from_multiple_tracers( self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment", - "cross_tracer", + '"cross_tracer"', ) def test_other_tracer_span_before_propagate_context( @@ -1326,17 +1505,17 @@ def test_mixed_tracers_with_metadata( self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "production", + '"production"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.version", - "2.0", + '"2.0"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.feature_flag", - "enabled", + '"enabled"', ) def test_propagate_without_langfuse_parent( @@ -1460,7 +1639,7 @@ async def level_1(): self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.level", - "nested", + '"nested"', ) @pytest.mark.asyncio @@ -1590,12 +1769,12 @@ async def async_with_metadata(): self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.async", - "true", + '"true"', ) self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.operation", - "test", + '"test"', ) @@ -1626,10 +1805,10 @@ def test_baggage_is_set_when_as_baggage_true(self, langfuse_client): assert baggage_entries["langfuse_session_id"] == "session_abc" assert "langfuse_metadata_env" in baggage_entries - assert baggage_entries["langfuse_metadata_env"] == "test" + assert baggage_entries["langfuse_metadata_env"] == '"test"' assert "langfuse_metadata_version" in baggage_entries - assert baggage_entries["langfuse_metadata_version"] == "2.0" + assert baggage_entries["langfuse_metadata_version"] == '"2.0"' def test_spans_receive_attributes_from_baggage( self, langfuse_client, memory_exporter @@ -1659,7 +1838,7 @@ def test_spans_receive_attributes_from_baggage( self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.source", - "baggage", + '"baggage"', ) def test_baggage_disabled_by_default(self, langfuse_client): @@ -1708,12 +1887,12 @@ def test_metadata_key_with_user_id_substring_doesnt_collide( self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.user_info", - "some_data", + '"some_data"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.user_id_copy", - "another", + '"another"', ) def test_metadata_key_with_session_substring_doesnt_collide( @@ -1739,12 +1918,12 @@ def test_metadata_key_with_session_substring_doesnt_collide( self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.session_data", - "value1", + '"value1"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.session_id_backup", - "value2", + '"value2"', ) def test_metadata_keys_extract_correctly_from_baggage( @@ -1769,17 +1948,17 @@ def test_metadata_keys_extract_correctly_from_baggage( self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "production", + '"production"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-west", + '"us-west"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.experiment_id", - "exp_123", + '"exp_123"', ) def test_baggage_and_context_both_propagate(self, langfuse_client, memory_exporter): @@ -1809,7 +1988,7 @@ def test_baggage_and_context_both_propagate(self, langfuse_client, memory_export self.verify_span_attribute( span_data, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.source", - "both", + '"both"', ) def test_baggage_survives_context_isolation(self, langfuse_client, memory_exporter): @@ -1854,7 +2033,7 @@ class TestPropagateAttributesEnvironment(TestPropagateAttributesBase): """Tests for first-class Langfuse environment propagation.""" def _capture_score_events(self, monkeypatch, langfuse_client): - """Capture score ingestion events before they reach the background queue.""" + """Capture scores before they reach the background queue.""" score_events = [] assert langfuse_client._resources is not None @@ -1971,7 +2150,7 @@ def test_span_score_uses_propagated_environment(self, monkeypatch, langfuse_clie span.score(name="quality", value=1.0, data_type="NUMERIC") assert len(score_events) == 1 - assert score_events[0]["body"].environment == "dev" + assert score_events[0]["environment"] == "dev" def test_span_score_trace_uses_propagated_environment( self, monkeypatch, langfuse_client @@ -1987,7 +2166,7 @@ def test_span_score_trace_uses_propagated_environment( span.score_trace(name="overall-quality", value=0.95) assert len(score_events) == 1 - assert score_events[0]["body"].environment == "staging" + assert score_events[0]["environment"] == "staging" def test_current_score_helpers_use_propagated_environment( self, monkeypatch, langfuse_client @@ -2005,7 +2184,7 @@ def test_current_score_helpers_use_propagated_environment( name="trace-quality", value=0.8, data_type="NUMERIC" ) - assert [event["body"].environment for event in score_events] == ["qa", "qa"] + assert [score["environment"] for score in score_events] == ["qa", "qa"] def test_environment_exactly_40_chars_is_accepted( self, langfuse_client, memory_exporter @@ -2119,12 +2298,12 @@ def test_version_with_metadata(self, langfuse_client, memory_exporter): self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "production", + '"production"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-east", + '"us-east"', ) def test_version_validation_over_200_chars(self, langfuse_client, memory_exporter): @@ -2398,7 +2577,7 @@ def test_tags_with_metadata(self, langfuse_client, memory_exporter): self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "staging", + '"staging"', ) def test_tags_validation_with_invalid_tag(self, langfuse_client, memory_exporter): @@ -2632,22 +2811,16 @@ def task_with_child_spans(*, item, **kwargs): LangfuseOtelSpanAttributes.EXPERIMENT_NAME, result.run_name, ) - for metadata_key, metadata_value in experiment_metadata.items(): - self.verify_span_attribute( - first_root, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", - metadata_value, - ) - - self.verify_span_attribute( + _assert_json_attribute( first_root, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.item_type", - "test", + LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + experiment_metadata, ) - self.verify_span_attribute( + + _assert_json_attribute( first_root, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.priority", - "high", + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA, + {"item_type": "test", "priority": "high"}, ) # Environment should be set to sdk-experiment @@ -2682,12 +2855,11 @@ def task_with_child_spans(*, item, **kwargs): LangfuseOtelSpanAttributes.EXPERIMENT_NAME, result.run_name, ) - for metadata_key, metadata_value in experiment_metadata.items(): - self.verify_span_attribute( - child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", - metadata_value, - ) + _assert_json_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + experiment_metadata, + ) self.verify_span_attribute( child_span, LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ID, @@ -2769,45 +2941,26 @@ def test_experiment_run_metadata_overrides_item_metadata( self.verify_span_attribute( span, f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.shared", - "run", + '"run"', ) self.verify_span_attribute( span, f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.experiment_run_name", - "run-name", + '"run-name"', ) self.verify_span_attribute( span, f"{LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.item_only", - "yes", + '"yes"', ) def test_experiment_attributes_propagate_with_dataset( self, langfuse_client, memory_exporter, monkeypatch ): """Test experiment attribute propagation with Langfuse dataset.""" - created_run_items = [] - - # Mock the sync API used by run_experiment to create dataset run items - def mock_create_dataset_run_item(*args, **kwargs): - from langfuse.api import DatasetRunItem - - created_run_items.append(kwargs) - return DatasetRunItem( - id="mock-run-item-id", - dataset_run_id="mock-dataset-run-id-123", - dataset_run_name=kwargs.get("run_name", "Dataset Test"), - dataset_item_id=kwargs.get("dataset_item_id", "mock-item-id"), - trace_id="mock-trace-id", - observation_id=kwargs.get("observation_id"), - created_at=datetime.now(), - updated_at=datetime.now(), - ) monkeypatch.setattr( - langfuse_client.api.dataset_run_items, - "create", - mock_create_dataset_run_item, + langfuse_client, "_get_project_id", lambda: "test-project-id" ) # Create a mock dataset with items @@ -2872,9 +3025,16 @@ def task_with_children(*, item, **kwargs): assert len(root_spans) >= 1, "Should have at least 1 root span" first_root = root_spans[0] task_span = self.get_span_by_name(memory_exporter, "experiment-item-task") - assert result.experiment_id == "mock-dataset-run-id-123" - assert len(created_run_items) == 1 - assert created_run_items[0]["observation_id"] == task_span["span_id"] + assert result.experiment_id == langfuse_client._create_experiment_id( + project_id="test-project-id", + dataset_id=dataset_id, + run_name=result.run_name, + ) + self.verify_span_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_ROOT_OBSERVATION_ID, + task_span["span_id"], + ) # Root-only attributes should be on root self.verify_span_attribute( @@ -2908,12 +3068,11 @@ def task_with_children(*, item, **kwargs): ) # Should have experiment metadata - for metadata_key, metadata_value in experiment_metadata.items(): - self.verify_span_attribute( - first_root, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", - metadata_value, - ) + _assert_json_attribute( + first_root, + LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + experiment_metadata, + ) # Environment should be set to sdk-experiment self.verify_span_attribute( @@ -2945,23 +3104,17 @@ def task_with_children(*, item, **kwargs): ) # Experiment metadata should be propagated - for metadata_key, metadata_value in experiment_metadata.items(): - self.verify_span_attribute( - child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", - metadata_value, - ) - - # Item metadata should be propagated - self.verify_span_attribute( + _assert_json_attribute( child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.source", - "dataset", + LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + experiment_metadata, ) - self.verify_span_attribute( + + # Item metadata should be propagated + _assert_json_attribute( child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.index", - "0", + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA, + {"source": "dataset", "index": 0}, ) # Environment should be propagated to children @@ -3066,8 +3219,6 @@ def task_with_nested_spans(*, item, **kwargs): def test_experiment_metadata_merging(self, langfuse_client, memory_exporter): """Test that experiment metadata and item metadata are both propagated correctly.""" - from langfuse._client.attributes import _serialize - # Rich metadata experiment_metadata = { "experiment_type": "A/B test", @@ -3114,21 +3265,19 @@ def task_with_child(*, item, **kwargs): # Verify child span has both experiment and item metadata propagated child_span = self.get_span_by_name(memory_exporter, "metadata-child") - # Verify experiment metadata is flattened and propagated - for metadata_key, metadata_value in experiment_metadata.items(): - self.verify_span_attribute( - child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", - _serialize(metadata_value), - ) + # Verify experiment metadata is propagated as one JSON attribute + _assert_json_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + experiment_metadata, + ) - # Verify item metadata is flattened and propagated - for metadata_key, metadata_value in item_metadata.items(): - self.verify_span_attribute( - child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA}.{metadata_key}", - _serialize(metadata_value), - ) + # Verify item metadata is propagated as one JSON attribute + _assert_json_attribute( + child_span, + LangfuseOtelSpanAttributes.EXPERIMENT_ITEM_METADATA, + item_metadata, + ) # Verify environment is propagated to child self.verify_span_attribute( @@ -3140,7 +3289,7 @@ def task_with_child(*, item, **kwargs): def test_experiment_metadata_values_are_validated_individually( self, langfuse_client, memory_exporter, caplog ): - """Experiment metadata is flattened so large combined dicts still propagate.""" + """Experiment metadata is one JSON attribute that skips the 200-char limit.""" caplog.set_level("WARNING", logger="langfuse") @@ -3168,17 +3317,12 @@ def task_with_child(*, item, **kwargs): child_span = self.get_span_by_name(memory_exporter, "large-metadata-child") - for metadata_key, metadata_value in experiment_metadata.items(): - self.verify_span_attribute( - child_span, - f"{LangfuseOtelSpanAttributes.EXPERIMENT_METADATA}.{metadata_key}", - metadata_value, - ) - - self.verify_missing_attribute( + _assert_json_attribute( child_span, LangfuseOtelSpanAttributes.EXPERIMENT_METADATA, + experiment_metadata, ) + assert "experiment_metadata' value is over 200 characters" not in caplog.text @@ -3292,12 +3436,12 @@ def test_trace_name_with_metadata(self, langfuse_client, memory_exporter): self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.env", - "production", + '"production"', ) self.verify_span_attribute( child_span, f"{LangfuseOtelSpanAttributes.TRACE_METADATA}.region", - "us-east", + '"us-east"', ) def test_trace_name_validation_over_200_chars( diff --git a/tests/unit/test_resource_manager.py b/tests/unit/test_resource_manager.py index f66a1e052..519b21e71 100644 --- a/tests/unit/test_resource_manager.py +++ b/tests/unit/test_resource_manager.py @@ -1,5 +1,6 @@ """Test the LangfuseResourceManager and get_client() function.""" +import logging from queue import Queue from types import SimpleNamespace from typing import Sequence @@ -13,7 +14,7 @@ from langfuse._client.resource_manager import LangfuseResourceManager from langfuse._task_manager.media_manager import MediaManager from langfuse._task_manager.media_upload_consumer import MediaUploadConsumer -from langfuse._task_manager.score_ingestion_consumer import ScoreIngestionConsumer +from langfuse._task_manager.score_consumer import ScoreConsumer from langfuse.types import MaskOtelSpansResult @@ -140,12 +141,11 @@ def should_export_b(span): client_b.shutdown() -def test_score_ingestion_consumer_pause_wakes_blocked_thread(): - consumer = ScoreIngestionConsumer( - ingestion_queue=Queue(), +def test_score_consumer_pause_wakes_blocked_thread(): + consumer = ScoreConsumer( + score_queue=Queue(), identifier=0, - client=Mock(), - public_key="pk-test", + api_client=Mock(), flush_interval=30, ) @@ -187,20 +187,20 @@ def test_at_fork_reinit_creates_new_queues_and_consumers(monkeypatch): rm = client._resources assert rm is not None - old_score_queue = rm._score_ingestion_queue + old_score_queue = rm._score_queue old_media_queue = rm._media_upload_queue - old_ingestion_consumers = list(rm._ingestion_consumers) + old_score_consumers = list(rm._score_consumers) rm._at_fork_reinit() - assert rm._score_ingestion_queue is not old_score_queue + assert rm._score_queue is not old_score_queue assert rm._media_upload_queue is not old_media_queue - assert len(rm._ingestion_consumers) == 1 - assert rm._ingestion_consumers[0].is_alive() + assert len(rm._score_consumers) == 1 + assert rm._score_consumers[0].is_alive() # In a real fork, old threads don't exist in the child process. # In this unit test they do — stop them explicitly to avoid leaking threads. - for consumer in old_ingestion_consumers: + for consumer in old_score_consumers: consumer.pause() consumer.join(timeout=1.0) @@ -222,13 +222,13 @@ def test_at_fork_reinit_skips_when_shutdown(monkeypatch): rm = client._resources assert rm is not None - old_score_queue = rm._score_ingestion_queue + old_score_queue = rm._score_queue old_lock = LangfuseResourceManager._lock rm._shutdown = True rm._at_fork_reinit() - assert rm._score_ingestion_queue is old_score_queue # queue must not be replaced + assert rm._score_queue is old_score_queue # queue must not be replaced # The class-level lock is shared state the child still needs (e.g. to create # a new client), so it must be replaced even when this instance is shut down. assert LangfuseResourceManager._lock is not old_lock @@ -330,7 +330,6 @@ def test_at_fork_reinit_recreates_httpx_client_by_default(monkeypatch): old_httpx_client = rm.httpx_client old_api = rm.api old_async_api = rm.async_api - old_score_ingestion_client = rm._score_ingestion_client assert client.api is old_api assert client.async_api is old_async_api @@ -343,7 +342,7 @@ def test_at_fork_reinit_recreates_httpx_client_by_default(monkeypatch): assert client.api is not old_api assert client.async_api is rm.async_api assert client.async_api is not old_async_api - assert rm._score_ingestion_client is not old_score_ingestion_client + assert rm._score_consumers[0]._api_client is rm.api client.shutdown() @@ -376,7 +375,7 @@ def test_at_fork_reinit_preserves_custom_httpx_client(monkeypatch): assert rm.httpx_client is custom_client assert rm.api is not None assert rm.async_api is not None - assert rm._score_ingestion_client is not None + assert rm._score_consumers[0]._api_client is rm.api custom_client.close() client.shutdown() @@ -410,6 +409,36 @@ def test_at_fork_reinit_new_httpx_client_uses_configured_timeout_and_headers( client.shutdown() +def test_create_score_with_invalid_input_logs_error_without_enqueueing( + monkeypatch, caplog +): + monkeypatch.setenv("LANGFUSE_MEDIA_UPLOAD_ENABLED", "false") + + with LangfuseResourceManager._lock: + LangfuseResourceManager._instances.clear() + + client = Langfuse( + public_key="pk-invalid-score", + secret_key="sk-invalid-score", + span_exporter=NoOpSpanExporter(), + ) + rm = client._resources + assert rm is not None + enqueued = [] + monkeypatch.setattr(rm, "add_score_task", lambda event, **_: enqueued.append(event)) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + client.create_score(name="invalid", value=object(), trace_id="a" * 32) + + assert enqueued == [] + assert rm._score_queue.empty() + error_records = [r for r in caplog.records if r.levelno == logging.ERROR] + assert len(error_records) == 1 + assert "Error creating score" in error_records[0].getMessage() + + client.shutdown() + + def test_stop_and_join_consumer_threads_broadcasts_media_shutdown_after_pausing_all(): events = [] @@ -429,7 +458,7 @@ def signal_shutdown(self, *, count): fake_resource_manager = SimpleNamespace( _media_upload_consumers=[FakeConsumer(0), FakeConsumer(1)], - _ingestion_consumers=[], + _score_consumers=[], _media_manager=FakeMediaManager(), ) diff --git a/tests/unit/test_score_consumer.py b/tests/unit/test_score_consumer.py new file mode 100644 index 000000000..a29adc535 --- /dev/null +++ b/tests/unit/test_score_consumer.py @@ -0,0 +1,416 @@ +"""Delivery of queued scores to POST /api/public/scores.""" + +import json +import logging +import threading +import time +from datetime import datetime, timezone +from queue import Queue +from typing import Any, Callable, Dict, List, Sequence + +import httpx +import pytest +from opentelemetry.sdk.trace import ReadableSpan, TracerProvider +from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult +from opentelemetry.sdk.trace.sampling import ALWAYS_OFF, ParentBased, TraceIdRatioBased + +from langfuse import Langfuse +from langfuse._client.resource_manager import LangfuseResourceManager +from langfuse._task_manager import score_consumer as score_consumer_module +from langfuse._task_manager.score_consumer import ScoreConsumer +from langfuse.api import LangfuseAPI + +ACCEPTED = (202, {"message": "Accepted"}) + + +class NoOpSpanExporter(SpanExporter): + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + return SpanExportResult.SUCCESS + + def shutdown(self) -> None: + pass + + +Responder = Callable[[httpx.Request], httpx.Response] + + +def _responder(*responses: Any) -> Responder: + """Answer requests in order; an exception instance is raised instead.""" + remaining = list(responses) + + def respond(request: httpx.Request) -> httpx.Response: + response = remaining.pop(0) if len(remaining) > 1 else remaining[0] + if isinstance(response, Exception): + raise response + status, body = response + return httpx.Response(status, json=body) + + return respond + + +class Recorder: + def __init__(self, respond: Responder) -> None: + self.requests: List[httpx.Request] = [] + self._respond = respond + + def __call__(self, request: httpx.Request) -> httpx.Response: + self.requests.append(request) + return self._respond(request) + + @property + def bodies(self) -> List[Any]: + return [json.loads(request.content) for request in self.requests] + + +def _score(name: str, **fields: Any) -> Dict[str, Any]: + return { + "id": f"id-{name}", + "traceId": "a" * 32, + "name": name, + "value": 1.0, + **fields, + } + + +@pytest.fixture(autouse=True) +def no_backoff_sleep(monkeypatch): + monkeypatch.setattr("backoff._sync.time.sleep", lambda _: None) + + +def _consumer(recorder: Recorder, queue: Queue, **kwargs: Any) -> ScoreConsumer: + api = LangfuseAPI( + base_url="http://langfuse.test", + username="pk", + password="sk", + httpx_client=httpx.Client(transport=httpx.MockTransport(recorder)), + ) + kwargs.setdefault("flush_interval", 0.05) + return ScoreConsumer(score_queue=queue, identifier=0, api_client=api, **kwargs) + + +def _drain(consumer: ScoreConsumer, queue: Queue) -> None: + while queue.unfinished_tasks: + consumer.upload() + + +def test_sends_queued_scores_as_one_list(caplog): + recorder = Recorder(_responder(ACCEPTED)) + queue: Queue = Queue() + for name in ("a", "b", "c"): + queue.put(_score(name)) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue), queue) + + assert [r.url.path for r in recorder.requests] == ["/api/public/scores"] + assert [score["name"] for score in recorder.bodies[0]] == ["a", "b", "c"] + assert caplog.records == [] + + +def test_splits_batches_at_flush_at(): + recorder = Recorder(_responder(ACCEPTED)) + queue: Queue = Queue() + for name in ("a", "b", "c"): + queue.put(_score(name)) + + _drain(_consumer(recorder, queue, flush_at=2), queue) + + assert [[s["name"] for s in body] for body in recorder.bodies] == [ + ["a", "b"], + ["c"], + ] + + +def test_splits_batches_at_byte_limit_without_reordering(monkeypatch): + recorder = Recorder(_responder(ACCEPTED)) + queue: Queue = Queue() + scores = [_score(name, comment="x" * 100) for name in ("a", "b", "c")] + for score in scores: + queue.put(score) + item_size = len(json.dumps(scores[0]).encode()) + # Room for two items, their separator and the list brackets, not three. + monkeypatch.setattr( + score_consumer_module, "MAX_BATCH_SIZE_BYTES", 2 * item_size + 3 + ) + + _drain(_consumer(recorder, queue), queue) + + assert [[s["name"] for s in body] for body in recorder.bodies] == [ + ["a", "b"], + ["c"], + ] + assert all(len(r.content) <= 2 * item_size + 3 for r in recorder.requests) + + +def test_drops_score_larger_than_byte_limit(monkeypatch, caplog): + recorder = Recorder(_responder(ACCEPTED)) + queue: Queue = Queue() + queue.put(_score("huge", comment="x" * 500)) + queue.put(_score("small")) + monkeypatch.setattr(score_consumer_module, "MAX_BATCH_SIZE_BYTES", 200) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue), queue) + + assert [[s["name"] for s in body] for body in recorder.bodies] == [["small"]] + assert "huge" in caplog.text + + +def test_partial_success_is_logged_and_not_retried(caplog): + recorder = Recorder( + _responder( + ( + 207, + { + "accepted": 1, + "rejected": 1, + "errors": [{"message": "b: value must be numeric"}], + }, + ) + ) + ) + queue: Queue = Queue() + queue.put(_score("a")) + queue.put(_score("b", value="not-a-number")) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue), queue) + + assert len(recorder.requests) == 1 + assert "accepted 1 and rejected 1 of 2 scores" in caplog.text + assert "b: value must be numeric" in caplog.text + + +def test_single_score_response_is_accepted(caplog): + recorder = Recorder(_responder((200, {"id": "id-a"}))) + queue: Queue = Queue() + queue.put(_score("a")) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue), queue) + + assert len(recorder.requests) == 1 + assert caplog.records == [] + + +@pytest.mark.parametrize( + "failure", + [ + pytest.param((500, {"message": "boom"}), id="server-error"), + pytest.param((429, {"message": "slow down"}), id="rate-limited"), + pytest.param(httpx.ConnectError("connection refused"), id="connect-error"), + pytest.param(httpx.ReadTimeout("timed out"), id="timeout"), + ], +) +def test_retries_transient_failures(failure, caplog): + recorder = Recorder(_responder(failure, ACCEPTED)) + queue: Queue = Queue() + queue.put(_score("a")) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue), queue) + + assert len(recorder.requests) == 2 + assert recorder.bodies[0] == recorder.bodies[1] + assert caplog.records == [] + + +def test_gives_up_after_max_retries(caplog): + recorder = Recorder(_responder((503, {"message": "unavailable"}))) + queue: Queue = Queue() + queue.put(_score("a")) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue, max_retries=3), queue) + + assert len(recorder.requests) == 3 + assert queue.unfinished_tasks == 0 + assert "Service unavailable" in caplog.text + + +@pytest.mark.parametrize("status", [400, 401, 403, 404]) +def test_does_not_retry_client_errors(status, caplog): + recorder = Recorder(_responder((status, {"message": "nope"}))) + queue: Queue = Queue() + queue.put(_score("a")) + + with caplog.at_level(logging.ERROR, logger="langfuse"): + _drain(_consumer(recorder, queue), queue) + + assert len(recorder.requests) == 1 + assert queue.unfinished_tasks == 0 + assert caplog.records + + +def test_pause_sends_carried_over_score(monkeypatch): + first_request_started = threading.Event() + release_first_request = threading.Event() + accept = _responder(ACCEPTED) + + def respond(request: httpx.Request) -> httpx.Response: + if not first_request_started.is_set(): + first_request_started.set() + release_first_request.wait(timeout=5) + return accept(request) + + recorder = Recorder(respond) + queue: Queue = Queue() + scores = [_score(name) for name in ("a", "b")] + for score in scores: + queue.put(score) + monkeypatch.setattr( + score_consumer_module, + "MAX_BATCH_SIZE_BYTES", + len(json.dumps(scores[0]).encode()) + 2, + ) + consumer = _consumer(recorder, queue) + + consumer.start() + assert first_request_started.wait(timeout=5) + # "b" did not fit into the in-flight batch and waits as carry-over. + consumer.pause() + release_first_request.set() + consumer.join(timeout=5) + + assert not consumer.is_alive() + assert [[s["name"] for s in body] for body in recorder.bodies] == [["a"], ["b"]] + + +def _sampled_provider(rate: float) -> TracerProvider: + # An explicit provider, because the global one is set once per process. + return TracerProvider(sampler=TraceIdRatioBased(rate)) + + +@pytest.fixture +def client_factory(): + clients: List[Langfuse] = [] + + def create(recorder: Recorder, **kwargs: Any) -> Langfuse: + with LangfuseResourceManager._lock: + LangfuseResourceManager._instances.clear() + client = Langfuse( + public_key="pk-score-consumer", + secret_key="sk-score-consumer", + base_url="http://langfuse.test", + httpx_client=httpx.Client(transport=httpx.MockTransport(recorder)), + span_exporter=NoOpSpanExporter(), + **kwargs, + ) + clients.append(client) + return client + + yield create + + for client in clients: + client.shutdown() + with LangfuseResourceManager._lock: + LangfuseResourceManager._instances.clear() + + +def test_create_score_posts_to_scores_endpoint(client_factory): + recorder = Recorder(_responder(ACCEPTED)) + client = client_factory(recorder, environment="staging") + timestamp = datetime(2026, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + + client.create_score( + score_id="score-1", + name="accuracy", + value=0.9, + trace_id="a" * 32, + observation_id="b" * 16, + data_type="NUMERIC", + comment="fine", + config_id="config-1", + metadata="not a dict", + timestamp=timestamp, + ) + client.create_score(name="session-score", value="good", session_id="session-1") + client.flush() + + assert {r.url.path for r in recorder.requests} == {"/api/public/scores"} + sent = [score for body in recorder.bodies for score in body] + assert sent[0] == { + "id": "score-1", + "traceId": "a" * 32, + "observationId": "b" * 16, + "name": "accuracy", + "value": 0.9, + "dataType": "NUMERIC", + "comment": "fine", + "configId": "config-1", + "environment": "staging", + "metadata": "not a dict", + "timestamp": "2026-01-02T03:04:05Z", + } + assert sent[1]["sessionId"] == "session-1" + assert sent[1]["name"] == "session-score" + assert "traceId" not in sent[1] + assert sent[1]["timestamp"].endswith("Z") + + +@pytest.fixture +def non_utc_local_timezone(monkeypatch): + # Makes treating naive values as local time observable. + monkeypatch.setenv("TZ", "America/New_York") + time.tzset() + yield + monkeypatch.undo() + time.tzset() + + +def test_naive_score_timestamp_is_sent_as_utc(client_factory, non_utc_local_timezone): + recorder = Recorder(_responder(ACCEPTED)) + client = client_factory(recorder) + + client.create_score( + name="naive", + value=1.0, + session_id="session-1", + timestamp=datetime(2026, 1, 2, 3, 4, 5), + ) + client.flush() + + sent = [score for body in recorder.bodies for score in body] + assert sent[0]["timestamp"] == "2026-01-02T03:04:05Z" + + +def test_scores_follow_trace_sampling(client_factory): + recorder = Recorder(_responder(ACCEPTED)) + client = client_factory(recorder, tracer_provider=_sampled_provider(0.5)) + + # The sampler keeps a trace when its low 64 bits fall below rate * 2^64. + client.create_score(name="sampled", value=1.0, trace_id="0" * 32) + client.create_score(name="dropped", value=1.0, trace_id="f" * 32) + client.create_score(name="session", value=1.0, session_id="session-1") + client.create_score(name="dataset-run", value=1.0, dataset_run_id="run-1") + client.flush() + + sent = [score["name"] for body in recorder.bodies for score in body] + assert sent == ["sampled", "session", "dataset-run"] + + +def test_scores_for_sampled_out_traces_are_dropped(client_factory): + recorder = Recorder(_responder(ACCEPTED)) + client = client_factory(recorder, tracer_provider=_sampled_provider(0.0)) + + client.create_score(name="dropped", value=1.0, trace_id="0" * 32) + with client.start_as_current_observation(name="span") as span: + span.score(name="dropped-span", value=1.0) + span.score_trace(name="dropped-trace", value=1.0) + client.flush() + + assert recorder.requests == [] + + +def test_scores_are_kept_when_sampler_decision_depends_on_more_than_trace_id( + client_factory, +): + recorder = Recorder(_responder(ACCEPTED)) + provider = TracerProvider(sampler=ParentBased(ALWAYS_OFF)) + client = client_factory(recorder, tracer_provider=provider) + + client.create_score(name="kept", value=1.0, trace_id="0" * 32) + client.flush() + + sent = [score["name"] for body in recorder.bodies for score in body] + assert sent == ["kept"] diff --git a/tests/unit/test_scores_batch_response.py b/tests/unit/test_scores_batch_response.py new file mode 100644 index 000000000..ffc084620 --- /dev/null +++ b/tests/unit/test_scores_batch_response.py @@ -0,0 +1,104 @@ +"""Parsing of POST /api/public/scores responses into the CreateScoresResponse union. + +The union is undiscriminated, so the model is picked from the response body. CI +also runs this file on pydantic 2.7, an older supported minor whose union matching +differs from current releases. +""" + +import json +from typing import Any, List + +import httpx +import pytest + +from langfuse.api import AsyncLangfuseAPI, LangfuseAPI +from langfuse.api.scores.types.create_score_batch_response import ( + CreateScoreBatchResponse, +) +from langfuse.api.scores.types.create_score_batch_results import ( + CreateScoreBatchResults, +) +from langfuse.api.scores.types.create_score_request import CreateScoreRequest + +BATCH_REQUEST = [ + CreateScoreRequest(name="accuracy", value=0.9, trace_id="trace-1"), + CreateScoreRequest(name="tone", value="friendly", trace_id="trace-1"), +] + +CASES = [ + pytest.param(BATCH_REQUEST, 202, {"message": "Accepted"}, id="batch-202"), + pytest.param( + BATCH_REQUEST, + 207, + { + "accepted": 1, + "rejected": 1, + "errors": [{"message": "tone: value must be numeric"}], + }, + id="batch-207", + ), +] + + +def _handler(status: int, body: dict, requests: List[httpx.Request]) -> Any: + def handle(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(status, json=body) + + return handle + + +def _assert_parsed(response: Any, status: int, body: dict) -> None: + if status == 202: + assert isinstance(response, CreateScoreBatchResponse) + assert response.message == "Accepted" + else: + assert isinstance(response, CreateScoreBatchResults) + assert (response.accepted, response.rejected) == (1, 1) + assert [error.message for error in response.errors] == [ + "tone: value must be numeric" + ] + + +def _assert_sent(requests: List[httpx.Request], request: Any) -> None: + assert len(requests) == 1 + assert requests[0].url.path == "/api/public/scores" + sent = json.loads(requests[0].content) + assert isinstance(sent, list) == isinstance(request, list) + + +@pytest.mark.parametrize(("request_body", "status", "body"), CASES) +def test_sync_create_parses_each_response_shape(request_body, status, body): + requests: List[httpx.Request] = [] + client = LangfuseAPI( + base_url="http://langfuse.test", + username="pk", + password="sk", + httpx_client=httpx.Client( + transport=httpx.MockTransport(_handler(status, body, requests)) + ), + ) + + response = client.scores.create(request=request_body) + + _assert_parsed(response, status, body) + _assert_sent(requests, request_body) + + +@pytest.mark.asyncio +@pytest.mark.parametrize(("request_body", "status", "body"), CASES) +async def test_async_create_parses_each_response_shape(request_body, status, body): + requests: List[httpx.Request] = [] + client = AsyncLangfuseAPI( + base_url="http://langfuse.test", + username="pk", + password="sk", + httpx_client=httpx.AsyncClient( + transport=httpx.MockTransport(_handler(status, body, requests)) + ), + ) + + response = await client.scores.create(request=request_body) + + _assert_parsed(response, status, body) + _assert_sent(requests, request_body) diff --git a/tests/unit/test_span_filter.py b/tests/unit/test_span_filter.py index cf9965d14..a639a534c 100644 --- a/tests/unit/test_span_filter.py +++ b/tests/unit/test_span_filter.py @@ -50,6 +50,30 @@ def test_is_genai_span_with_genai_attributes(): ) +@pytest.mark.parametrize("predicate", [is_genai_span, is_default_export_span]) +@pytest.mark.parametrize( + ("attributes", "expected"), + [ + ({"gen_ai": "value"}, False), + ({"gen_ai_custom": "value"}, False), + ({"gen_ai-request.model": "value"}, False), + ({"gen_airflow.model": "value"}, False), + ({"custom.gen_ai.request.model": "value"}, False), + ({b"gen_ai.request.model": "value"}, False), + ({}, False), + ({"gen_ai.request.model": "model"}, True), + ({"gen_ai.usage.input_tokens": 0}, True), + ({"gen_ai.custom": False}, True), + ({"gen_ai_custom": "value", "gen_ai.request.model": "model"}, True), + ], +) +def test_genai_filters_match_attribute_namespace(predicate, attributes, expected): + """Match gen_ai.* attributes without accepting lookalike namespaces.""" + span = _make_span(scope_name="unknown.scope", attributes=attributes) + + assert predicate(span) is expected + + def test_is_genai_span_ignores_non_string_keys(): """Ignore non-string keys when checking gen_ai.* attributes.""" assert ( diff --git a/tests/unit/test_span_processor.py b/tests/unit/test_span_processor.py index 9591929be..d7c5366be 100644 --- a/tests/unit/test_span_processor.py +++ b/tests/unit/test_span_processor.py @@ -134,6 +134,11 @@ def _serialized_request_size(spans: List[ReadableSpan]) -> int: return len(encode_spans(spans).SerializePartialToString()) +def _uncompressed_size(request: _RecordedRequest) -> int: + body = gzip.decompress(request.body) if request.content_encoding else request.body + return len(body) + + def _default_exporter_processor(base_url: str) -> LangfuseSpanProcessor: return LangfuseSpanProcessor( public_key="pk-test", @@ -170,7 +175,9 @@ def test_default_exporter_enforces_max_batch_size_bytes_at_boundary( expected_requests = ( [] if expected_result == SpanExportResult.FAILURE else [request_size] ) - assert [len(request.body) for request in received_requests] == expected_requests + assert [_uncompressed_size(request) for request in received_requests] == ( + expected_requests + ) def test_oversized_batch_is_dropped_on_flush_without_blocking_later_batches( @@ -199,7 +206,7 @@ def test_oversized_batch_is_dropped_on_flush_without_blocking_later_batches( finally: processor.shutdown() - assert [len(request.body) for request in received_requests] == [ + assert [_uncompressed_size(request) for request in received_requests] == [ _serialized_request_size(small_spans) ] @@ -293,10 +300,46 @@ def test_client_otel_compression_sends_gzip_request(compression_env, otlp_http_s @pytest.mark.parametrize( ("otel_compression", "env", "expected_encoding"), [ - (None, {LANGFUSE_OTEL_COMPRESSION: " GZIP "}, "gzip"), + # Nothing configured: gzip by default. + (None, {}, "gzip"), + # The argument wins over every environment variable. ("gzip", {LANGFUSE_OTEL_COMPRESSION: "none"}, "gzip"), ("none", {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, None), - (None, {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip"}, "gzip"), + # LANGFUSE_OTEL_COMPRESSION wins over the OTEL variables (case-insensitive). + (None, {LANGFUSE_OTEL_COMPRESSION: " GZIP "}, "gzip"), + ( + None, + {LANGFUSE_OTEL_COMPRESSION: "None", OTEL_EXPORTER_OTLP_COMPRESSION: "gzip"}, + None, + ), + # The traces-specific OTEL variable wins over the generic one. + ( + None, + { + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "gzip", + OTEL_EXPORTER_OTLP_COMPRESSION: "none", + }, + "gzip", + ), + (None, {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "NONE"}, None), + (None, {OTEL_EXPORTER_OTLP_COMPRESSION: "none"}, None), + # Invalid values fall through to the next setting, then gzip. + ("brotli", {LANGFUSE_OTEL_COMPRESSION: "none"}, None), + ("brotli", {}, "gzip"), + ( + None, + {LANGFUSE_OTEL_COMPRESSION: "zstd", OTEL_EXPORTER_OTLP_COMPRESSION: "none"}, + None, + ), + ( + None, + { + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "deflate", + OTEL_EXPORTER_OTLP_COMPRESSION: "none", + }, + None, + ), + (None, {OTEL_EXPORTER_OTLP_TRACES_COMPRESSION: "bogus"}, "gzip"), ], ) def test_default_exporter_compression_precedence( diff --git a/uv.lock b/uv.lock index 08fb4789b..6f94d3a32 100644 --- a/uv.lock +++ b/uv.lock @@ -1,22 +1,24 @@ version = 1 revision = 3 -requires-python = ">=3.10, <4.0" +requires-python = ">=3.11, <4.0" [options] -exclude-newer = "2026-10-07T08:40:36.267082855Z" +exclude-newer = "2026-10-06T09:31:30.784708305Z" exclude-newer-span = "P7D" [options.exclude-newer-package] -opentelemetry-sdk = { timestamp = "2026-10-11T08:40:36.267095148Z", span = "P3D" } -opentelemetry-exporter-otlp-proto-common = { timestamp = "2026-10-11T08:40:36.267096921Z", span = "P3D" } -opentelemetry-api = { timestamp = "2026-10-11T08:40:36.267089407Z", span = "P3D" } -opentelemetry-instrumentation-threading = { timestamp = "2026-10-11T08:40:36.267099746Z", span = "P3D" } -opentelemetry-exporter-otlp-proto-http = { timestamp = "2026-10-11T08:40:36.267097412Z", span = "P3D" } -opentelemetry-proto = { timestamp = "2026-10-11T08:40:36.26709626Z", span = "P3D" } -opentelemetry-exporter-http-transport = { timestamp = "2026-10-11T08:40:36.267098444Z", span = "P3D" } -opentelemetry-semantic-conventions = { timestamp = "2026-10-11T08:40:36.267095739Z", span = "P3D" } -opentelemetry-exporter-otlp-common = { timestamp = "2026-10-11T08:40:36.267097933Z", span = "P3D" } -opentelemetry-instrumentation = { timestamp = "2026-10-11T08:40:36.267099215Z", span = "P3D" } +opentelemetry-sdk = { timestamp = "2026-10-10T09:31:30.784962394Z", span = "P3D" } +opentelemetry-exporter-otlp-proto-common = { timestamp = "2026-10-10T09:31:30.784963814Z", span = "P3D" } +opentelemetry-api = { timestamp = "2026-10-10T09:31:30.784962048Z", span = "P3D" } +opentelemetry-instrumentation-threading = { timestamp = "2026-10-10T09:31:30.784965743Z", span = "P3D" } +opentelemetry-proto = { timestamp = "2026-10-10T09:31:30.784963434Z", span = "P3D" } +opentelemetry-exporter-otlp-proto-http = { timestamp = "2026-10-10T09:31:30.784964177Z", span = "P3D" } +opentelemetry-exporter-http-transport = { timestamp = "2026-10-10T09:31:30.784965016Z", span = "P3D" } +opentelemetry-semantic-conventions = { timestamp = "2026-10-10T09:31:30.784963106Z", span = "P3D" } +pydantic-core = { timestamp = "2026-10-10T09:31:30.784961598Z", span = "P3D" } +pydantic = { timestamp = "2026-10-10T09:31:30.784796083Z", span = "P3D" } +opentelemetry-exporter-otlp-common = { timestamp = "2026-10-10T09:31:30.784964659Z", span = "P3D" } +opentelemetry-instrumentation = { timestamp = "2026-10-10T09:31:30.784965353Z", span = "P3D" } [[package]] name = "annotated-types" @@ -32,7 +34,6 @@ name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -74,15 +75,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "certifi" version = "2026.2.25" @@ -107,22 +99,6 @@ version = "3.4.6" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/8c/2c56124c6dc53a774d435f985b5973bc592f42d437be58c0c92d65ae7296/charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95", size = 298751, upload-time = "2026-03-15T18:50:00.003Z" }, - { url = "https://files.pythonhosted.org/packages/86/2a/2a7db6b314b966a3bcad8c731c0719c60b931b931de7ae9f34b2839289ee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd", size = 200027, upload-time = "2026-03-15T18:50:01.702Z" }, - { url = "https://files.pythonhosted.org/packages/68/f2/0fe775c74ae25e2a3b07b01538fc162737b3e3f795bada3bc26f4d4d495c/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4", size = 220741, upload-time = "2026-03-15T18:50:03.194Z" }, - { url = "https://files.pythonhosted.org/packages/10/98/8085596e41f00b27dd6aa1e68413d1ddda7e605f34dd546833c61fddd709/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db", size = 215802, upload-time = "2026-03-15T18:50:05.859Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ce/865e4e09b041bad659d682bbd98b47fb490b8e124f9398c9448065f64fee/charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89", size = 207908, upload-time = "2026-03-15T18:50:07.676Z" }, - { url = "https://files.pythonhosted.org/packages/a8/54/8c757f1f7349262898c2f169e0d562b39dcb977503f18fdf0814e923db78/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565", size = 194357, upload-time = "2026-03-15T18:50:09.327Z" }, - { url = "https://files.pythonhosted.org/packages/6f/29/e88f2fac9218907fc7a70722b393d1bbe8334c61fe9c46640dba349b6e66/charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9", size = 205610, upload-time = "2026-03-15T18:50:10.732Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c5/21d7bb0cb415287178450171d130bed9d664211fdd59731ed2c34267b07d/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7", size = 203512, upload-time = "2026-03-15T18:50:12.535Z" }, - { url = "https://files.pythonhosted.org/packages/a4/be/ce52f3c7fdb35cc987ad38a53ebcef52eec498f4fb6c66ecfe62cfe57ba2/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550", size = 195398, upload-time = "2026-03-15T18:50:14.236Z" }, - { url = "https://files.pythonhosted.org/packages/81/a0/3ab5dd39d4859a3555e5dadfc8a9fa7f8352f8c183d1a65c90264517da0e/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0", size = 221772, upload-time = "2026-03-15T18:50:15.581Z" }, - { url = "https://files.pythonhosted.org/packages/04/6e/6a4e41a97ba6b2fa87f849c41e4d229449a586be85053c4d90135fe82d26/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8", size = 205759, upload-time = "2026-03-15T18:50:17.047Z" }, - { url = "https://files.pythonhosted.org/packages/db/3b/34a712a5ee64a6957bf355b01dc17b12de457638d436fdb05d01e463cd1c/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0", size = 216938, upload-time = "2026-03-15T18:50:18.44Z" }, - { url = "https://files.pythonhosted.org/packages/cb/05/5bd1e12da9ab18790af05c61aafd01a60f489778179b621ac2a305243c62/charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b", size = 210138, upload-time = "2026-03-15T18:50:19.852Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8e/3cb9e2d998ff6b21c0a1860343cb7b83eba9cdb66b91410e18fc4969d6ab/charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557", size = 144137, upload-time = "2026-03-15T18:50:21.505Z" }, - { url = "https://files.pythonhosted.org/packages/d8/8f/78f5489ffadb0db3eb7aff53d31c24531d33eb545f0c6f6567c25f49a5ff/charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6", size = 154244, upload-time = "2026-03-15T18:50:22.81Z" }, - { url = "https://files.pythonhosted.org/packages/e4/74/e472659dffb0cadb2f411282d2d76c60da1fc94076d7fffed4ae8a93ec01/charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058", size = 143312, upload-time = "2026-03-15T18:50:24.074Z" }, { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, @@ -242,18 +218,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "execnet" version = "2.1.2" @@ -362,99 +326,115 @@ wheels = [ [[package]] name = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, - { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/1f/8176d92e001f86505424b41664032ae26a882bc9ca41a32c803f373f9195/jiter-0.17.0.tar.gz", hash = "sha256:03e432f226a453851079fb84cd17c6da9991eab723e28d716f14ae3d906e0c12", size = 229037, upload-time = "2026-09-12T15:14:14.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/50/17afdaffcc8af4bf4fddf2b6c26d066553aa2221983f2affcde435fc2532/jiter-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cfafd7be8b16ceadd298db542cead37cddc211c4c49e04ad2596924df18625b1", size = 290699, upload-time = "2026-09-12T15:11:30.085Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/c185d32d5b3657ad84da26c84a9eb15f00aa1b39d6882fcc0052dba2d7c2/jiter-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8adca2e793288e5f1bb29279bb439d0d3cfbb50eddca7e7e6ffd42ff4f482406", size = 324246, upload-time = "2026-09-12T15:11:31.3Z" }, + { url = "https://files.pythonhosted.org/packages/24/7a/8b8903bfe91a90a8fa1ec9b45d9fda5b6287a386693b69d720a882d73f3c/jiter-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c692d567ba206c7cca38c9d1d0ccc70c9786290173c184d871ca12e9981ed7", size = 343275, upload-time = "2026-09-12T15:11:32.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5d/6821fae2abc71a3c3a84bef8598d31fc4f27d9edfb55bd8f6c08afb8ef93/jiter-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81c83c0abe614446a283d994d2c07c4f58632dea2cdf66ba9e2921bb8ccd593e", size = 328034, upload-time = "2026-09-12T15:11:33.9Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/8e7a963b1c2dfdc01d6228b004f50a2a3d7c46f0549d7b096a5d15ef81d5/jiter-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:073dc68c1a700c8fc480e877864a6b6ffc887533e261f4380c08c16bf09d057a", size = 342457, upload-time = "2026-09-12T15:11:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/8b2a267e3bda45d9298331cacfe3521e761f0a2b05ad10a23c6548d08358/jiter-0.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:492f37230bbf9581ab2c17bcda862c249afb9ae2e3ab2dd6db59943bc4cc3153", size = 351271, upload-time = "2026-09-12T15:11:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/8f/5c74e5e142a6736833d7a991ab04d5c0738038dc44db05af0bb3cd2559e8/jiter-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5888fe5abc1ca2fa834a3e1b4c7ef0dcece286a7d7e95a609ef0934b777b9fc9", size = 346052, upload-time = "2026-09-12T15:11:39.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/9c/f54920f06d1696e80b1be841d56412871c6856b1b1e3b541b1ed35346554/jiter-0.17.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:84ac78df457e1ee3f7e733bd114823302ae8c5ad5542d7e6647d92ffaa090a04", size = 328915, upload-time = "2026-09-12T15:11:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/21/53/080f126863bceb055db9f1fd5431485eb493b35545a3c35e961ac18cd924/jiter-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7573e80232c5bcf80c24c038cf7e53a463f5c3b1dd1dd4109d66304f4dccc233", size = 337240, upload-time = "2026-09-12T15:11:42.356Z" }, + { url = "https://files.pythonhosted.org/packages/f0/76/3ab742823a0e0e70e143c6c90a482d9d90396ac2445e7b9483eb7245d3b5/jiter-0.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:11902505d401691720f5785c15b02204248526edee11b635cd6c40cd52b81599", size = 484917, upload-time = "2026-09-12T15:11:43.549Z" }, + { url = "https://files.pythonhosted.org/packages/14/e0/8ca71bc8b9cc9ed96c9da565863e00f3bb875a8fb82abcf03e975e067902/jiter-0.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:64846211a2debe7c071d2146d2283d2b0c1c93dc8fd5fb7794faac2ca6061b5c", size = 521868, upload-time = "2026-09-12T15:11:44.704Z" }, + { url = "https://files.pythonhosted.org/packages/c1/02/81f8719dcedb75713082a2048405376c81f6546b75af8167f3bba01a1ed0/jiter-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c19b9357309b8cc6de8a48fca8e44a8c9c2feaaa2f5896d037fa505d48fcab80", size = 187844, upload-time = "2026-09-12T15:11:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8c/59693f348488f01ed12d862e99ab8da14961152d3e9c39b9b1ef363f3572/jiter-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:e654b6b04e39c9cb19cb8b04c6ddf1f2db07751fa14156413969fd78bad0e5cb", size = 231402, upload-time = "2026-09-12T15:11:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/fe/89/fb35e286463cb9f01edc2c4e47df6e5477ee36bca5096414e9ea87985588/jiter-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:3ad556afc289f15d2b181b941982d01f06190863c07440185b9f354e1bd2def3", size = 187006, upload-time = "2026-09-12T15:11:48.245Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f8/07bd8c3a23f7a8a6875e6a820bbffe1483a18f18f9398a91b5495123176e/jiter-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ebf918dfd6a74adc1b9ad71f63c4ab00902fcd3b7fd39f2e24d871db8d713b91", size = 291633, upload-time = "2026-09-12T15:11:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5e/0de4c6f84ffefa6809ffc2d550b9a314365acf7e7ec9b6c7375d49047900/jiter-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61aed66ee042b3b49ef85fdf75714234d055d89d8496ac1c6e47f89e7a30d5e4", size = 321695, upload-time = "2026-09-12T15:11:52.727Z" }, + { url = "https://files.pythonhosted.org/packages/20/ac/befe2e82065bee37a0252081666ed2f48c1ac5f5c6c318c2de8168ba393d/jiter-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76eb4a5c20e86f9f848286f167024890f2862258a965d254774deb7fc1545ca1", size = 341967, upload-time = "2026-09-12T15:11:54.231Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cd/9797c1e529746750ae589da7c1a8c24373f00d88e11a989f9e5eb1959079/jiter-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcc064f99183a9cbe7f26ed648c352031a74145cd61ed75d34632c73eb46a5a8", size = 326546, upload-time = "2026-09-12T15:11:55.41Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fd/e6914c38d6347bab4ebff2b1f0c0f191db276e7a1d5c376176757da42fe3/jiter-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b64e69c4150748e020356d958af94bec33c70a0a93d665cfa8f6d580fe1a63", size = 340995, upload-time = "2026-09-12T15:11:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/611b3abf6f88945b5474da5cdc6d1a185e805ac9bf446bb7766dcda6ea87/jiter-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0bc7f684b65bcda9c20434267577db71bf9905ceddd32b60d1d93278d8c8d3a", size = 352188, upload-time = "2026-09-12T15:11:59.414Z" }, + { url = "https://files.pythonhosted.org/packages/52/f8/b6e513ecbdf3b3cebe587c2279281ecf775b729a58cf4cc7bdf898ded029/jiter-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c21265b251d99bbb40080d178a8953e35601d3a1564e05c4de4c0d2ca616797", size = 345025, upload-time = "2026-09-12T15:12:00.697Z" }, + { url = "https://files.pythonhosted.org/packages/28/a8/fe26d06c5a6c5a4cfe703c5154c8a140da1305671eb3681aba9422d4f393/jiter-0.17.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f3d7f7b34114f7ddc6d72a8e882d49de636b35d9fd12b4d420d3c5729f6c9812", size = 329180, upload-time = "2026-09-12T15:12:01.831Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/e6d66a26af40a20e62486feb7e222fd50f6e7aaa4f107abd89675dcc835b/jiter-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5078ab00664307fab2019b522a93aeb191122789f085daf5fd9e362154021d4a", size = 335805, upload-time = "2026-09-12T15:12:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/96520aa2fef5ef831d95483a902140bfab83dcac9eaa74f7df61b5e50a1b/jiter-0.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:470e1b1e4c42f1ead2189166a299691871a2df5056c976e7fb96feafaf5f9d44", size = 484121, upload-time = "2026-09-12T15:12:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8f/5d9d92fe538bf36ff481a2278c48147e59c1cf8eb2f7be665260665febe5/jiter-0.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6eb6aedeb7352b8f3b6af9cbd67983840165c00428e63f1b420a85885128ea31", size = 521310, upload-time = "2026-09-12T15:12:05.612Z" }, + { url = "https://files.pythonhosted.org/packages/50/06/a09f979b22e652afbc3de66c709b2ba92edcef555f7535ab937c86b4f21a/jiter-0.17.0-cp312-cp312-win32.whl", hash = "sha256:362bb47423886d45a9f705d2d9d4008c6eedd4e41eb1bab4e96fb6daa06b33fd", size = 185029, upload-time = "2026-09-12T15:12:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/98265a005b2473ec2be5a84e2b64c2f65382c673879f1574845cd4bcd77c/jiter-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9bd3caac219df476dd0cc3fe01d2f1581ed588906feac767abd9614c1c12f8b3", size = 227381, upload-time = "2026-09-12T15:12:08.823Z" }, + { url = "https://files.pythonhosted.org/packages/a8/11/2e05bf5a56e57a543ebb8f585074adf09383e99d7b062dac92eab1f4d57f/jiter-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:36ee6e69027396664e59995b9a635a947a5304ee9837279584a0bb8145c8f6b8", size = 183610, upload-time = "2026-09-12T15:12:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/2c4a8075ed5ea02b56911e9375d4c8d7784572ff4af32e5a99ae0d071044/jiter-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b18434638228c0c184281609bf3d9459026a0f1ea48fb76c205e3ef72069caa", size = 290991, upload-time = "2026-09-12T15:12:11.641Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b1/34bfa29599d420423baac6ff7cada6674fe63d5a7a2ccb3900b904678783/jiter-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec89771f4272b989487a6364e519db6bbaba323e8bbf949ac89a45ea9c18b7a3", size = 321425, upload-time = "2026-09-12T15:12:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/11/71/a5ac64a62a04aebd556afadab14a6b730001e16df87266ded943a100a1d9/jiter-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3f052c671d5f425cca5ea5901cf11a831369fba4a55a3862cab93c323b4c3b", size = 343138, upload-time = "2026-09-12T15:12:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/f761e320ea473314cb68612bc6a435393464dbd198051399b36848b4ebf3/jiter-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785a216bbaf8f15fc974e964ced7322cd3d774bb0e86949edd78c6bffd6ba35b", size = 325805, upload-time = "2026-09-12T15:12:16.506Z" }, + { url = "https://files.pythonhosted.org/packages/19/1a/27d8e40f0fb29bbc7a5adf30907144396a115dbe93d5d8976c054a6dfe96/jiter-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d85c558c9f8532bba287a990ac63767c7daf756f0d8c030219f62499b1fa228a", size = 340230, upload-time = "2026-09-12T15:12:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c0/30bcde78a28155461f965d16b7aca4ffca6d17494d905f7a0bb072e6c64e/jiter-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c23849235d2142ce444b2b8c6eceee9f82f4cc0bd5c9081602e4155c6197807", size = 351343, upload-time = "2026-09-12T15:12:19.337Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/91420b156315ae22732f5ee1a7b5725a030aab9dc8fd7dcdacfb4aa588d3/jiter-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58df29268a95e910f17db7ec9178eb7f15aa8619aaca3575275c4e6b3f4fe4c5", size = 344990, upload-time = "2026-09-12T15:12:20.705Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a2/ae6d5672644cc11127970277c9aeb0fa6fae376845587f5b0a8e8828167c/jiter-0.17.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a277f97eba7d66b1ee27eb5dab5b774ff46a10c78d89a1d3dcce04ce1357c8ca", size = 328624, upload-time = "2026-09-12T15:12:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/45cb1162f6aa586536e4a973fc339d72dc6b08cca030d70a838a307aa778/jiter-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe15ddf316f1f1f643347d3a474e74ce61880c79a11ec5dca53df20c071bd3e8", size = 334731, upload-time = "2026-09-12T15:12:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/45c1574b644da7deda0b7591c349520dcf83ce45b24d7ca19922dab1fc27/jiter-0.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:02adebb7ce6413c44d40af9ad59d1c1cd79630ccdcb6f7bdd2d461e48c03d8f9", size = 483649, upload-time = "2026-09-12T15:12:27.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d3/ebea1ecb5b241c519f192b30215c79a8e47f42f1621acbcd6f8830728416/jiter-0.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:55d0e0e613a3f9ad600cf436e0e2b8057d1b52bcf1d91b2d36ac53451231e6a8", size = 520758, upload-time = "2026-09-12T15:12:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/682b641ff0765ea9bdc349dbc7d223de5c8af8ec1abda0db3406992f92fe/jiter-0.17.0-cp313-cp313-win32.whl", hash = "sha256:2c45ad7c973ef33fe5114a953377b35a95240f4542c0724d9f781e47dc24bac7", size = 184334, upload-time = "2026-09-12T15:12:30.813Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d2/9a49aac2b27af4cc5015e368c0cc3588491a532f717a668ffce1f1ac57da/jiter-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:a3cebb1fe4a1abb00465f3f8a17e09112603e8b7c59e5c3adbcd9f7815a64acd", size = 226601, upload-time = "2026-09-12T15:12:32.096Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ce/9a43e9f614608eafa78de22aedcff54cd21324467b5d442d5c9b00244145/jiter-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:96b8b0c6dc5d78682f54a450785e075aa929cde768304cad363cd4efba5a82ac", size = 183103, upload-time = "2026-09-12T15:12:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/01/9e/23065f8e2c7a4c372c1b6f6622e4cfab4dc786cb5150052b1527e6a6a840/jiter-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:00d783a779c5664e16dbad5e3a3c3a75e128b07dd5f4765159658d9210a50ca5", size = 292210, upload-time = "2026-09-12T15:12:35.613Z" }, + { url = "https://files.pythonhosted.org/packages/ea/81/67b58647560bc82a4490d722caa8561d7a86a9f45d4fa620b7e5fe282c7a/jiter-0.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0619d806e260ecf0c2a64521942c94af5d547c9ec99b55ae4f51b538b5576a76", size = 321512, upload-time = "2026-09-12T15:12:36.907Z" }, + { url = "https://files.pythonhosted.org/packages/c7/07/6658359a25f55927f7f8bf0e16465dee2ccd0b2a1a5208acc0df8972e074/jiter-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc0288ce39190ee33fe6e4ec73161eed34e7e2da509b525546ca061778d62b64", size = 343897, upload-time = "2026-09-12T15:12:38.189Z" }, + { url = "https://files.pythonhosted.org/packages/46/04/5d50a9f0319cbdc37fd53c27f8c313d46afc34f1b048219ae6d8ea068da4/jiter-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a52a430d04225ffde633e6840bf2381d34c019ff98526b5929755b9052fb199", size = 326519, upload-time = "2026-09-12T15:12:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c7/d02517832b29eb8275fdd0f4ce0f17b80f58cc4c3ebecd4d9ace990d633d/jiter-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37f33d327900bf2879613b3363fd48df97b4232d0c41f54bcf2e790c2fc40a71", size = 341369, upload-time = "2026-09-12T15:12:41.486Z" }, + { url = "https://files.pythonhosted.org/packages/3b/07/499b5f5603501cdd93a73a6a176dfad9c96555a3ae58ca9f8e3acba63dc9/jiter-0.17.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cf564d43c4388149ca58ee571d0f5ccf875e20d1fd4662fd94cc0d1ea3b10ef", size = 352160, upload-time = "2026-09-12T15:12:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/f5/75/b04013c7743269d4533ef4e746fc0ed678a143968dd7448658e3f51daad2/jiter-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:523c499235fb65add25d4bb01b1c4709ce695efdc7deb6c0a7bc515b5c44e0fb", size = 345018, upload-time = "2026-09-12T15:12:44.192Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/cbb6fd1e42a77c8412ec4643db95059b30cdfc635e387cc9193e098ce268/jiter-0.17.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:455e4ab35cb2a4a91a8404e08fd3c621bae433922e59bf1c494fe20a426b013b", size = 329244, upload-time = "2026-09-12T15:12:45.491Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/d3be402f398566a379bf40ae65be5c3505b14d9e95e0802a597ddde7ddee/jiter-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6871973bfbd4408f7f1c632b30bbb5bbd9671c1bc8650af6823e24b7be13709b", size = 335693, upload-time = "2026-09-12T15:12:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/98e2c4130b93d64f1d67c89060b928d04102549bf05e64451c9e6024f9ca/jiter-0.17.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:77f6aac0137309b31448c1bdcda4c6c77077664a6d018ece8d94019c68a5a5b9", size = 484329, upload-time = "2026-09-12T15:12:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/78/5e/8da91e49f0fbca37c3489fb4cf3ad6676d4965f00ae5468bca3a2513737a/jiter-0.17.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:93946d89fa04d5ba64dd323a8dd8d901676cb8a3c81d99ae4f6c051a9b4c3f2f", size = 521358, upload-time = "2026-09-12T15:12:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/5388684a5a38af3557cd9c2424b9827c71809cff24373c75ef9d0d3dfba9/jiter-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:70f19a2ca8429f91e82eeffb2f51cb87bc2d6e953b009b91a92d29c3a16ccb03", size = 110459, upload-time = "2026-09-12T15:12:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/58b3a93525d2ffca7f54d9dee441381990082bd1172fbeb8d6a3f72a4dc3/jiter-0.17.0-cp314-cp314-win32.whl", hash = "sha256:71dbd74314c5df52a1bccf7b8bca46d14e943af7a2012e73b23f49977ef194c8", size = 185043, upload-time = "2026-09-12T15:12:54.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/1aa520eb6c359b262c14ff995ca7283837208ddfb1202082ce9d73cf214d/jiter-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:ac3c6ee3264d6f5c44c617f90bc7e8b9e1587e7d6708c9d8f811cb65582ee312", size = 227163, upload-time = "2026-09-12T15:12:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e4/5997f648794bd9b499491d0ff480b096cc9a9c65bdba29f57568e6aa1705/jiter-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:6219adaf59711ba7063a52496e8ec6d3fa3e209d7827d83eee3b2abc780a1744", size = 183505, upload-time = "2026-09-12T15:12:58.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/84a5ec271d09f7590b6073af5ee4abb44eab4ccace453b7e2c5ce45234ca/jiter-0.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:59bddbe6f9ffecc68d641e1e2d619ce64cf8a9e9eeb74e5c518f74fc87abf1b0", size = 321527, upload-time = "2026-09-12T15:12:59.394Z" }, + { url = "https://files.pythonhosted.org/packages/39/71/9e1fd0045f5920b4c36be35c3f0f0dfd123668684f8ad352619d7aa44183/jiter-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cb41cd1432f1dc19a231cf70b54d42b2c9f05085155859263fce06fa4d41388", size = 340865, upload-time = "2026-09-12T15:13:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/14627fd2bc377f3dd09491bcace6b90e34b4d7fea2f1f3295031ff91f528/jiter-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd7790aa79c8b518e512ebcdfce9f11d8ef5f30efd43720c8a19a548b39fa489", size = 325412, upload-time = "2026-09-12T15:13:02.152Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/8d5808f7bf0f456bde79e6393587183a0cee5f83d179fe1f7f1eff2ba067/jiter-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbbfe4e3c21c8166980cddc5bee1a315df082454f007947dfb6fb73800768165", size = 340473, upload-time = "2026-09-12T15:13:03.485Z" }, + { url = "https://files.pythonhosted.org/packages/4f/da/1d8c7c6c4ae6b2423b94a81b6b907d37b28f87664e077427b531bf1b5313/jiter-0.17.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c286860abfe8b100cac1c02e225e5776eb9216edd71ba17cdb237da4af32bc9", size = 350757, upload-time = "2026-09-12T15:13:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/eb/96/c1813dcca15c5a370145a448aaea7d1f83f6f0228a5f1130e79340ee385f/jiter-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f753eb70b1474a29e635e7542ff7312e6d6b951e0b25e8a2e8c34eeb1ddcd478", size = 345203, upload-time = "2026-09-12T15:13:06.131Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/fc61cbcf2992d169ede13648fc3fd8e2d3171a3669dde43cd4db556549ac/jiter-0.17.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eae86b1f027031e39db2e0e9c4842221edb7b8cd474d23f87a79b3bd4b651768", size = 328322, upload-time = "2026-09-12T15:13:07.392Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/46418a3abbdffb7dc41b314200360f24f75faaeb35573e81c92de322cce9/jiter-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bf350452a43173e69e1fc74847c57a60e3d7515807287f29849baa2a85d8718", size = 336570, upload-time = "2026-09-12T15:13:08.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/28/b8a55b949be6306df8888e365a8df05441de8a7b11289f6957004302e41e/jiter-0.17.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:da139721f4b7cafdbff580a4f511ea24cb91f4909330c6b926a1ca53836c0a59", size = 482879, upload-time = "2026-09-12T15:13:10.037Z" }, + { url = "https://files.pythonhosted.org/packages/75/3b/21d0afa53ba0680962c39f3eb95ed2946f8793369ed44b0c82b490723081/jiter-0.17.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8079849db9a1371bfd90bad088458a8fb836261879df2233cc9632464ecf64e1", size = 520406, upload-time = "2026-09-12T15:13:11.456Z" }, + { url = "https://files.pythonhosted.org/packages/ef/03/bcbaf8b6b9ea23c2c074411f8ecfbb02d820abac5d0cb8f4e280209174a2/jiter-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06", size = 184434, upload-time = "2026-09-12T15:13:13.037Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b5/5d6ce2c93ef6fe1241b37a9005547f9b6d58db1f07f39fe95807d4b98f51/jiter-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9", size = 227392, upload-time = "2026-09-12T15:13:14.933Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4b/1e52baf90187606e33a7b8cfa8f96f5829acd7f01870077eb01059ab76d0/jiter-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc", size = 182776, upload-time = "2026-09-12T15:13:16.239Z" }, + { url = "https://files.pythonhosted.org/packages/05/fc/efe3ac75564ab10f53517958f5ccdc231fc7334af66c76776cb554a88967/jiter-0.17.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:84963d3f395ef5e9a32ce47155e08a7962fa292c159a10cb98b931cef1416925", size = 292143, upload-time = "2026-09-12T15:13:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4c/46982118d91f9ffe9714319d21ec4f98d9b7e0cfd9062826c524a54de24e/jiter-0.17.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ffa0380ad091de7d3fc33e17a97ff479851ee18a0a2a3ee56ff3215cdc886656", size = 321341, upload-time = "2026-09-12T15:13:19.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/12/9b1ac6ecc6307049913db54839ddba1c11c1ef72c5a8bbb5514bc3b50d1b/jiter-0.17.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:755079792868ce5d4938e83b91a0939b34fb858a1ca65a104f2d771bea57faa1", size = 344383, upload-time = "2026-09-12T15:13:20.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b6/527cc72af836d824e9d4d666e64f0a1ca7eafd662a8da9657b78592172ba/jiter-0.17.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3bf4dc2b84a464117fb097d15a25c58d100d2692888e3b0d92df5b48ed16b7c0", size = 326841, upload-time = "2026-09-12T15:13:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/d1/41/567f98617e88005b249503b933803f633ec6ba2d427cf4cc35e5c832125c/jiter-0.17.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02a360707033d8cef53f7f3480817a1489177a259ec6ec01e98c37e0b922ddca", size = 341354, upload-time = "2026-09-12T15:13:23.323Z" }, + { url = "https://files.pythonhosted.org/packages/40/da/b29cda895b785f7d426e224638a885b6145a08ce853b381f34afe3e88c5d/jiter-0.17.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:300ce01ab0215e3dea4d00090143c909aedc65c0f809b3c07983e1d038f291b9", size = 351985, upload-time = "2026-09-12T15:13:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/f7/5c/8a73829e7389e72ea298a450f2b3cb58e71a3e464b45f6d8753740f1c4f5/jiter-0.17.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746243a080b4ca790b8499af3d7cf9825d5f5987933950cd818e767ee353d826", size = 346052, upload-time = "2026-09-12T15:13:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2f/98d6001026932c095ba440925570123043bed29f5ff56158dfe729a9e81b/jiter-0.17.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:b550585523339b71cb852b811aae49d08d7601ad8ffe9f5dc1562f4c3d22fd87", size = 329159, upload-time = "2026-09-12T15:13:31.569Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/708dc1d2678f092c31c12754e860cd8353e6a85ecbdb1010157edca0da9e/jiter-0.17.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0239520085cac678e77a606fd7e3f1c60c371d719790c5e3807388d3da4354c2", size = 336001, upload-time = "2026-09-12T15:13:32.846Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/75a5ae38862f4eaf0fe2f8a9fbf6484c4890df04c06dcdffc45e36bca61a/jiter-0.17.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:eb2295da7c3769f6719b227a237aa6a5cfa6550e478bc838001b592c57e16575", size = 484281, upload-time = "2026-09-12T15:13:35.333Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/6636fae811c27c7f93e1b11fb5800de6a5c9e4269a27cf718e0b31218ad1/jiter-0.17.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:e088612ff90ebc9247e1a43074b72835804261c47e6a6c01cb3ddcb55360d688", size = 521300, upload-time = "2026-09-12T15:13:37.101Z" }, + { url = "https://files.pythonhosted.org/packages/61/aa/12df7e0b0b1a2602e3d5a5a7104d7d9700f254b400f134a9b50955c4d231/jiter-0.17.0-cp315-cp315-win32.whl", hash = "sha256:0b52d52035b3907c5b1f6277857b29c1cbfc965e24e0f27330dbed83edb591ec", size = 185138, upload-time = "2026-09-12T15:13:38.901Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ec/3dd2e495032cddde05723c1f4c743b67a23e55d2af244692a7f58f0cdae3/jiter-0.17.0-cp315-cp315-win_amd64.whl", hash = "sha256:10f5558eed511b830488003449d942bd75829ad6257dc58cb9a03e596a7777b1", size = 226950, upload-time = "2026-09-12T15:13:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c7/ef85704e0a57e9cadb2babc05f6d7c5df4a1c75da1a6ee31e1986b0099a5/jiter-0.17.0-cp315-cp315-win_arm64.whl", hash = "sha256:fa13acf1046f95df808c64b1310705e143fab87aee73ae00cc42d640867fd2c1", size = 183618, upload-time = "2026-09-12T15:13:41.432Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/a4b348349de68762b58d6713973d363ad80a1c741d0bf8def7975f0ecb26/jiter-0.17.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:af2f7501580f274b63c4b2283bc425f5df7edf06ae5b171e5f87d912ff359a20", size = 321155, upload-time = "2026-09-12T15:13:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/aebd6d0b5f0677de3a3d0bdc4a05fac949b97c4ede454c8809f180ac7b17/jiter-0.17.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c5349312e5cb02b7a21e123a57665afa895953f05bf252a9dd4c13a572b7ab", size = 340985, upload-time = "2026-09-12T15:13:44.115Z" }, + { url = "https://files.pythonhosted.org/packages/a7/82/4c3b49796b5eb62f3f5046f957683f4ba0135fe1a60957c11180512460df/jiter-0.17.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86f3f9343a288eb85a81ef20a752b2f84564296636db54a9fff0b5c8deaf1df2", size = 325670, upload-time = "2026-09-12T15:13:45.901Z" }, + { url = "https://files.pythonhosted.org/packages/bc/43/f6341ecb4872202a4ef150486fcee0e1ace4aa3da39b71b82061452cdd3a/jiter-0.17.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4607ec7d93355fbc25b8dc5189153cf21d66063b9f9cd04dd2774e6e783f9b6a", size = 340339, upload-time = "2026-09-12T15:13:47.442Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c4/bc2c86e08fa065e03cb2fbc53b367c3640a7d257ef9d877b29118ea636b7/jiter-0.17.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10cd64a5720ad7f809ac5466ff1705813f1b6b510f195a73acafba0ac0e1f675", size = 350705, upload-time = "2026-09-12T15:13:48.848Z" }, + { url = "https://files.pythonhosted.org/packages/9d/67/91f12aa111cca6e3a197c3e36bf60a034bf9f122f6d41112a639e44217d8/jiter-0.17.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efe9f61bb30174d2f5c8396445c360c96c44e78164d0815dfe627ccf57849574", size = 345011, upload-time = "2026-09-12T15:13:50.215Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/9f5556e8f6ec89755fb5a709d8eb8270c9a324e31079eda0dfbeca451b6e/jiter-0.17.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:370d8fe5bf201dc6925e8a84c81ac7291f74d9fd1778234fc79d517064a5c76b", size = 328268, upload-time = "2026-09-12T15:13:51.809Z" }, + { url = "https://files.pythonhosted.org/packages/22/98/153f20680fb75781a490fb849940e2b00f95035c7aa054df592f36ed33fc/jiter-0.17.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6b303d88e6a0bda789ec4b7801c7bad68e27230ba1fe4baffc756d1fbd32dc9d", size = 337024, upload-time = "2026-09-12T15:13:53.095Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b16c9be3a5035df4466cc72e888188c027562de90a723d290ab6814cb9d4/jiter-0.17.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:30793a24a31e968969757c9e08d830cbb15a2cd3c4959b4498b38f4b1c2258eb", size = 482766, upload-time = "2026-09-12T15:13:55.713Z" }, + { url = "https://files.pythonhosted.org/packages/d0/55/667dea313094024bef082175d6bfe8976f90d1c00c926af9df1d8e0eab48/jiter-0.17.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:686c93d86f2b426c803024b805bd161a6cd10e9627c23e901640eab646c0ad8a", size = 520367, upload-time = "2026-09-12T15:13:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/21/e3/4b1a43501fb9ed17b01d137e380cb0e8fdcb39a254ce31aa2ab95bc861ac/jiter-0.17.0-cp315-cp315t-win32.whl", hash = "sha256:86d703d9faa1ffc8ae4e9de0fa007712ed2171b5c0d93811a8e2e105ac729b0d", size = 184603, upload-time = "2026-09-12T15:13:59.27Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f2/b8ee0372b6ebdf1bde5cc44495d5291d17f961065f5b48f8616cc67cac2e/jiter-0.17.0-cp315-cp315t-win_amd64.whl", hash = "sha256:42b0260445251b1bc520a63baa94a32d88e0f931fba234f1764db7feb7c72174", size = 227936, upload-time = "2026-09-12T15:14:00.472Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/923a1215daba959aed8355973315cb3f81f53e0d01c5b211870a27b41f45/jiter-0.17.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d47687806f9c54c84ea38733507081337922beca90ce819c7d852dd485bc0f23", size = 182977, upload-time = "2026-09-12T15:14:01.799Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3b/05a917204413e2e09906dfa35240c1021227aeb56c7305abea9562c598b4/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:eaba834b72d573547b9d966465b3394b749d5e14208cc70acb63aca37619ab33", size = 288143, upload-time = "2026-09-12T15:14:02.998Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/a1cd3c0cf8f79945939e4f8caae9990529f67d7f77f671769f73956329b9/jiter-0.17.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:51e1519d676a9f14dad9c2a411170d43b022ddb7989562df4e849b261ce127b2", size = 281847, upload-time = "2026-09-12T15:14:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/72/b4/9b797679e09f4a46c32986aeb3670bd9bc562fc0b373c7a0ee5c5dce1206/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ce4feb52493e3513335b2accdcd75605652e4632772d3c8c2f7b86954d7f39", size = 304797, upload-time = "2026-09-12T15:14:05.733Z" }, + { url = "https://files.pythonhosted.org/packages/25/4a/0d77415b27a00d970e4e710f7c1de62e96a11c4cab3ed1add0015af04626/jiter-0.17.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29f49b325e0234e4ad9ecca5b861ffbd09b95ccac9bd46fa55841b6e56eea5fe", size = 307815, upload-time = "2026-09-12T15:14:07.105Z" }, + { url = "https://files.pythonhosted.org/packages/17/31/4bb27f54333d3b9ef1e5bd3312dc0b4bbe59c68bb0885fdb40583a6b1567/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:454c4997d73cc466c71fd565d91e603b0274e48ea0c6b0b7a7aee6967e4ceb7c", size = 288415, upload-time = "2026-09-12T15:14:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/879570ecf82574eaea77c5eb10309f4b630dece5f2a556e9814a90ba3f2d/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:40d2c240f8f80b5b0f201b29f0ae129c81448c60c772227a41747b5e0026f6a2", size = 279113, upload-time = "2026-09-12T15:14:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/77/7a/1f0b8a35fbd079a4f1752c31a15dc99cf277f863747c459be0af39e900e5/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e05f5adbf68c4bd11e1610f394034d984152988e84be6f8314235ce6f2139e5", size = 303708, upload-time = "2026-09-12T15:14:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/d76219ebdbcf3d4209d9d21a0810db4c8d0a6f88e3ee87d30bdea4e90d30/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff", size = 307147, upload-time = "2026-09-12T15:14:12.897Z" }, ] [[package]] @@ -540,16 +520,16 @@ wheels = [ [[package]] name = "langchain-openai" -version = "0.3.34" +version = "1.1.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "openai" }, { name = "tiktoken" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/23/91461c6c2b1c8ceeadd6fefe453a03b1537afeff4ace3c118d7e7659f5a7/langchain_openai-0.3.34.tar.gz", hash = "sha256:57916d462be5b8fd19e5cb2f00d4e5cf0465266a292d583de2fc693a55ba190e", size = 785924, upload-time = "2025-10-01T15:18:40.635Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/fd/7dee16e882c4c1577d48db174d85aa3a0ee09ba61eb6a5d41650285ca80c/langchain_openai-1.1.12.tar.gz", hash = "sha256:ccf5ef02c896f6807b4d0e51aaf678a72ce81ae41201cae8d65e11eeff9ecb79", size = 1114119, upload-time = "2026-03-23T18:59:19.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/94/132aff5a1ed4ebfe7b4d3e4dbf30b9f70ce2bd2ac3470d0c0e742845afb1/langchain_openai-0.3.34-py3-none-any.whl", hash = "sha256:08d61d68a6d869c70d542171e149b9065668dedfc4fafcd4de8aeb5b933030a9", size = 75605, upload-time = "2025-10-01T15:18:39.415Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a6/68fb22e3604015e6f546fa1d3677d24378b482855ae74710cbf4aec44132/langchain_openai-1.1.12-py3-none-any.whl", hash = "sha256:da71ca3f2d18c16f7a2443cc306aa195ad2a07054335ac9b0626dcae02b6a0c5", size = 88487, upload-time = "2026-03-23T18:59:17.978Z" }, ] [[package]] @@ -571,9 +551,10 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "autoevals" }, - { name = "langchain" }, + { name = "langchain", marker = "python_full_version < '3.15'" }, + { name = "langchain-core" }, { name = "langchain-openai" }, - { name = "langgraph" }, + { name = "langgraph", marker = "python_full_version < '3.15'" }, { name = "mypy" }, { name = "openai" }, { name = "opentelemetry-instrumentation-threading" }, @@ -606,11 +587,12 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "autoevals", specifier = ">=0.0.130,<0.1" }, - { name = "langchain", specifier = ">=1,<2" }, - { name = "langchain-openai", specifier = ">=0.0.5,<0.4" }, - { name = "langgraph", specifier = ">=1,<2" }, + { name = "langchain", marker = "python_full_version < '3.15'", specifier = ">=1,<2" }, + { name = "langchain-core", specifier = ">=1,<2" }, + { name = "langchain-openai", specifier = ">=1,<2" }, + { name = "langgraph", marker = "python_full_version < '3.15'", specifier = ">=1,<2" }, { name = "mypy", specifier = ">=1.0.0,<2" }, - { name = "openai", specifier = ">=0.27.8" }, + { name = "openai", specifier = ">=1.0.0" }, { name = "opentelemetry-instrumentation-threading", specifier = ">=0.59b0,<1" }, { name = "pre-commit", specifier = ">=3.2.2,<4" }, { name = "pytest", specifier = ">=7.4,<9.0" }, @@ -701,172 +683,246 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, - { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, - { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, - { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, - { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, - { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, - { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, + { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, ] [[package]] name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/55/18dbb4778b30ada5ce071608503cc3edc9e14e13d868c17a6d178fc30f7a/markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346", size = 11520, upload-time = "2026-10-02T23:04:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/6c/14/0b05f79b4733e264a18d08fe08fa1df7347630ff32a6cb82180d9dccec55/markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91", size = 12063, upload-time = "2026-10-02T23:04:37.291Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3a/63ba10b6c1463216b3e4df669a9f0e5a3b0c3071557d2e8229e3968c79fb/markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef", size = 24321, upload-time = "2026-10-02T23:04:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/5f015261b76ad633d187ef6f388b413aedd64a8773c4df59e530a0be5525/markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169", size = 28845, upload-time = "2026-10-02T23:04:39.295Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/26e594b26be40c2f1fec63ccf8a8b99d0335a5b2cbe84835c7d82a994375/markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb", size = 24701, upload-time = "2026-10-02T23:04:40.348Z" }, + { url = "https://files.pythonhosted.org/packages/81/a5/a513b76c139a3915b43404324e55c0b7979ae4f0d39eb6f075b0282e90a8/markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808", size = 22961, upload-time = "2026-10-02T23:04:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/4c66192c100b4542bcbe392ae06696b670f66927be3ac38a213234778ff9/markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692", size = 21920, upload-time = "2026-10-02T23:04:42.429Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/ac3662678bfbad649893117ada2ba44dc30bf56884e84f13154a792b10f1/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d", size = 23726, upload-time = "2026-10-02T23:04:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/fe47cee339180a69ebca3c57fb3483d0f5cbd1e8337d1871fb1d9c1aebee/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21", size = 22539, upload-time = "2026-10-02T23:04:44.618Z" }, + { url = "https://files.pythonhosted.org/packages/12/32/d55440ba140442800e02d799c9cb5ab597bf6ebdb1177b5ea39a11f797bd/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707", size = 23963, upload-time = "2026-10-02T23:04:45.656Z" }, + { url = "https://files.pythonhosted.org/packages/50/9d/9c86042cb364c2ad4c971e6d1247929effd25f714cd7ee11b05e6316445b/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e", size = 21506, upload-time = "2026-10-02T23:04:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/75/ef/5b824f03ba40c3b3652b6272d083d2fc4fcdd440de519a3a39ba2c3e7262/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7", size = 22955, upload-time = "2026-10-02T23:04:47.698Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/44cfcb5ea40e5e43cec7793ef90704ed0c475280839076c4345757eb8e59/markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5", size = 14094, upload-time = "2026-10-02T23:04:48.788Z" }, + { url = "https://files.pythonhosted.org/packages/91/89/f2b509f7bf79352e40117824c1070dbeafd4df67031d3fa98165a3134228/markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3", size = 14328, upload-time = "2026-10-02T23:04:49.801Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5a/ccf22672a0f64dc682306e288f0dabcb06c7a201f3bb6e6cbf86d9e8ad03/markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e", size = 14188, upload-time = "2026-10-02T23:04:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, + { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/7632a53360671a9b750cdbabaf9cdd89f18b42248b8e4cb42c0b0296e459/markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", size = 14108, upload-time = "2026-10-02T23:05:05.513Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/62495e180b7000aaf30000fff849e933f74264638057176cf46852500adc/markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", size = 14303, upload-time = "2026-10-02T23:05:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/4c24208776a65878d656996945aacfbfe010d3720d1a98fc0eb8491fc03b/markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", size = 14174, upload-time = "2026-10-02T23:05:07.617Z" }, + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, ] [[package]] @@ -877,17 +933,10 @@ dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, @@ -1094,83 +1143,65 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, - { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, - { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, - { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, - { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, - { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, - { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, - { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, - { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, - { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, - { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +version = "3.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/1a/a7075a8e8b0d3f5097d17ac3099017104b6b7b42012041147995d5b2da05/orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92", size = 223409, upload-time = "2026-08-14T16:12:12.654Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c2eb3b2900e5597db7841a4c6416ac2d90081bd956b02d4dd1833fa2b96b/orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10", size = 124015, upload-time = "2026-08-14T16:12:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/df/b49081766a75b6a37b3d33bdc0a39e492abab8441dd25e3e1998e7b83fcb/orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8", size = 113471, upload-time = "2026-08-14T16:12:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/58ea28eeef95c2a27358ed927380a621162cf20bd740bbccf9c3f09a200a/orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3", size = 129998, upload-time = "2026-08-14T16:12:17.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f4/1e82aa2efc9916422d804697876ce433c907a1abd7c7e5c6d3d48565e5f9/orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e", size = 130891, upload-time = "2026-08-14T16:12:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/15169e9d22b59a406264f99d6db387c0b0b12b6357a8a0169917c2a713eb/orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5", size = 131285, upload-time = "2026-08-14T16:12:20.251Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/763dbd426290d044ec3e615a05e70adb6d8b6f95bf17dc355c0081a5e8b6/orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998", size = 135707, upload-time = "2026-08-14T16:12:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/3b2038ed168d22e14182ed715d6963f9c073a83a2ba43cfe918a4fc43c64/orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e", size = 127669, upload-time = "2026-08-14T16:12:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/88/ae/b84b3d3e65f5629ada0edcb1d2bccc55d7c5f89d8b981537ecdc3d6f31ec/orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710", size = 128043, upload-time = "2026-08-14T16:12:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/35/24/2ed0e6f51ea3d0af45d807233a851175af75bec83ef5fd0d6a2601904ec0/orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252", size = 122084, upload-time = "2026-08-14T16:12:25.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/95d25fcfbc9471799ef6bb01c552d64ee5cde93ee40ba2f423dd3442c708/orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868", size = 127035, upload-time = "2026-08-14T16:12:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, + { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, + { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, ] [[package]] @@ -1179,14 +1210,6 @@ version = "1.12.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" }, - { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" }, - { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" }, - { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" }, - { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" }, { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, @@ -1285,15 +1308,6 @@ version = "0.11.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/91/c7/e0b3bbe72e0003e5d02726e0d406ea47d523a2aec9c41d831817a8e0bce1/polyleven-0.11.0.tar.gz", hash = "sha256:d74d348387cf340051711c0dd6af993b4c264daa78470098de16f4a2b725785c", size = 6407, upload-time = "2026-02-09T09:41:49.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/6c/34c6189c80adf7575fb2daee38c4b836c154e416ab3c14d17afa7f88b9c3/polyleven-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ccf87f6ac8d76aa4c48a4828becc0c19fd1589b14b20affe23e5e012be4fa64f", size = 7421, upload-time = "2026-02-09T09:40:33.243Z" }, - { url = "https://files.pythonhosted.org/packages/e6/21/b20d3c9f9b6bded43a0388037044f2bcb1add20fa9a758d1144d79e09d10/polyleven-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1a02e3f0acfd1164cbaea25192398bc943ee9b93b9883a1fba9b2613d3616b0", size = 7514, upload-time = "2026-02-09T09:40:34.514Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d8/60290fd8d8298671edb4ce221d8ee4d81156b3c21b1154a615da7ea8e57d/polyleven-0.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6526d2516b439065864722069de6fcc418a4135696990dad66b81ddb18863bd9", size = 19556, upload-time = "2026-02-09T09:40:35.868Z" }, - { url = "https://files.pythonhosted.org/packages/c1/cd/17f1f6009a344c18f4213edcc97a9e7dae22e9a26e722aae10052378a43e/polyleven-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65fc01fe6cfe287f2f20170b35687a436ab36b882db568a55d81d6e0acd8379d", size = 20303, upload-time = "2026-02-09T09:40:36.99Z" }, - { url = "https://files.pythonhosted.org/packages/4e/18/30c7da8056adc4b9a81775f5b1810a2c9b6cc87fc34c8cf4c0280d28cab6/polyleven-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4b8e5ac8faecc6daa7b3d325436a3f23f8c33dec7bfca5d22df3fbe00f92ddd9", size = 19565, upload-time = "2026-02-09T09:40:38.471Z" }, - { url = "https://files.pythonhosted.org/packages/de/14/86e9c33ff9fda84297556373a6376100cbe9bb5d917fc3421dce4ada441b/polyleven-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc4f17007b07fde292ad33ec43a3ae8febe27a5bd92462b920736fd81d774fce", size = 19365, upload-time = "2026-02-09T09:40:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/be/18/1341f7860bbe2287f6cb8a540c3435c504cfc58659b67106ab60f695175e/polyleven-0.11.0-cp310-cp310-win32.whl", hash = "sha256:cae70197d545a09bfab8d7e506eed66ef314fa6c4e7a5e2c402c2febc31db74b", size = 11672, upload-time = "2026-02-09T09:40:40.822Z" }, - { url = "https://files.pythonhosted.org/packages/6d/09/ed2cb3dbec7a925d80107516b06dfe10dd368c6abfb43765df6feb6cd551/polyleven-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:bf47079b6dc62e6af2bd6ecb45a6087efd9a27b61666b98d0326c246a22ea991", size = 10828, upload-time = "2026-02-09T09:40:42.224Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/cb74c2ffe4e35935d80ef6f180f9e0987b8000917d52050a3563b32e1e73/polyleven-0.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:4c78b4d3e7d7b74315d5422178118963374c0cf3d7a9532a955f446ed365320a", size = 9389, upload-time = "2026-02-09T09:40:43.172Z" }, { url = "https://files.pythonhosted.org/packages/6c/7a/27ea9a78b617ddb14c2f5d2416df2fbf07fa5e52685f2968686a0308c8af/polyleven-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a28860fe33a7f907bc5f86e55a0b9faea80047d1677fa23b4d6c631ccf91ef2f", size = 7420, upload-time = "2026-02-09T09:40:44.505Z" }, { url = "https://files.pythonhosted.org/packages/a6/9c/fea309d41502aa5a344a6d4d6e5b8bdabb1df1e28f1af52bb53180f6c956/polyleven-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:47a3fb5b8cb60f647d2832d38b7d87cda27da8622b27c1292bceb9a04954c189", size = 7514, upload-time = "2026-02-09T09:40:46.44Z" }, { url = "https://files.pythonhosted.org/packages/48/77/c7d3bb6c66050304c3fe3cae1a716f62fea947ac3f14d02ef71e24422f76/polyleven-0.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:209fa669ca23ac453a7e9fbf07695350d5cbe61d71a6226b861757ccab28e664", size = 20887, upload-time = "2026-02-09T09:40:47.304Z" }, @@ -1374,7 +1388,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1382,127 +1396,142 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/fb/6e44b63b26efea1cec48c26d8362313310202ef5ed6e7a52f1669e64e2cd/pydantic-2.14.0.tar.gz", hash = "sha256:8a51a7aaddd60f55566d1f07bdd87b92b463903f39a8f26b71a06314cd1548ae", size = 898613, upload-time = "2026-10-08T14:34:48.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/eb/9146591cc819d040475bf7f2be786710c7f7eb8083693bf859728da2ca9c/pydantic-2.14.0-py3-none-any.whl", hash = "sha256:15fab1bea6f1dc5003b54fc2ecab230c1fd1dbade2acd4addc52d81e32416d4b", size = 487109, upload-time = "2026-10-08T14:34:46.864Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e6/6d/196e8c819e0e934f35a1a33b3530396feadb0af4ca38fe9f995249e55794/pydantic_core-2.50.0.tar.gz", hash = "sha256:84d2d38f7d163c4dec292f379e9de1960c661795442aca6c90d706436cb3749e", size = 513355, upload-time = "2026-10-08T14:30:58.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/99/114c2e71405da7610c884c64f8370f85b2fbb69bd427158a3f714515a803/pydantic_core-2.50.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:55684bc850059fc85ff8dc21a3e342b722d3178993c49ebdfdee8b698a432720", size = 2152583, upload-time = "2026-10-08T14:26:12.913Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/fabcfea81a5c3525507af4347fa3a50e51036d5db6611a63e47379776e56/pydantic_core-2.50.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3ae59461625518449f800acc4f874753ae96fcf993c47a27be07beb651371fd", size = 2045652, upload-time = "2026-10-08T14:26:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/226019008a041da423dc59d34f980b2b6e60a71971d07a8f52e10fc4747a/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7080928078ff56c07da393392f772054e93a39c8033dc9c04547bd949f9b614", size = 2050325, upload-time = "2026-10-08T14:26:16.081Z" }, + { url = "https://files.pythonhosted.org/packages/b9/29/11c98cd9c49338f64aeb3d6e166c99b65d8949f00f285c5b66de75ade316/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b1bae96f41806b14dd8bb1568d39b5e33b5af7fb27f7be36c78b8fa84708917", size = 2189027, upload-time = "2026-10-08T14:26:17.581Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b7/af7d9c4de715cc13e947d1135b73f0c181bce2f01e9f1d9e3c428f9ee31c/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8facaf0a16121ac82cc403a71399a061b74624c1ae2034eec9ed6169c5d16ecb", size = 2388815, upload-time = "2026-10-08T14:26:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/cd95dcd4d066180b2e22eff3dca01ac3af56607f9ecc43b7ba0748496f47/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:610e9f483ac53c5cc59c2d3687224029b54eb494feec2d40a2bcfdd187635192", size = 2431821, upload-time = "2026-10-08T14:26:20.958Z" }, + { url = "https://files.pythonhosted.org/packages/f0/14/7122b78e9915e7dd50f9dcec6041a4e32874907b41b9e0880db2f1c7ae1a/pydantic_core-2.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac223ea031905319a64d0df116979ae438b42d6265862a3d63c9f9808c2905a7", size = 2157622, upload-time = "2026-10-08T14:26:22.947Z" }, + { url = "https://files.pythonhosted.org/packages/97/86/fcddead5bda54fc8efa0d80dbba5962e9a91193570e227404c7593707833/pydantic_core-2.50.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:75b451ec5e64a1d4b803317f34710131742ff2e42f4bf5403f597d0af857d7d8", size = 2254921, upload-time = "2026-10-08T14:26:24.607Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e8/041d1c3e3e1f203a656ce592f8c1548e1ef2b0939d19c5ce66432c30e4bd/pydantic_core-2.50.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eee44c3f1f8acc220a743a5ed5588949a4f18b33df4cfbef2b1277470285401", size = 2268163, upload-time = "2026-10-08T14:26:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/f515994ef1351e883276e85af0a50d839ead9d0d22743fa2c6dfd552f7ce/pydantic_core-2.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f44107b5fdfecc03438feb165431652c8006650471326e53dd86d4c19124f5c5", size = 2302005, upload-time = "2026-10-08T14:26:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/b970c4c87d92c4c974a5c5dc16bcf747f89b4e5729c5c45131267ef0421b/pydantic_core-2.50.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:718d05d4e078c7828f40ad3e310870fe4b837d93a32e7e8497a080c1f1040490", size = 2465387, upload-time = "2026-10-08T14:26:29.529Z" }, + { url = "https://files.pythonhosted.org/packages/4a/84/c0a92f97f9de7f133dfe694d0e8ca033a1c90fdf692dc69dfee3bf8a2e7b/pydantic_core-2.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:031867348c98ab49c6d3a16ad39738369ea48da59900d76d71c1e64de71fe79b", size = 2478673, upload-time = "2026-10-08T14:26:31.898Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/88e851761351cfef90382f175b07cede48edafc62a1509575dbc879934ef/pydantic_core-2.50.0-cp311-cp311-win32.whl", hash = "sha256:e41f9d1d9240e8e0d8a670ad3e66c0c00f0b1f7150a31bc6c445a4f87c1cb3ba", size = 2121424, upload-time = "2026-10-08T14:26:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/89184f13b9ebaf6d8710cd0d1cfb466d7dc829de23468ab471b543dd07eb/pydantic_core-2.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:90874428bc6678b26434336c77931c9734ecebedf50132c179205e5a9908761d", size = 2176278, upload-time = "2026-10-08T14:26:35.388Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7a/5a630fff51218a860aa4e7699f781a7c9bf41c031987b613629c98f16618/pydantic_core-2.50.0-cp311-cp311-win_arm64.whl", hash = "sha256:b9be297ffe1015bfb2db4a23b6e1fec7e48361cf7c7b4f4f6bbdd008451b0a7b", size = 2183464, upload-time = "2026-10-08T14:26:37.485Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/e0fe374bc0082dfd337ca319505c9302ce383aeeeacc33d93383edd159e5/pydantic_core-2.50.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:79e8fc9c135ef628c45cb8aa5deef8d21de13d33bb4c59a859fa73d335ceb40a", size = 2222860, upload-time = "2026-10-08T14:26:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7d/0a2f829e3e1d809393faab907e3d9307245cd6043ebf54fad439f72f0003/pydantic_core-2.50.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0abe1b44d361b948404b6b2ed80be2583e0077340572e071afa6e0eda4e1de30", size = 2025752, upload-time = "2026-10-08T14:26:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/67/d4/e2808af12de4809dd6a4b532e3877baa5f1000df51bd8ebb8756450c6c92/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae45853d25a23fba56681d2f9ed41f3e3f12f0a3b2393fefa08ff6406320a1f5", size = 2048432, upload-time = "2026-10-08T14:26:43.593Z" }, + { url = "https://files.pythonhosted.org/packages/18/58/46ad42a321051585d89f4e563b115015332caf79ba4e96f2785715c7f52d/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd841dcf394ff261c26763a9d7be754176d4f1e6d26cb2a5d5331e91b6b56a5f", size = 2173565, upload-time = "2026-10-08T14:26:45.299Z" }, + { url = "https://files.pythonhosted.org/packages/92/69/541206e657ecea865e5f6d49a4a0d8062586d05da482d9d7ed77fdcdfc86/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2e97985641fe53ad7824d1b5bfeb7990a5ff788c4559ee44d7522642560ddc2", size = 2361974, upload-time = "2026-10-08T14:26:47.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c0/cf4850441d0d3d3736abe5e8b6fcb49a50166aa0651ed8444172f81f4b4f/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7987866a2569396e6765c54d643fd6a7b3b234e89ee8d45a3729a7b4b2726145", size = 2440817, upload-time = "2026-10-08T14:26:48.937Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e2/2f793fa2f1b338aa03124efd4362956c84d7bf1b331dc86050bc0151e67d/pydantic_core-2.50.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f187030fc3d62c668feb0f09e92852e0eb414d7fcefc4748f2e67d245aade37e", size = 2160358, upload-time = "2026-10-08T14:26:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/35e314c08e721ec1f070408a4e41926b90347ecd2863c08c78daad5f8a16/pydantic_core-2.50.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d187f43d1c5b844adc871c5b8c22b4aa12a116aaca4e9bd1521bc9ce479aae1f", size = 2231416, upload-time = "2026-10-08T14:26:52.733Z" }, + { url = "https://files.pythonhosted.org/packages/0f/3a/6f7c36afe35a9eca24e5f74f33ca641ac8cf308aae4ac7af4e585d464a40/pydantic_core-2.50.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb4fcaabb28cabf21396a9b816b9afe091f8c776805fdf03e2cbc606b64dfa7c", size = 2242496, upload-time = "2026-10-08T14:26:54.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/77adf30285836c25f6a927e170df45f8dd4af713c89135b22abcbf3c6d67/pydantic_core-2.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1751e92d56fbb623b937d73985e500b1a2b1053e56f718a6c564660d99bb9cb0", size = 2279176, upload-time = "2026-10-08T14:26:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/08/37/4e2a05247f82b59337bf45e9bb14aed85e2d83aecd21e49ad6cedf91882b/pydantic_core-2.50.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:70df7ff903aea05383298715ea53551c8f27c8f70cbe54ba7f05606cd822e7e4", size = 2450488, upload-time = "2026-10-08T14:26:58.218Z" }, + { url = "https://files.pythonhosted.org/packages/85/e6/75f25906212ecb94d69a0c8ed16f5c4ac312dc9961c2e93ff2a7fdf9a07a/pydantic_core-2.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:24302bf47319a64e5c5c7c29e971d2b7a20a190c64e59035a3df9582dec636fa", size = 2476664, upload-time = "2026-10-08T14:26:59.98Z" }, + { url = "https://files.pythonhosted.org/packages/75/72/ebb97b3becd0c0f2c722f698d34dec951ab1aa6086edf535f8769b545e3e/pydantic_core-2.50.0-cp312-cp312-win32.whl", hash = "sha256:5dbf9f18c8af11db719e67633be0af556d7d765bee0ca9419bd706fe4b7ed9fe", size = 2102409, upload-time = "2026-10-08T14:27:01.846Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/5ebf3e62f5d0f6e3503ffdfffd7d0c2f8d45690afd37fa0cfdcb1786d7d6/pydantic_core-2.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:1541c334af5d42cb9eb03862a9b4d2cfbfc670fd05172ec51f3ce02d704550f1", size = 2171536, upload-time = "2026-10-08T14:27:03.797Z" }, + { url = "https://files.pythonhosted.org/packages/58/1c/879ee9d5b63c60e5a077bab74ad93a4a48476090ca9c97534ab4263d0bae/pydantic_core-2.50.0-cp312-cp312-win_arm64.whl", hash = "sha256:b1399f918aea8fb76ffa99b474c9b768accea1f079fde407ee538bec89f20fa7", size = 2162946, upload-time = "2026-10-08T14:27:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/81/25/f9a6958f73d92f66d620e3e1b091becf6b7a2ea89437118d397e2c6ca9ba/pydantic_core-2.50.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:049b0404792dcb942f1092bfdae5f819ef30445b0d174782e1909fbdd91bb48b", size = 2222904, upload-time = "2026-10-08T14:27:07.869Z" }, + { url = "https://files.pythonhosted.org/packages/c5/41/7f299b2ecf0ddbec8c2a68057ed53d29458ea5b0850615570849c454dd89/pydantic_core-2.50.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8f16bc5bb12f4c581b0f40facc28dbf286db32d1bf4e7e4f4c4e0f7f4e34ecf9", size = 2024863, upload-time = "2026-10-08T14:27:09.78Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/a9852fa8780acca5dd81a21bb66d4b2fb41c39dc5672cec66c32cc7f13e9/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1d084e92d0f4a096a5155b23d1ac603db8073ef98a3d1384badf1455e5ae742", size = 2047340, upload-time = "2026-10-08T14:27:11.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/82/cd174e776e71ebcb1d289a57cb3e89565123a84c5708865e12efa82313f9/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0af2d2f745ec00a6616c4dfba4477f9156ccfb45690f6ffa5fd34f42e871ed4", size = 2173461, upload-time = "2026-10-08T14:27:13.948Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5c/9c4b2aea09ec7b7af966e79631b1970f1adeb6a8ed001bc8bb64a2d1c23d/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f28a5a299d4cefd1066a6883d22600b9ae3606f881c3015d255784634647580", size = 2362629, upload-time = "2026-10-08T14:27:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/93afd007b61c50425c3c8f0402425779341c8d498bd224e5788eaf419c4c/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdd70c2d9e73bca09fad54605dfeaae2b4e770b2800c65f5f5342901ed567b9f", size = 2440134, upload-time = "2026-10-08T14:27:17.716Z" }, + { url = "https://files.pythonhosted.org/packages/c9/93/a4ef199535547aaa7c5d2cbf009a1d6a2b252885b14c6e70404e1c661716/pydantic_core-2.50.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e58acd43ac8d3905659c1d5309318dd576243e723dd7c3b5dd4f555479b77d4b", size = 2158480, upload-time = "2026-10-08T14:27:19.618Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/f216e028d3bfbd9f8c67f7a2f0f8db04240926f630e2e66a5a8ac45fe00d/pydantic_core-2.50.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:5a07c644047f5abc268b4c39f8cb5a30e08a3c349228cb70142c7d3ed87587c0", size = 2231837, upload-time = "2026-10-08T14:27:21.742Z" }, + { url = "https://files.pythonhosted.org/packages/56/3f/57a6acf26e0acb82238ba4e25b35cbd6013b1d74eac99c4f358ff56bf6d6/pydantic_core-2.50.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8ad8dad549cd1645be591a50b4573995ee7e018f6619ddc2bc6ea44b2ad9f694", size = 2242240, upload-time = "2026-10-08T14:27:23.564Z" }, + { url = "https://files.pythonhosted.org/packages/94/41/f5f4014b40f91db4479e87dc038ac48a460a60baadea7ff9c942bc64dc62/pydantic_core-2.50.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1773001198030e16f946a7ff7760fc3ebd45b12150f66fd0f433780043dc13d8", size = 2277852, upload-time = "2026-10-08T14:27:25.517Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fa/485e4db093e10f29676fc696abf9d88f3626ee520e99cc488fb091222db9/pydantic_core-2.50.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:8ab9e74878172948e0426e3d27fba333fd6a1c3f9456d8e75643e8e6868ac1a1", size = 2450132, upload-time = "2026-10-08T14:27:27.493Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/fd07063cfc65e498528cea59b27b979eb5c890a54dcf5bb19ab0a589516d/pydantic_core-2.50.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e15bb1535f68a27f28e579ba3a8f74e7b05d350c45f5622b76a311d5a19ae48a", size = 2477011, upload-time = "2026-10-08T14:27:29.972Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/6f34c285dc87ed2fae5f1b53c51bb732eda21dc0c6c73d6a5d9dcc7033cd/pydantic_core-2.50.0-cp313-cp313-win32.whl", hash = "sha256:c21e6a6e4e6d32fb6acbc4f0fa69e8319cac0d65eeaa8298d757371cc2a9c687", size = 2102027, upload-time = "2026-10-08T14:27:32.497Z" }, + { url = "https://files.pythonhosted.org/packages/5e/50/dbdb3ba6699d494e59db2f145aeb97990e28d1503bc9f7bdd57eb4c15677/pydantic_core-2.50.0-cp313-cp313-win_amd64.whl", hash = "sha256:4f96ccd9368ecbf6685d8f6981584ab72b4f0ffd20685e747737e7e340277b8d", size = 2169974, upload-time = "2026-10-08T14:27:34.692Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ae/cfe0e52a9b45b5ba12b3a839db928669543948760a059a72f13cf0346c29/pydantic_core-2.50.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2b88f9f9fa52e1c34938ff1a18ee7fbffe482df0bb43c087a9a60578d273d68", size = 2162504, upload-time = "2026-10-08T14:27:36.989Z" }, + { url = "https://files.pythonhosted.org/packages/59/d9/6dd838672e5ccddf01556bfd1b4a6767e0c75abf9aaa4092cb87a56dbc4a/pydantic_core-2.50.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:62a93a9d206a3580c975c3c1f65a869cd063844d016c004f8d786a9309b3c591", size = 2224496, upload-time = "2026-10-08T14:27:39.281Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/c57d2efcc63fb8fb6ae9ec2818baf30e2a68751e8d1dedefaf3dab2e45fd/pydantic_core-2.50.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5a8403eef4a66743e339102fb3cdd8c8b9016b8bd67924685893d062c88896f9", size = 2023860, upload-time = "2026-10-08T14:27:41.332Z" }, + { url = "https://files.pythonhosted.org/packages/26/c8/f44ea3f1b00288f715e2320cbcd0f604104325370ef4bee42c5ed2ab076e/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8354fbbe2abf0fb724b9303bef43bfd9b7a2779332813fa6d6559983954e7d8", size = 2049144, upload-time = "2026-10-08T14:27:43.695Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/2f5cf84ceaf6d7c351737124aebc135f50ce48a9dd28d2b5283103face54/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00963fde61cf8880d9e7b5635a9830e0591edfa45168447fdc5b47635c0f6437", size = 2173456, upload-time = "2026-10-08T14:27:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/97/de/dc0bd815a328e62b72bb93b5f3c762939670d4119fd27405b454970833da/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bbd7da16d8b2912cc56c9d0b85c4998a6cbf39b220f81c0d8c397c64672ae0e", size = 2365204, upload-time = "2026-10-08T14:27:48.117Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/458c4a29f52fdecb16b192fb27f117359da9c9c58401ebfd956637822dca/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed557fa2744617eac3e34dd39b85037efbf23cd33f06851c00fdb8f18ad8f4c2", size = 2445007, upload-time = "2026-10-08T14:27:50.637Z" }, + { url = "https://files.pythonhosted.org/packages/98/14/0c0e72e0663be91a456167a2f7ed49c65195b388e8f1e8b2bb5083b27458/pydantic_core-2.50.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f816dc04e99627a5a6352ae51dfb30e603f2cb0b7009c91dc640c533def014", size = 2159911, upload-time = "2026-10-08T14:27:52.881Z" }, + { url = "https://files.pythonhosted.org/packages/62/7f/64af6921e17ed04dda0e07ebbd394b29fb557d33618cb0f3f81ec59c0508/pydantic_core-2.50.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:779b6c74526596a86d38248dedaecfb7851bbaf319c234be042c57acddd2c8c4", size = 2235761, upload-time = "2026-10-08T14:27:55.194Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/b6ff9f7207af629094f4deac947d04be3812d5c95054010ad8f710a052f0/pydantic_core-2.50.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:21b62d45f327eb0802f842c132fda6d01a8376a8077922dc4dda69011c64d34a", size = 2243726, upload-time = "2026-10-08T14:27:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/6f7101f3c746a1686987a32875ae2f7c19f518d783979cee1a8a32e0a9ef/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f31af62efd1fd0257735b72e6716b32d4f207654adeabfe524d44baf1bb6bed", size = 2277158, upload-time = "2026-10-08T14:27:59.714Z" }, + { url = "https://files.pythonhosted.org/packages/d2/74/67fe208f3ec7f5d8c47bf7cba70019a84b1cffad947ebc8caef14271b54f/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:933f2d639eb81a3e1f145aec415453cc00983236629933f028d9507222583a2e", size = 2450059, upload-time = "2026-10-08T14:28:02.17Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/4a9c56a69f5841bdd94ffff5876a12ad2733209d5b89cadb67638d69e7c7/pydantic_core-2.50.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:c05d035e72530f6b00297941b0162218601542b76870c3bf2756bd87f16fc538", size = 2479142, upload-time = "2026-10-08T14:28:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c1/30d36746051e42bb67df986ee874f787526edb99205f6b1583c33dd81202/pydantic_core-2.50.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab3f95f737fc1b258b8210308fc02ce1442cf5950cf6d30b09ad89ab9e8afebd", size = 1483929, upload-time = "2026-10-08T14:28:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/04/2c/c0e8949be4f99a74f02ae02d603bdd518c8a9eba72982a34d675134a792a/pydantic_core-2.50.0-cp314-cp314-win32.whl", hash = "sha256:f12d9690634414fc04b1a7072fdc35c34a9242232c1851fe4518383578bb09d4", size = 2104255, upload-time = "2026-10-08T14:28:08.729Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6f/a5a5baf99509c998e0f2335d66b815880c7d6c63231c8e4017b237d6837d/pydantic_core-2.50.0-cp314-cp314-win_amd64.whl", hash = "sha256:63263d64884688554fb025a3906c7b00573980cfee75f9afeb86239d577384bc", size = 2170939, upload-time = "2026-10-08T14:28:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/40/bc/c89b93b69d59cfe61c1e91b1cd3d87cc740e57327ecfef84ce34b7dcadac/pydantic_core-2.50.0-cp314-cp314-win_arm64.whl", hash = "sha256:753863dd4317ec8cc9eb3e6d9d01a8ef1a1726a8003b4354658d68a1ae05f9db", size = 2157035, upload-time = "2026-10-08T14:28:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5e/d0ad47a406c42db95876900f4bfc520313c28d07a0dd5080da4972220c27/pydantic_core-2.50.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c4abc425789f8540e86ca4cfdbc8dc650433cc2ac7c6136fee9bcb29d5665a02", size = 2234007, upload-time = "2026-10-08T14:28:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f4/69a7ec8400c1e34e5e11ab967f850ffd8ca71bb3aa0a8db93df5912bb6f2/pydantic_core-2.50.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf150693a51ca21e8288cd08a9de05e5ab331776dfcfd0537b14523338f0502a", size = 2000888, upload-time = "2026-10-08T14:28:17.983Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/860bb499f7cdf4bb453e8080af5bc182487f2f025d94baeee29c907bde6f/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158749408ee19682b8a2a7e7135cced7f41d6f2f9de96088b1b1609858a6a158", size = 2022761, upload-time = "2026-10-08T14:28:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/89/fd/f571420436e79b9f7cc8f8ec534dee1759c240b4dc086737608f14d05c93/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcbd1fe5083315243447c13f7252ae9fe9d128b1ae2857e3a916c609235dd863", size = 2192910, upload-time = "2026-10-08T14:28:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/b47c4a01ea978a7a3d02551e3625f0344ae1352d67ea78011c2eca5e39e4/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fdeda6272d60b1f6fb63f6a3dade55e274af62e1514d24a6549a12150c385cc", size = 2390178, upload-time = "2026-10-08T14:28:24.861Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/d109f26b3ee4443cca220eaaa4f3fbcf78ff07aeb577becc37d9224db63f/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b16e164205a90b1050d2f5469f8a7829db7698ad198c69e6aab6cbfb5648b87", size = 2464453, upload-time = "2026-10-08T14:28:27.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/a4/7be2f608f6f8e7655155057a82a7e2f135ad9b37fcbadd34fdf11b1ade24/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17a5ca9c197788a6424749a09a3dce824cb2c17b72f835f8a5e330935b973609", size = 2150682, upload-time = "2026-10-08T14:28:29.65Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/a8d867f6d3981c232d0f3c2254458cdee8471de7bff3f7edd252cc00c30c/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:01340f4fbb4f854b1f36a6fe9dcd2b26c8936aead4e4ad1205624ac025c875fc", size = 2245995, upload-time = "2026-10-08T14:28:31.934Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a6/7297a39c8814beab877be5ba4f594c2e166108cdd20c1f1c51fb71d5389e/pydantic_core-2.50.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99e203d5c2a814facef78dcd993b0fb7a933124a3475a38979fc09cceae210b7", size = 2264090, upload-time = "2026-10-08T14:28:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/c3/65/634fc407eaf61abec0d015d6fd4fd8456c18c71358e984f60b0a05092d49/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:2092ce156f92aff17e3345baba2b7c0c1701f32ba922c5de71bd6248fbe164e3", size = 2293708, upload-time = "2026-10-08T14:28:36.725Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/b87fcf8062b8907c77769385a4340a814fe359660593dbbe4d8f3f7e9e61/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:74cbb6cbd74445ca279668790e0c10eccac0428fd79fe061fce6c9e3982ad3fe", size = 2468464, upload-time = "2026-10-08T14:28:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/77/f4/d7aacec95f9e00dcbad84bee2fd0fc081ae292d380dc8a3268e4103de957/pydantic_core-2.50.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:42fff617cb0b08505d8123d71e6e7a8e54210d9007498f564855bd843ce984b1", size = 2511897, upload-time = "2026-10-08T14:28:41.681Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d8/0a755b0069f9a0f4c57f98fd05b524473952556406244649d632d25f8646/pydantic_core-2.50.0-cp314-cp314t-win32.whl", hash = "sha256:36f9ed6ae1069913e4f6e86d8233e119e83e00a20c54f88faf9c81292f2fecc0", size = 2098906, upload-time = "2026-10-08T14:28:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/cd10a1fbd1ba1d730871e382ec2ac550487465c7c9be1a72b3f4f9181817/pydantic_core-2.50.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c81c2ec53ea9eb2227c14b3e6de35670b2de163b638f2a803e90a3bd5bbb0", size = 2148359, upload-time = "2026-10-08T14:28:46.47Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b1/a766eadfbc16b58d401f63dde3bf4e0943dcd2402011b6b8d9de2031ebe0/pydantic_core-2.50.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dec0dafc116ac29a84d143fdbc3b83fdb5d4ed339276be2251154537ab30e14d", size = 2175241, upload-time = "2026-10-08T14:28:48.934Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/43d0e765a80d6fc7d87b5f66a9023777aad6da7e7d64f486a4c2be8158b7/pydantic_core-2.50.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:af2b808a79bb04075e87c81a5b6179365b93f9a851f29dafd67abff72085d0c8", size = 2224750, upload-time = "2026-10-08T14:28:51.227Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/07e047c21ad90f184c7e8bfc6c9966b9ca51d4698e1a06460b65ca2ce105/pydantic_core-2.50.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2918547195ffb20118b829fdb8938e9dc92c9527fe6cbe58572594c96362880e", size = 2023936, upload-time = "2026-10-08T14:28:53.978Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a9/fb26fdd343ead65a699245c9d2150e84c47496a9b250859214a90cceecc9/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0393763d66f6f61715488d074a2cefac04aeb3ee281e36fb4925dd44deaf9e17", size = 2048872, upload-time = "2026-10-08T14:28:56.342Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4d/9fc4ea28a8ecc1b85fe77a616f8df9a51696476aaa93c5b256269d025044/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62ec6568896e0abf258cbcd22c406c1c8bf27d16224b21bec8f75c4ae88a8173", size = 2173752, upload-time = "2026-10-08T14:28:58.805Z" }, + { url = "https://files.pythonhosted.org/packages/70/7e/74b55196339413d787b283c2568cca4679b0645e48229421228acfa8839f/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec786cb9d597dd75d993f8c1273e31bb2114c9bc22f67fba611e654e8347701b", size = 2365284, upload-time = "2026-10-08T14:29:01.434Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0e/4ebc7a851f0ef63fcaba205739c171e50dd183f8d79dcf0099c3b46aa60b/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fce74add1099da1ea09473268950270071fd56773e2968604efb3ab1d240e02", size = 2445279, upload-time = "2026-10-08T14:29:04.034Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/03d3524fc7d4960840e32fd88c8cf487b5516daa7b6845ffc74274e0247a/pydantic_core-2.50.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:243088c95e23b12db9f2cd7661d584a3f00814e087489f40cde7f9feac56b694", size = 2160424, upload-time = "2026-10-08T14:29:06.628Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b9/7c530c84049089033dd746700bfbb51520ed9376bc90f3dbbcb319880acf/pydantic_core-2.50.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:42a56b0052ac11d9d0d87b1c94a1ce52e31914fd133f269585e0f63a4ed988f2", size = 2236109, upload-time = "2026-10-08T14:29:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/70e07379ebc2538c22f8c372a37c009b96a3a1bd7921aee1dad28ee41be0/pydantic_core-2.50.0-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:350001f5573451150d919722ee095aa28bec037d6e23b86d7f04581a910fe924", size = 2244011, upload-time = "2026-10-08T14:29:11.549Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/294810baacae4b088bfac60edab06036951e5ae6e0c20e8f6cfef7d70f66/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:21e38a011783d8afc8b9d79928e273d06f349b52ae84edf63ec18ad07f077484", size = 2278392, upload-time = "2026-10-08T14:29:13.998Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0e/6990b812f124cf3564b36492799df7a66cd3cf5678004ffe65dda85b136b/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:588d309ce5c85448379556d72011b191c0414ee2e80d7c3f1ebe2ceb2d9027b1", size = 2450012, upload-time = "2026-10-08T14:29:16.524Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c4/f8f1a763550a077c51d2c69b7a32db56d1b54e61c613bc19e6c7fd2e7cb2/pydantic_core-2.50.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:a8ee3965e01f10e4ff92ba1727626328ab7ba93bcd5674ff2b78ea1048ee0cea", size = 2479233, upload-time = "2026-10-08T14:29:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/9d/83/7a09baa1e0e4710be47b84aaf808e7b86247d7fa43251ac8934099113f8d/pydantic_core-2.50.0-cp315-cp315-win32.whl", hash = "sha256:c05b75ef3574c9ee4e05bbcf8513f7ccb155d426514be9efef5f6f53152d5f5c", size = 2104507, upload-time = "2026-10-08T14:29:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/f1cf61f747538834ea2c14c442264e4f29663c95294ad7933867633a1dea/pydantic_core-2.50.0-cp315-cp315-win_amd64.whl", hash = "sha256:8447678b49412294801c9ea15ae31ea3bae7da65425268c92d40e032c38eac6f", size = 2171379, upload-time = "2026-10-08T14:29:24.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/c8/9871261b760672cfa334498d94bc7dcbff2495345e639e806127f7087254/pydantic_core-2.50.0-cp315-cp315-win_arm64.whl", hash = "sha256:92016718bcf3e6f35a6bd986880191a8da7a35aa1f5b1e97544582ef938464cf", size = 2157182, upload-time = "2026-10-08T14:29:27.082Z" }, + { url = "https://files.pythonhosted.org/packages/de/3c/5107269aee5ee7855fa370ff837998792dbf124b8545244c370bec251a66/pydantic_core-2.50.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:71061800a3c225e730f7997f8a7fad6e0d0dcbe36609cdc7e60576a099a2832b", size = 2234440, upload-time = "2026-10-08T14:29:29.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/cb179b0c404337ef517515539f9899164a6d3d43d5c79c2556165da211c1/pydantic_core-2.50.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bc87bd34239835c8d73171acd039e591cea0a2ca8615e6188044ad170a40fca1", size = 2001074, upload-time = "2026-10-08T14:29:32.327Z" }, + { url = "https://files.pythonhosted.org/packages/b0/59/ef1714204f145e9497ab71e8bd82c5e40b5adf365d420be777d1e047cdaf/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57c0e5b26d82bf31ab2b044781527b1b1a36e9ed400856b6aca5097eb1abb909", size = 2023016, upload-time = "2026-10-08T14:29:35.06Z" }, + { url = "https://files.pythonhosted.org/packages/87/e9/08a3ade34b4af09a83740b62a4442b7623127d388fc21d4938798dd85a26/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:41b9f2821f0a105f88cd54ad03fe1392ec600618ffa8610f7c5e466ecd98c531", size = 2193165, upload-time = "2026-10-08T14:29:37.721Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/29aa3e540d3aca72139e5b7dc56395a940849d85da2b7a9ccfbff71d0928/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:364f62d8024997536e57865cb1c8b36effad3249bf392a29a2379ea69db28238", size = 2390198, upload-time = "2026-10-08T14:29:40.616Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a5cbdde0a2090c47002819d85317428e15b41d22e581cf38e9714b4903af/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:955d7878130dfc124d6a5343e1b87d2933244fe14a4a0a3e98787e8e660a8eb4", size = 2464777, upload-time = "2026-10-08T14:29:43.29Z" }, + { url = "https://files.pythonhosted.org/packages/c5/55/fff3a363b0dcf72fdd72311c676b1fa769f2c13342129c694b2eb9886580/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf81432281af66ba3285b26b09c2d478d84730dc50ff90926c1bdcef54048a33", size = 2150389, upload-time = "2026-10-08T14:29:46.271Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f3/0062361fd2377185d322dc84cd05ec4b96da352195c89939c7b509664721/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:34a4a0938eb30931baca56e55786c5a6871ac7aa891a38c8cabcdb7e49dab91b", size = 2246225, upload-time = "2026-10-08T14:29:49.173Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/662e3870143d5f1632a02ceb9ce2d5168e88caf3da80e9b42f495ceb1284/pydantic_core-2.50.0-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f517a417cd02aa8fb05b603a0ac6d87b7b004c3ba4fcd279cad25cab7229043b", size = 2264404, upload-time = "2026-10-08T14:29:51.699Z" }, + { url = "https://files.pythonhosted.org/packages/32/5e/8b14ebf111700362c6e519e40209d86f521da81ef171b2891af0bb5de311/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:d01029d54ff1f45c195b1f7e6fbf6e58fb7e7e12cda9a6e639570d9c581decb2", size = 2294152, upload-time = "2026-10-08T14:29:54.349Z" }, + { url = "https://files.pythonhosted.org/packages/55/29/85c486e0d25a8b523803032e97d7138edb7b7bc887fa8c6dd085e8824cdb/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d06dbbfe8da01a0574de27afc19915bb3e7dddfbd184bb97e958f94051d8531b", size = 2469160, upload-time = "2026-10-08T14:29:57.193Z" }, + { url = "https://files.pythonhosted.org/packages/12/bf/c451db7567e92d6601c4d2f6bdbb945cf60104182e2b241d8fe0b6f01414/pydantic_core-2.50.0-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:5307a8bd49158b57a0ca4e10950d31085aa35d9007e934047043ccc6d28eea97", size = 2512204, upload-time = "2026-10-08T14:30:00.177Z" }, + { url = "https://files.pythonhosted.org/packages/43/1e/5b93a2513ced099acb0dc4dd6684742d6564ade60a59abcbabd161624e07/pydantic_core-2.50.0-cp315-cp315t-win32.whl", hash = "sha256:c2b246fa7cbdf9918488d1542a82bbb928cf71bcba66905c24131981e759ff0b", size = 2099026, upload-time = "2026-10-08T14:30:03.154Z" }, + { url = "https://files.pythonhosted.org/packages/37/f5/1b5967e31b025a23f2f627ffea1f5a874cff0adb72fa5f5af6bb36394ad9/pydantic_core-2.50.0-cp315-cp315t-win_amd64.whl", hash = "sha256:36c49d4e1769127461b609f110d91963790091ffcc2de401ac1d3b2f6a63bd54", size = 2148456, upload-time = "2026-10-08T14:30:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/213d6fe84816e8a5ed7b7539ecebb3f14c20739c2469f4e372f0f15bb042/pydantic_core-2.50.0-cp315-cp315t-win_arm64.whl", hash = "sha256:f3abcabb04503023e1053add24472e78878de0bfd5c8a93686be01f4032c363c", size = 2175368, upload-time = "2026-10-08T14:30:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/11/ff/8d126ca04417cbc76a81648c135296ecc2f36d07adfb0b3376638efd1dd0/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:38b03c5439c6a7a952f56e3e6596dae44bc78c3fb6a69df46cabfb2f888e5f0c", size = 2162541, upload-time = "2026-10-08T14:30:12.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/e3f11c40950d0f37b3891c1ed015f04a4ef36c881d04ef1a5d1ac91d1c6f/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:4faa766450ef44d9eabed5b65a280f63f903d1e1ed6d2e278961eb22345ec49f", size = 2075402, upload-time = "2026-10-08T14:30:14.968Z" }, + { url = "https://files.pythonhosted.org/packages/42/f2/727d2b23ff6ca67c9c050c4ff4e5abd7b599d9ebcd819a74141d3b11c7c1/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50920d66aaab60dbc6023c3035d55fbabfa58b6d205d0b39d47d820c4a7a13a8", size = 2108552, upload-time = "2026-10-08T14:30:17.774Z" }, + { url = "https://files.pythonhosted.org/packages/4a/da/a4321058eea61f0504366668a09472a4c37514be30cce77967c35ab7ffaa/pydantic_core-2.50.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586699b43066ca6038f96bd71d0eff0b6c292929e3d66ff63d5dc79d0f06d589", size = 2267745, upload-time = "2026-10-08T14:30:20.619Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c8/45e2cbac6379b8cec8cffd7e6aed50754fce13d93f0cff28bbbfe18c1d1c/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b916ff828d604d4311a5639b7b3da51eaea3923833ec3e300a5ee35eade99691", size = 2217049, upload-time = "2026-10-08T14:30:23.532Z" }, + { url = "https://files.pythonhosted.org/packages/61/8c/b81c4139bff6305e7a98fa7845a99e05df1461d3c33d0fef417e02114ae7/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:732efbb50977ab7cf18f01d4c255834aeb14cb2425459f7bff681a1ba3a4ffa1", size = 2039121, upload-time = "2026-10-08T14:30:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/0f/49/679293741809cf835290bccf80216aebfcf5d94cb84a34709f18b21abfd5/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad9185366893714cd4514ae5e1a098227704fa395cb41a7855d75968ecedc826", size = 2109280, upload-time = "2026-10-08T14:30:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/f5/49/76f167aec9b9d77a82e38131539ca6c6fcf69d720965f76eda20b87b454d/pydantic_core-2.50.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0def1dc09802a790e4a1b3cbc4401f0b53c58f273ce3671df9867f7bdf1fbe20", size = 2269313, upload-time = "2026-10-08T14:30:32.096Z" }, + { url = "https://files.pythonhosted.org/packages/1f/50/a6bd3c397b609aade207ffb78ce46f4ad4e29bb42a39007c87ee00e778f3/pydantic_core-2.50.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:68421ba548f6d86c7dededd70bfe530b4faf67eb811c53c70a1be69438d3ce99", size = 2154371, upload-time = "2026-10-08T14:30:34.965Z" }, + { url = "https://files.pythonhosted.org/packages/11/3c/2d32a1f945b9809b65701d2b72315bdcd700f13c093df5e60baaa784bc6a/pydantic_core-2.50.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:23e0940b7d73f405d92e98b2a05b93b16db163be13b7478bb19e1db2e486ade6", size = 2087028, upload-time = "2026-10-08T14:30:37.814Z" }, + { url = "https://files.pythonhosted.org/packages/aa/57/c2aea271192d8c4b81afeea9d61de8f5dc8686a1fcd1ace80e70a482fbce/pydantic_core-2.50.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee9913fa2b5bfa6111c11158cf481bb13544f8bd5d10382fb6ee2612b16fcf28", size = 2259611, upload-time = "2026-10-08T14:30:40.581Z" }, + { url = "https://files.pythonhosted.org/packages/35/88/32128d5498b95722fe914a21a25f59edd7dc8917474f47924005682532a8/pydantic_core-2.50.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ad3532291deedfdfcad3cf5d351d0076d646c68a0c63869a1bebf87c22159600", size = 2268636, upload-time = "2026-10-08T14:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/7dae31279e07aaa63dd4b17756f4ebd5647aed6bee5581ee3b3e85d82a0c/pydantic_core-2.50.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:9279f4be22bc3765dd9f61f5b4d38d9cb219d167bbd6f2e0ffa3368b1b71b4d1", size = 2305416, upload-time = "2026-10-08T14:30:46.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/61e47defbd490618fcde2641f80d5c077f2961de62e88500edacb49297f9/pydantic_core-2.50.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3b2f3e44af4c6512dd73557621385a71b18094c8be81d60e5a8e73a8ce96b7f4", size = 2466713, upload-time = "2026-10-08T14:30:49.214Z" }, + { url = "https://files.pythonhosted.org/packages/61/57/81327752c007834d684b0b7d20ce00087206f512c7347a6236fd89ef0223/pydantic_core-2.50.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:593dd4f24abeb3ed90222f98bc6eefcac68cd5aa96fc7745f93862a802952bce", size = 2481971, upload-time = "2026-10-08T14:30:52.088Z" }, + { url = "https://files.pythonhosted.org/packages/48/c3/e321d3a4b2737372cf775ffdd1a02428c7d2791ee5afc9f228793a0ee245/pydantic_core-2.50.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b123f9d8702106f39dc3af63a031ca8b5279862a3747ec6c03ca49dbe78b71b9", size = 2337958, upload-time = "2026-10-08T14:30:55.045Z" }, ] [[package]] @@ -1520,12 +1549,10 @@ version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1537,7 +1564,6 @@ name = "pytest-asyncio" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/1e/2aa43805d4a320a9489d2b99f7877b69f9094c79aa0732159a1415dd6cd4/pytest_asyncio-1.1.1.tar.gz", hash = "sha256:b72d215c38e2c91dbb32f275e0b5be69602d7869910e109360e375129960a649", size = 46590, upload-time = "2025-09-12T06:36:20.834Z" } @@ -1601,15 +1627,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -1675,123 +1692,122 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/b8/845a927e078f5e5cc55d29f57becbfde0003d52806544531ab3f2da4503c/regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d", size = 488461, upload-time = "2026-02-28T02:15:48.405Z" }, - { url = "https://files.pythonhosted.org/packages/32/f9/8a0034716684e38a729210ded6222249f29978b24b684f448162ef21f204/regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8", size = 290774, upload-time = "2026-02-28T02:15:51.738Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ba/b27feefffbb199528dd32667cd172ed484d9c197618c575f01217fbe6103/regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5", size = 288737, upload-time = "2026-02-28T02:15:53.534Z" }, - { url = "https://files.pythonhosted.org/packages/18/c5/65379448ca3cbfe774fcc33774dc8295b1ee97dc3237ae3d3c7b27423c9d/regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb", size = 782675, upload-time = "2026-02-28T02:15:55.488Z" }, - { url = "https://files.pythonhosted.org/packages/aa/30/6fa55bef48090f900fbd4649333791fc3e6467380b9e775e741beeb3231f/regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359", size = 850514, upload-time = "2026-02-28T02:15:57.509Z" }, - { url = "https://files.pythonhosted.org/packages/a9/28/9ca180fb3787a54150209754ac06a42409913571fa94994f340b3bba4e1e/regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27", size = 896612, upload-time = "2026-02-28T02:15:59.682Z" }, - { url = "https://files.pythonhosted.org/packages/46/b5/f30d7d3936d6deecc3ea7bea4f7d3c5ee5124e7c8de372226e436b330a55/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692", size = 791691, upload-time = "2026-02-28T02:16:01.752Z" }, - { url = "https://files.pythonhosted.org/packages/f5/34/96631bcf446a56ba0b2a7f684358a76855dfe315b7c2f89b35388494ede0/regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c", size = 783111, upload-time = "2026-02-28T02:16:03.651Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/f95cb7a85fe284d41cd2f3625e0f2ae30172b55dfd2af1d9b4eaef6259d7/regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d", size = 767512, upload-time = "2026-02-28T02:16:05.616Z" }, - { url = "https://files.pythonhosted.org/packages/3d/af/a650f64a79c02a97f73f64d4e7fc4cc1984e64affab14075e7c1f9a2db34/regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318", size = 773920, upload-time = "2026-02-28T02:16:08.325Z" }, - { url = "https://files.pythonhosted.org/packages/72/f8/3f9c2c2af37aedb3f5a1e7227f81bea065028785260d9cacc488e43e6997/regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b", size = 846681, upload-time = "2026-02-28T02:16:10.381Z" }, - { url = "https://files.pythonhosted.org/packages/54/12/8db04a334571359f4d127d8f89550917ec6561a2fddfd69cd91402b47482/regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e", size = 755565, upload-time = "2026-02-28T02:16:11.972Z" }, - { url = "https://files.pythonhosted.org/packages/da/bc/91c22f384d79324121b134c267a86ca90d11f8016aafb1dc5bee05890ee3/regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e", size = 835789, upload-time = "2026-02-28T02:16:14.036Z" }, - { url = "https://files.pythonhosted.org/packages/46/a7/4cc94fd3af01dcfdf5a9ed75c8e15fd80fcd62cc46da7592b1749e9c35db/regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451", size = 780094, upload-time = "2026-02-28T02:16:15.468Z" }, - { url = "https://files.pythonhosted.org/packages/3c/21/e5a38f420af3c77cab4a65f0c3a55ec02ac9babf04479cfd282d356988a6/regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a", size = 266025, upload-time = "2026-02-28T02:16:16.828Z" }, - { url = "https://files.pythonhosted.org/packages/4d/0a/205c4c1466a36e04d90afcd01d8908bac327673050c7fe316b2416d99d3d/regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5", size = 277965, upload-time = "2026-02-28T02:16:18.752Z" }, - { url = "https://files.pythonhosted.org/packages/c3/4d/29b58172f954b6ec2c5ed28529a65e9026ab96b4b7016bcd3858f1c31d3c/regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff", size = 270336, upload-time = "2026-02-28T02:16:20.735Z" }, - { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, - { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, - { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, - { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, - { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, - { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +version = "2026.9.29" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f2/af1da9d3ceed77bfcdce40427d49ba0be94e4fe84245e3bfef68c10e75b6/regex-2026.9.29.tar.gz", hash = "sha256:8b5fcc4771732191b2b7d1dd68d8f0353f47f8d90b6150f6dce58bf1112442cb", size = 419199, upload-time = "2026-09-29T00:49:58.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/6b/6dea87689c3a06a6e79d254bf824e6f3e3d724b5ba027c6112559aa6cd2c/regex-2026.9.29-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6abb75ab16bc3281714a5b99548a2225db70dba1f995f6d7f7419b76eb5a8fbe", size = 495413, upload-time = "2026-09-29T00:46:14.51Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a5/0c791a0e83ad1013d262c13247c4c77e0f4a8d05bdc167df96aba6681c0d/regex-2026.9.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b7b893976e7fe42053da64f2aa27239c24252fd2ec6df471e1be197c0addc3b1", size = 295145, upload-time = "2026-09-29T00:46:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/b1/07/9bf3607d8d13a12e436ab9d63f9791e10706827d535695b23964ad79fd79/regex-2026.9.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:066d0e3dbfdd739bce2bf8c2a41dd16f73e3d8adc2eb06dd803a36a307f56075", size = 292123, upload-time = "2026-09-29T00:46:17.646Z" }, + { url = "https://files.pythonhosted.org/packages/64/6b/32c2e6fc617e1d3f247e250fea31a9a35b1265bd32f585968aa13b9999b9/regex-2026.9.29-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7020ed44df30b3aa492c00ee3b52d0548c1f30c2c6c5bb13ae897680900d3413", size = 799537, upload-time = "2026-09-29T00:46:18.976Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/f041177f3c7a4606f7c81a95fe7eea03e2a0c4e8bff9e439a01432cbc9f2/regex-2026.9.29-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4613d7d9dda60fcba95f846cc6f808017f1843f392cf9daad14a6534493d71", size = 873255, upload-time = "2026-09-29T00:46:20.684Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4e/a78948e11dd715e0e46716c2e0f3404b3fe6a44e2a2e9abdc7d965cab2b3/regex-2026.9.29-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec37990e3d6121f29ecfb594bd8f1bf009e9f7926daba2e50e3b27d3892a783", size = 914570, upload-time = "2026-09-29T00:46:22.599Z" }, + { url = "https://files.pythonhosted.org/packages/8a/70/aa08d1d2b294894b365e5f8ba5380fe3f8546acdb81f10639dfd74209c37/regex-2026.9.29-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:612b709381c0355b70d89cdb51b7f670591ed5cbbc0e3b5337488019dc667b65", size = 804978, upload-time = "2026-09-29T00:46:23.981Z" }, + { url = "https://files.pythonhosted.org/packages/21/32/1b03534c4715aca3b564416d28d518083ed4dab3bc913267600d2256140d/regex-2026.9.29-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a760da040b47767b4b873adfb7c3b691e9ba2fc60f113f9d0b88f1a62f323e85", size = 779297, upload-time = "2026-09-29T00:46:25.318Z" }, + { url = "https://files.pythonhosted.org/packages/76/a7/378f6f558d9e4444af315a307c5953565a511d1e3666f1bb7bdc82012b6b/regex-2026.9.29-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:49ee178ca31c94621294bf9b8b676a92a2e6bba8af0529591753719e57edb621", size = 786961, upload-time = "2026-09-29T00:46:26.963Z" }, + { url = "https://files.pythonhosted.org/packages/59/13/79f0b1846f5f342f92ddbd4b27b18bcb86da96d902c1a0be26520bde98d7/regex-2026.9.29-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5eeb8edc6110d9194a4d0d54610f64c37a31c605b5dbb7e407fc6ec7fa34a4a1", size = 863234, upload-time = "2026-09-29T00:46:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/19/05af70dec9f2eed6ba34e08d2dcc6a48e7ae5e307659d5fe4201a5d7bbee/regex-2026.9.29-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ccb64d887a9db1cd76dbc0f92051a1a478a2a67e7f56c62d915cb881d7734704", size = 766487, upload-time = "2026-09-29T00:46:29.941Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/9c7486d4afe8fdd1fe0ad60139f8aa91427381f409af6a29b609d8fdcb3a/regex-2026.9.29-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9e4482589065c8ecd761cff522dcd85f2d39e62f551e37e025d1c7d54772def3", size = 854792, upload-time = "2026-09-29T00:46:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/49d008ff5f741d9a9799d7315556f3a12b983ff0fcd2cdfb62904bedafbf/regex-2026.9.29-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d60030baaa7bfbb02d650c126cdcddcb6e33dbff14d819434c8fa2fdcaeeeba5", size = 793050, upload-time = "2026-09-29T00:46:32.775Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a1/46ba549e65562ca04608b24179b8a7bb6f146ae0e7c6d7f5e70f3339c8ba/regex-2026.9.29-cp311-cp311-win32.whl", hash = "sha256:18ae8eed4526e35bdb754d61562b90bf5c00a67fdcf3cc1380dd59597486631b", size = 268940, upload-time = "2026-09-29T00:46:34.179Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4a/aab232183c70fdcf77bcf0c51819da02ec522e393e6a0bf00bcf2142e21f/regex-2026.9.29-cp311-cp311-win_amd64.whl", hash = "sha256:1043aedf5917caa861bcb25a9c11460049656bdf0017a90a309fa8f255467725", size = 280641, upload-time = "2026-09-29T00:46:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/33/b1/7c05954af0f51de376df2ba97f7f78a8b79334c7e5b3d2d9f2aead1f4d3d/regex-2026.9.29-cp311-cp311-win_arm64.whl", hash = "sha256:352cf115a810b357caa35193ab656ecf5ef41056855e82f292c99e8514f8d954", size = 279411, upload-time = "2026-09-29T00:46:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/48/3fdcde9a0baa84d7d25571223265d6e434e114763b438601d54a8028bf3e/regex-2026.9.29-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc79d36d0618752265f0d575915bdc5c5130ecb9c9f6b3bcefeae32e4bdfafcf", size = 497903, upload-time = "2026-09-29T00:46:38.938Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1c/4ee3e97c76f53940488dfe7a7e18705e78daac8cd7fb161d246b9e328449/regex-2026.9.29-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3a21a9509d0ee88e7a70e1ad228cd2f0e0fd1e187458db132e8a8d18c97daf9d", size = 296416, upload-time = "2026-09-29T00:46:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/37/14/f3f0ba083d2094392d5eabf56db5ea6ba469fd6e927afd187042054ea68a/regex-2026.9.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f57dc6b8fef170f105d2cf5cdce254f47b137d7755086cf7050f47e16582abba", size = 293633, upload-time = "2026-09-29T00:46:41.959Z" }, + { url = "https://files.pythonhosted.org/packages/c9/72/67e7a8ce17f1aea49df215564048efb49cc8c2b31a0e0fc30f36838f8516/regex-2026.9.29-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f93bc1c3486ef3747e07c9d7c1d0a147b8fbaab975f80e348aed6f71309dfaca", size = 805885, upload-time = "2026-09-29T00:46:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/25436bcfd4d2260b4b4090094d55d7ab53ec8a1ab4865a0b8bcb33c7d5c0/regex-2026.9.29-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d3a4cb7993b708f0ada8d0c84590efd853f169e7147d2202c9da503180242", size = 878344, upload-time = "2026-09-29T00:46:45.328Z" }, + { url = "https://files.pythonhosted.org/packages/97/e6/a09ec3a23ae41d6179880e67f0aace9284b2d95f2d7b326eff203f8eec5e/regex-2026.9.29-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dabee8f4935e731fb46b2a3091bdda0d3d94b3bbfb907d2b4f12eefce4009619", size = 919181, upload-time = "2026-09-29T00:46:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/26/83/d2fbd2e4e3afb1167daa825187d196f313cbaa1a4768f311fb041bb0e3d2/regex-2026.9.29-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39ab5894d971f9ac68baa6eca5c50387db579cfcacf36ae8df3feceb1815e6d0", size = 807783, upload-time = "2026-09-29T00:46:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/46/0b/eb429a7016610d44fc89a597163f8c9127505f0d7dc724dc9effbb6a3ac0/regex-2026.9.29-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c1a9a6651197fbed6f0212591418b9def774fc3f8324f78d1bf0e6a63e5f8aa1", size = 783465, upload-time = "2026-09-29T00:46:50.64Z" }, + { url = "https://files.pythonhosted.org/packages/1b/07/58a3c0153c7476898430f6a7cf3d9062a1d17fbea4f43399ecaf411c7b4c/regex-2026.9.29-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87fb80cbe3557e27e7b28b995c2b2eedf689b8886f941ab93e0e288f0976518a", size = 793519, upload-time = "2026-09-29T00:46:52.396Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/161b94d39164520e21a7befe0245569bf7fda4c7cf1fc4e2df2b5def49da/regex-2026.9.29-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3c5c2ef13797466aa64170cbb66ad98a32351dd4127694cea7199f80f213750d", size = 869293, upload-time = "2026-09-29T00:46:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/8f/07/3b02ed829aa2decdc1955d222bd1e2f99d1c8bb4873bbb9a66b2f0a36bff/regex-2026.9.29-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:59b49507f47479e299a9e1bc41b5cb83a7afda0540625f1dbae886615978acbf", size = 770239, upload-time = "2026-09-29T00:46:56.106Z" }, + { url = "https://files.pythonhosted.org/packages/42/5b/ba61f6fe062eb8562e742367d177bb75370434138ef6c9d2a27114f8d613/regex-2026.9.29-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0dd8af32e9f7b56b7f95cc1fd79b23054c3bdc172392ae560acc24d57b7ffe71", size = 861973, upload-time = "2026-09-29T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/767259b20e8a842948990f5e99138d6c077248fd42f8b5468b1d9ca4b814/regex-2026.9.29-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db5e82ba15c142425b8406690032df89e39cca4a2e8afbbb9a3d84edc2373ac3", size = 796470, upload-time = "2026-09-29T00:46:59.236Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/2566c4ba849b68a8ab81a6bf428fa79d20aae7ddee83979103c0381df254/regex-2026.9.29-cp312-cp312-win32.whl", hash = "sha256:d0c3082bf79bcd6a614d55916590ad4b8f93200e10b97f463ea5d9d07c9b5f23", size = 269327, upload-time = "2026-09-29T00:47:01.135Z" }, + { url = "https://files.pythonhosted.org/packages/93/19/489bc8db91196381c935752df01ba3f607140daece33b78d88573f028e64/regex-2026.9.29-cp312-cp312-win_amd64.whl", hash = "sha256:fdd88ed5e20b1bcdd234421e454962c971aa44b653bdb7f1ea9ef683e90fb649", size = 280334, upload-time = "2026-09-29T00:47:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/fb88ba779d0e5e7d4b0ec1aceeb13845948a2cb876bd572a2d1dfdba090b/regex-2026.9.29-cp312-cp312-win_arm64.whl", hash = "sha256:4fe97894d1b306c919b4e50def1e6f6c522f4d03a7283811f4d108f1ce5d3ac2", size = 279614, upload-time = "2026-09-29T00:47:06.541Z" }, + { url = "https://files.pythonhosted.org/packages/79/d5/6080f7d1a6e7e36aa720f806ac93c035ba39c209ae6cc510e8ef4c0279c6/regex-2026.9.29-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1a0d5117230dd46b399a30a38afa44f79c99f3168988fdc4f425c3f928b39df", size = 497622, upload-time = "2026-09-29T00:47:08.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/c87fc7a2e21a42f9d57489db32951c37eef56d153840459a80d464f0321d/regex-2026.9.29-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0fe9834e5aeccaf19a0d8feb296d66a24be1a7c9922002f842a682cd5abb787", size = 296281, upload-time = "2026-09-29T00:47:09.764Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/aa0f4cde3bc4688c1d58b0cd8415edd708339bc0bc401a195b0b1e8c8f0c/regex-2026.9.29-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c90fcf7804ea0a54b896ce0f2b9565350220b8d4890fd0db461a476a4c687963", size = 293472, upload-time = "2026-09-29T00:47:11.723Z" }, + { url = "https://files.pythonhosted.org/packages/90/d4/e835c487850ed922a8d6074f953b888c8ea99775c76b9ed5f8a4d72eab92/regex-2026.9.29-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e11edba5bc344a32b029a7af9d4b3173982dd79eeafa0b9dbd787364414b0509", size = 805966, upload-time = "2026-09-29T00:47:13.235Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/ba8809847fbae8d2cbc71367c6ded510a7ec88bf52493c65efc1acf4effb/regex-2026.9.29-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb90e7177944b6684738c1fc36aabd2dd00d1de3be7dbe09f91e196f1bc0dc81", size = 878346, upload-time = "2026-09-29T00:47:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/e3da19fc3cc15ef67ab67e121e87887c3bccfdb683a7a9ec557c460ca5b7/regex-2026.9.29-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d06fcdecc10fc7954d7c8f27a03c96055fe525274dc84a7b0dbdc3d6b9e03dab", size = 919250, upload-time = "2026-09-29T00:47:16.622Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8e/c1ed81f55f992f6aa0b699a592a50c1ce9e6d44ff1aee2c14c0537dcef9c/regex-2026.9.29-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d49c18f1ea294cf4adde2e5ac256e98c82ea9d708462ce4bf799dffa7cfe8a2c", size = 807902, upload-time = "2026-09-29T00:47:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bc/5a6886eb470e41040e21e05b75024a18b6ebfe7ea400b72094a60f949101/regex-2026.9.29-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e778bfccd63075167709136afbc251c1f683758d5bf49c803c60ac3f894ce6b", size = 783514, upload-time = "2026-09-29T00:47:19.921Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/6d951d453b023c6edb880f1ba474291b53b8ce1cc438b96a9db6d791d991/regex-2026.9.29-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:686ac5350fceae63830bb98805fcb8039325bf4c06d9f6f048ff65229d5bffa5", size = 793466, upload-time = "2026-09-29T00:47:21.552Z" }, + { url = "https://files.pythonhosted.org/packages/99/b9/d5a41adc08360f5eee0dc4846c578f002366947211fc8af5a69a64ee7b9f/regex-2026.9.29-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:26ec4ccce55aa533fbd603d08911b01101a8fcfec987845ac3ae2c7087b2bde3", size = 869464, upload-time = "2026-09-29T00:47:23.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/d76c9d91f5d798e2e9e67f6f85ec4ae35445ac425f7454797311cecb80ca/regex-2026.9.29-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a655d34b2a6943af32401f3d94f72e9d731f6ad16285815550bf2b4ee69d420a", size = 770278, upload-time = "2026-09-29T00:47:25.193Z" }, + { url = "https://files.pythonhosted.org/packages/24/00/aeebdb540c620a0f7317f6d6fad80a47729ecf0599a24b5c34ec155351f5/regex-2026.9.29-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0c992c19cd45058a4b92f68f139c93db168b48fb1f322c9a7cd620806afb6b51", size = 861949, upload-time = "2026-09-29T00:47:27.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/62/d0314bcedfd3586197e4596931fa220260eb2385bf53184e5b9ae67db24b/regex-2026.9.29-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ebb8912f565b8cdbbf27debfe00df04202c20e2f651b9e32767930c5eace3621", size = 796602, upload-time = "2026-09-29T00:47:29.233Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c7/d5a8c13a613facb03e0fb55c1ebaaf7bb35d8e2c1abe8bef8dca809fc1d9/regex-2026.9.29-cp313-cp313-win32.whl", hash = "sha256:4d7d93613b01b0199961330e49cfc52d479b3d5776c56c691db31130c0a07d91", size = 269306, upload-time = "2026-09-29T00:47:31.14Z" }, + { url = "https://files.pythonhosted.org/packages/80/a7/bf93a3a6afa5f7bc16b7afb94ae581b01cae620b8ad56bd8f9572a985959/regex-2026.9.29-cp313-cp313-win_amd64.whl", hash = "sha256:61956f074ecd123f55adca68ee3eab46e6a07ad3f8e64e6db95dfacb444f55c4", size = 280307, upload-time = "2026-09-29T00:47:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7d/388274e53605a86297f433a08102a7bbdcf9379d47683d307ccaefd88e2c/regex-2026.9.29-cp313-cp313-win_arm64.whl", hash = "sha256:bfc71e6d970419c1309b3640305298643e2a734cad3f7cfb6d2ddee4175ab53d", size = 279599, upload-time = "2026-09-29T00:47:34.674Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/d9dc6f02f569625faf67a4daec926cd5023472dcd69bb44286dccd5a5ab3/regex-2026.9.29-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:957bb708e8057ab1649ba566456429d691ec9b90d1c9ad1af1ba7ffbbeaf05f2", size = 497598, upload-time = "2026-09-29T00:47:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/9c/83/9b693a3fd1451381e812031a8961ec5b3b8f0c8cc6871f14c5223642804d/regex-2026.9.29-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c9b602fae1e00b7c035d661ce85575365719192a7b46784bd71cf64c68053aa0", size = 296250, upload-time = "2026-09-29T00:47:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/52bc2abc3fef040cd9de76ab29c918d6a717a454ae2b9dd7938b0c95656d/regex-2026.9.29-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0166844493626c5015c6088ee15c9ca2fd060ca15b7641d1657da6a58432ae33", size = 293518, upload-time = "2026-09-29T00:47:39.957Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fc/cf50671215ee0057046980b4571ef8646a005819bb67f0957e779ed107a5/regex-2026.9.29-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b97a38fb4c732b6832db6bf108963adbcd82ef1268ba2025dce390f45af75efa", size = 806037, upload-time = "2026-09-29T00:47:41.676Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/dddef8fc15c63e4347cc9efb138d0cd306f30e6c98acbcc81a8f780083b9/regex-2026.9.29-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a540abfab208e1b7ef2df231c40ef3b6cbb30a0aad6204e9b6a81c10a6794628", size = 878881, upload-time = "2026-09-29T00:47:43.755Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/38daabed32d28f7e58a06e9344ce00dc67952e9996bc578ed6a29fe1240e/regex-2026.9.29-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ddfa987262763c3c22a8367d2a49c244b018a74c3a8e3ab1a864119ad45c5633", size = 918684, upload-time = "2026-09-29T00:47:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4d/041d9458a645fee4fce4d642a89d27271a3cfcd91095104f6dde44da70bf/regex-2026.9.29-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f7f7aa47b229f2b39a2ae2596d2ad5625d77b5eb9856fac2dab3eb506cdd0a0", size = 807176, upload-time = "2026-09-29T00:47:47.372Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/4383eed7aa5aef67616cb1b3f3ad06b7c624c4e6cced48630cd5ce133d85/regex-2026.9.29-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d9b77b25b4f395f92de6099ab08e8ae2bc7e51dfe157f22900902243a5cc90c7", size = 784315, upload-time = "2026-09-29T00:47:49.518Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/0086ad31cebb183c637d3198547075aa493afde308e1ff61fccccb29ba6e/regex-2026.9.29-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34b6925af9853bf461950e6508910f179fd6e9b1a7ec8548e069606b7e51a26b", size = 793748, upload-time = "2026-09-29T00:47:51.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/f9005cba3f629a859573fc5d1224ea4e1f97919ec8581d018e03a351a604/regex-2026.9.29-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:addd736a0547d553283adaf4e05d7104e7f2c7b0b092e9b4d28756825f14531f", size = 870302, upload-time = "2026-09-29T00:47:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/01/4f/e1a3e46bb5315a4e18b01a990e7a28e2a16595609d50c442baf2815a3c65/regex-2026.9.29-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fe3fa1dd453ed5c7f5ea23a26218329790ed7197a99b90e94330e313959a7f52", size = 770299, upload-time = "2026-09-29T00:47:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fe/f303b4acfda44e1ff1379368748c1ef2dad04a6a8e9c0ecbc970b19d97ca/regex-2026.9.29-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0cc63b5e47c12a48d90c7e9d7de6a035dd14f62868aaedbb4e0ff8ba2b8bfe7b", size = 861570, upload-time = "2026-09-29T00:47:57.617Z" }, + { url = "https://files.pythonhosted.org/packages/60/b6/b4f7e99249f596017c60ccad5faf9310fc8e3e59bb2244940a90a1b0bdff/regex-2026.9.29-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:724184b4aafed865e4f13ca313fdcb43024300c028ec67319cfa16847d84685e", size = 795967, upload-time = "2026-09-29T00:47:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d3/fc865a4638d9f6762192b6bab5b7aa1f33a90e9e99578c2e111e2a63c8c3/regex-2026.9.29-cp314-cp314-win32.whl", hash = "sha256:c6c8fabf1dafc1f1ddcbb67896d3f93efb092e8c4b6322d7389b944e76a484e5", size = 274758, upload-time = "2026-09-29T00:48:01.8Z" }, + { url = "https://files.pythonhosted.org/packages/31/e2/c2b466924ccbeb874862968ca638051b15a8fd29d994a0e99004a5cbf78e/regex-2026.9.29-cp314-cp314-win_amd64.whl", hash = "sha256:1c2a0026062abcc321a53db4a185ceba0b59a66b5d37b0808917a88b55a5257f", size = 283817, upload-time = "2026-09-29T00:48:03.614Z" }, + { url = "https://files.pythonhosted.org/packages/c6/42/ea0f8dbaa924fa75c6338935eaee2f44dab369b27f02db1e03d74344b049/regex-2026.9.29-cp314-cp314-win_arm64.whl", hash = "sha256:121a76a0985db80ceae9e171c337f8c927868e37d01b54e3ce87bc87f9c6a208", size = 283663, upload-time = "2026-09-29T00:48:05.624Z" }, + { url = "https://files.pythonhosted.org/packages/44/48/d58e5081119f5c223bbb37d2340acde3d069e1df8e8cd166c37502eee4da/regex-2026.9.29-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e31f72490b7c12f7790e1e25c3afffd20503ee1bfb43461d7838b871ff244b19", size = 501393, upload-time = "2026-09-29T00:48:07.833Z" }, + { url = "https://files.pythonhosted.org/packages/72/3c/c49945287d4f9efee7d41f98072f8ad880efb8f430595a612fbdea996a4e/regex-2026.9.29-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:80ea96f5c1a30bf09007d48466521d9c294bebe197c708c3359096e3e3691632", size = 298237, upload-time = "2026-09-29T00:48:09.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1f/688cb61c3d4cf7bcc1ed444b5cc49399eba3e51c469ae285cf87fea3022e/regex-2026.9.29-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:554bffadcbcb6d5f4e5fb10a61cc52084b9a63d1dab5f10bcd2c4343972e8e2c", size = 295936, upload-time = "2026-09-29T00:48:11.454Z" }, + { url = "https://files.pythonhosted.org/packages/26/a3/de43ac6b877b7d09c19a3a426b1bd5acdd209eaaf68f406466f80439ccf6/regex-2026.9.29-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:864e9b87ac33c3fb9fb4ad48166d4fdb579c351d5c77deb0d34bccb36a775cd9", size = 816905, upload-time = "2026-09-29T00:48:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/62/14/9940763201c51d537786304984c67d0fc3d2ed18837ffb6f09a869f6b6c9/regex-2026.9.29-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:044265d77d94f5e3cb2fd72c76723807c429cb8c533e9d4672d0334a6f14f588", size = 881527, upload-time = "2026-09-29T00:48:15.313Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e1/c842d8df0b23245ebf202f8ab9c39fd48e2db39959454ec39a41c8c72082/regex-2026.9.29-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2089fe39c406784d90101c726755ffa1497bb74638fd434300d2b88006186de8", size = 923115, upload-time = "2026-09-29T00:48:17.328Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c1/98622479e3c354a446a75232e522d747d2b3df23092dcd8a5309380a2020/regex-2026.9.29-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0def9fb6abac55492d6d51cddb7225d07d6f279e774e0adc08569a54a5fc8d46", size = 820674, upload-time = "2026-09-29T00:48:19.32Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/5808c95f9c79ed27b5eedaafc3df6239ec56a49f2e23ea8f831b18427c82/regex-2026.9.29-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:888d60953908dcf761aa320c3e390ab8556efbdb551ace63921de90f6ae0848d", size = 793306, upload-time = "2026-09-29T00:48:21.615Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/021ca2638671ad20603bcd9b4d5bfa35d2610cd216a043ea7f0b44ea39f6/regex-2026.9.29-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed511a0708e2297e1d6431e7fb217e3402791e491e02da800658ace4973df1bb", size = 803103, upload-time = "2026-09-29T00:48:23.871Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2d/755c6d13ef9c657378013676c391c7a402166b3f419a464a3e058dcbe533/regex-2026.9.29-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e1172147d28d8fbcf8cb8d26c41506169f5ad8fe9ec969cb116835a19d4d8eca", size = 872175, upload-time = "2026-09-29T00:48:26.255Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/e1cab183b9dafe8597f58c1c766da9bf96204d3b2f232bcf3eeb75ff7b6c/regex-2026.9.29-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:92f05c9c42bde5785dc48770bc2194d9f7442544156f951e19cd31b096cec562", size = 777362, upload-time = "2026-09-29T00:48:28.389Z" }, + { url = "https://files.pythonhosted.org/packages/06/7c/e10ea17fba31fb4a1f9d13ed53a2d2a9066a2aea58d7557e263f6d99e7b0/regex-2026.9.29-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f37964e4a5e993d2fd45147741e9dff7f34a2d8c00ab94c4ea0514a4677f959e", size = 865606, upload-time = "2026-09-29T00:48:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6e/69824d9aee1fd41c54ea7264654a47c8d9d84d8a228e11c2bcf4c201ed81/regex-2026.9.29-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:951733b1bbdb71e377cec567b409f1a7881b47cfcad84121aa74cb575fa425ea", size = 807945, upload-time = "2026-09-29T00:48:32.375Z" }, + { url = "https://files.pythonhosted.org/packages/89/22/857050a86e21ce60193e02a8ef662521f2e263a645c8b1b905fc136b61a7/regex-2026.9.29-cp314-cp314t-win32.whl", hash = "sha256:65b408d8fcb273e3499e7ef2ce796810da1becd208c7fb4373692a242d79d461", size = 276758, upload-time = "2026-09-29T00:48:34.72Z" }, + { url = "https://files.pythonhosted.org/packages/4d/96/56808fe029553d7d4c703414f2a527faad2ea2bfa9ca094a2e7f8762b530/regex-2026.9.29-cp314-cp314t-win_amd64.whl", hash = "sha256:bf48516e35cf848390ea68850aba53e7c333720d2945b4d2c25b69fc5171723f", size = 286527, upload-time = "2026-09-29T00:48:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/074e2cfb3d8101a6a764aba5f7c5d1e21de087483e35bdc0c4ce2eb60364/regex-2026.9.29-cp314-cp314t-win_arm64.whl", hash = "sha256:9173db3be74a35cb6731701094b98120f7ee4876a287882a59cdea1fa7da342f", size = 285954, upload-time = "2026-09-29T00:48:38.901Z" }, + { url = "https://files.pythonhosted.org/packages/a7/dc/d84990386c9dfdf8c377f00f371b241fdc9a2c8aea0e3d66941b2e51be0b/regex-2026.9.29-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c3589f40749acce747510bf5d589d54e376cb0930ea58b35effac97e5312b0c1", size = 497836, upload-time = "2026-09-29T00:48:40.858Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ab/a569ebde875fa12ff8c6c9a30e07503620f195e4be4d54c3d3ee8eecc283/regex-2026.9.29-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:32ab11df9677ca80bcbb5fe4eb1da9109a5019239a054836efc6fa1c64e683cf", size = 296244, upload-time = "2026-09-29T00:48:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3e/7d548e82a108e7c8b2d5246650e397a2f8db599f9b2e975466939c5b4e70/regex-2026.9.29-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:7c03031610e3e6ed1768a2b7a8fc84637c1257b50c5eacaf094c6e17a84fc563", size = 293748, upload-time = "2026-09-29T00:48:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/34/a8e19a52f452bbb07b32a2bef70dcdf90c2737049749f74cc12d7486fb4f/regex-2026.9.29-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:42e82e578c904445d4c8a35b8f28052cf567593215fa5db06266fbc6f77aaa2e", size = 807840, upload-time = "2026-09-29T00:48:46.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/7b/11fbd4640b3bb82b72822a63c20ade4013d562d291703a9debeedc24e682/regex-2026.9.29-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0b65c72739f981377c9c22e0c5c3cd7f42da7bd8a3c9209330fac772c7d893ed", size = 879330, upload-time = "2026-09-29T00:48:49.168Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/de58c74f1f4e31586d83eb39c56872d686c4e0d0966d151884c833b94ced/regex-2026.9.29-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4408b2b27a95ca8cc48b7411945753773353b5c93b307754781086c99d3a576f", size = 919251, upload-time = "2026-09-29T00:48:51.322Z" }, + { url = "https://files.pythonhosted.org/packages/81/42/a8c480f6dd5ac59fa28ddae79afd9d7ac7e596fdb61813adc65bb6e674b8/regex-2026.9.29-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a714befaacbd10092ffe4cea0d3c5f008fb9efe9bc322c715bcdfdee414b9a3d", size = 808808, upload-time = "2026-09-29T00:48:53.529Z" }, + { url = "https://files.pythonhosted.org/packages/68/60/0bc0d1ec8b37ad64be6fa30e035251f11de9667a0fac9e82ee74517d81be/regex-2026.9.29-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33026515aebc0e70d1c89978e53e8d695d35d9e472f8d5b34465ba3c74028650", size = 789907, upload-time = "2026-09-29T00:48:56.036Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/116a3ef19b3acfe81077f0bf2cbc7714a5e94bc8935b7243ab61cb0f1c3c/regex-2026.9.29-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:31b003f9a070335e2a8233ee9b14a3ca8e6d792012ae011f741bf0aaf11744c5", size = 795770, upload-time = "2026-09-29T00:48:58.284Z" }, + { url = "https://files.pythonhosted.org/packages/96/ba/e38c3f203e7e7e18c957d48e6cb6dbf96c11e95a44efa4a480522afc5d6d/regex-2026.9.29-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:c03c6eb6ece86dfdcbb34799efaa339b093132e1aceed491ba5e08fe06cdf699", size = 870671, upload-time = "2026-09-29T00:49:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0f/9ee0b0cb76c55f63684bd7fff554978e8773b4fc86e2bcb2d50772dc1086/regex-2026.9.29-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:a5300757f8a68f5b6cc33f57338d72a0e3589c5cc9ad5f8504ea06f028be582a", size = 778631, upload-time = "2026-09-29T00:49:02.984Z" }, + { url = "https://files.pythonhosted.org/packages/b6/19/e6e3eeb226af5872c4958002f6edef4e4f40ea4cc5f5665023f2019eb045/regex-2026.9.29-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:80c7cadd3fd2bfde5df8aa0787e315812cad0c313a753095d02f4c2b6c01677b", size = 862187, upload-time = "2026-09-29T00:49:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/5b/62/823c102e106bb2711d6b7dfe5981552fe4467b2969c46a20c5c383cf498c/regex-2026.9.29-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3f1e6cb402a89457582cd696f982559217d13484a193202c394015297968c86d", size = 798423, upload-time = "2026-09-29T00:49:07.644Z" }, + { url = "https://files.pythonhosted.org/packages/37/e0/e927776258fa70b2f6feffc3be584ffc85ba4c1e20a320f0aee9a632fc7d/regex-2026.9.29-cp315-cp315-win32.whl", hash = "sha256:a64b85a4760337cfefdb27d42da6ed8b58e8cde3f2d57b6ef43e76ef6ea9ef47", size = 274757, upload-time = "2026-09-29T00:49:10.513Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/358de85d1860238e1b4fa98fc2c80c990124a25d2e14739e28cc02c25562/regex-2026.9.29-cp315-cp315-win_amd64.whl", hash = "sha256:b3e445b66c80b4eb4234e855ce94d9adc183eedbd632816228d89930b91b2c5b", size = 283824, upload-time = "2026-09-29T00:49:12.849Z" }, + { url = "https://files.pythonhosted.org/packages/92/d3/d5c5b264784a5ab2b0f8cf620c1eeb4dbf3440d306761905e7d99345bef5/regex-2026.9.29-cp315-cp315-win_arm64.whl", hash = "sha256:8f39588af4731c8923c26810eb3b33f76f17633985e40f59c3cd45a33805a895", size = 283664, upload-time = "2026-09-29T00:49:15.331Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/f63ec2c201445ce1150fe780f5c56f16a10124d9a9da3a93161dbb0d8892/regex-2026.9.29-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:fb99cc9d45f48895d9d67f6a0b8a57f08d39c174d9f25ad97a313e0470267b1c", size = 501549, upload-time = "2026-09-29T00:49:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/d2a698dc6bfc11fbce03f1cb0249c13284e93b79ed11f893edf6fac431c9/regex-2026.9.29-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:720537c7ea6f80dc61913184edb0ce2497a306b39ef19f28505b322553d52bdb", size = 298187, upload-time = "2026-09-29T00:49:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/85/b7/88dcdb38cd3935d4ee9e9ce9b8e56cb3b3518d1f020acfa7dd62ad289bf8/regex-2026.9.29-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0fd2c901cc307a745ad4bc87f20060d7a0825a3371d1e93488af22e7a387f78f", size = 296157, upload-time = "2026-09-29T00:49:22.342Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/ba6c01dde33a69fc294b38b43f6677baaa5735a6248f39708031a738158a/regex-2026.9.29-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b11b589e00095ec69cf79841a76360f9b079e95b0368a25b5ebb951ab0c157ff", size = 818468, upload-time = "2026-09-29T00:49:24.612Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f1/2586693e3a2d6b1247852593d37a6c17b42a92ee44f7cdcb9a0c1494e64a/regex-2026.9.29-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d7cab119d0df0b9413f106b4d7fc34f2872d3574ed3806fb48959c830b1537da", size = 882825, upload-time = "2026-09-29T00:49:26.996Z" }, + { url = "https://files.pythonhosted.org/packages/30/51/084f3e7bdcd0e9c33665c938cf5d134dc3548cbb4a75f0197ec7bfd754b1/regex-2026.9.29-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b89efc38431793d28b7cd91227e2f952ad7c48df19132b17f43a5fec3c14143b", size = 923314, upload-time = "2026-09-29T00:49:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f1/066c6fc23b7dc229789c21c880b5ba5ad689fb95fed12e078266f55a1f9b/regex-2026.9.29-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80a5ea3b4fd9d6a5b9a44f7976a9acaaab35aa3c1f6b29e5bd857dfabaded223", size = 822222, upload-time = "2026-09-29T00:49:32.404Z" }, + { url = "https://files.pythonhosted.org/packages/0a/56/592cd46fdb8f2f8682a1d7fd1310e4d0bcb93fbd0e6bbe4141ac28240227/regex-2026.9.29-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19959129885356df0e97556856f77eb2888380dac18bed075a7c05c5128c618d", size = 794882, upload-time = "2026-09-29T00:49:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/ee/4d/d65384bb071c864b01aa8314e3a6a687845ebd57588390976edc960c218b/regex-2026.9.29-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6a1a824fbed817e0a891103886b68f063b1e83cc51bc97192a90a60195a9291f", size = 805887, upload-time = "2026-09-29T00:49:37.395Z" }, + { url = "https://files.pythonhosted.org/packages/65/b6/358de0d8f40d5178e4f7e7e121cfd5b961c812b77a055d11f5079e3f8fd7/regex-2026.9.29-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1ba8c6a416569ce0d37e83e28a254a61dc99a419084dfb6476cea02d997f74fa", size = 872901, upload-time = "2026-09-29T00:49:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/00/06/6bfded72d043240c6b52bbb5e16f639d81affbf7484b4fe2ec45f3d4afc9/regex-2026.9.29-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:446654b29bfaa30500d80947eda42cef1449dc8a87f4e3cf061cc8485d3a1f0b", size = 782970, upload-time = "2026-09-29T00:49:42.581Z" }, + { url = "https://files.pythonhosted.org/packages/5a/20/9f418a50baa78b3ed8308fcb0cc49e472dd000b7ef935a7295af202ea744/regex-2026.9.29-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bf3c49863c23a1ad6da9c30351aed6cff8d5ddbeb63c5c8420ae54e98c7d0138", size = 865441, upload-time = "2026-09-29T00:49:45.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/29/817c7eacdeaf8463123e949bd394c39ad024eea1ec38ddf5ad141da2f3bd/regex-2026.9.29-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:01000ddf0e3ffef97f2413ceb514f6313040106b6d18a03ee00a4fe35c1eb1db", size = 809431, upload-time = "2026-09-29T00:49:47.878Z" }, + { url = "https://files.pythonhosted.org/packages/63/0b/83aab3b5b739947f744135a7a3a446e25433ebc92b05e01aae197ccbfdda/regex-2026.9.29-cp315-cp315t-win32.whl", hash = "sha256:c4e38dd8f39c43a91d2410ad2b85610701b0979342c3df1d69eaf8e838c757d8", size = 276964, upload-time = "2026-09-29T00:49:50.524Z" }, + { url = "https://files.pythonhosted.org/packages/72/f2/6314b5fc68789b5dcc38885bc6e3d6986b34fb3372b7231088ee5cecaa05/regex-2026.9.29-cp315-cp315t-win_amd64.whl", hash = "sha256:e2c89e9b762c57f59d5e99ee8b20202adb892e35f8d3485741340999ca55058e", size = 286487, upload-time = "2026-09-29T00:49:53.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/bc/97b2245c8c7b2dd01f2db74f2bea003cd33c15009b4996a2447f46b5325c/regex-2026.9.29-cp315-cp315t-win_arm64.whl", hash = "sha256:e8c65ef3862a8ad6e86492b6ed9327805dd66904c012bd3649dc67d822ed6c34", size = 285955, upload-time = "2026-09-29T00:49:55.655Z" }, ] [[package]] @@ -1823,124 +1839,137 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +version = "2026.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/68/3bd46b8a5e01d3c2ebdf9c5e9497912e3fe0cde02bac21a7130ca866e403/rpds_py-2026.9.1.tar.gz", hash = "sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12", size = 63948, upload-time = "2026-10-04T16:32:36.469Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/6d/ddb8dfcf41892ba9c672d27988592c038d30db61708230313831b6780a1b/rpds_py-2026.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2", size = 344947, upload-time = "2026-10-04T16:28:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b2/632c3d034961149eff606e3e357547620a9ab37b849ae5815f67838753fd/rpds_py-2026.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044", size = 346770, upload-time = "2026-10-04T16:28:56.884Z" }, + { url = "https://files.pythonhosted.org/packages/6d/45/4df346410bb873d03ec60c3b891d1fceb8cabde1971d29844bbb625cc296/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa", size = 377795, upload-time = "2026-10-04T16:28:58.193Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b8/a70f302207e4f29f873c01a986fcac5def49b9c8224328cef1ad7da6b7a9/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52", size = 383137, upload-time = "2026-10-04T16:28:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/13/e3/3aa28a48a01169d0e20728bf0e5ebbea3ae2774285b40853ae49b3ef332a/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f", size = 496789, upload-time = "2026-10-04T16:29:01.011Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a7/414e8d088602d66ad6f47c65bf07fe5090992a8574cdd4571c1a83b647ed/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7", size = 395929, upload-time = "2026-10-04T16:29:02.465Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5c/aac3b960fd55d37b49f77c0eebbe043eaf36762ebd48e7b46951e59c1c05/rpds_py-2026.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff", size = 375426, upload-time = "2026-10-04T16:29:03.944Z" }, + { url = "https://files.pythonhosted.org/packages/d3/80/77987a41c40be7202a78d9aed4a650bfb05792237cfd3b9e22638d990fe5/rpds_py-2026.9.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e", size = 381497, upload-time = "2026-10-04T16:29:05.308Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/640b02eb1ecd71a913c1178b6235287b31006266bca9a26862e80e497b54/rpds_py-2026.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888", size = 405740, upload-time = "2026-10-04T16:29:06.578Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8c/d034195d45d215625a23f2ae85dbbdb86a49a93a51272061c97c80b9d316/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834", size = 555850, upload-time = "2026-10-04T16:29:08.115Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/532f1addfc86207bc3cf23749638bb0d457bce891595012ed8a08d0543b8/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391", size = 618661, upload-time = "2026-10-04T16:29:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/50/5b/b0f2c9833a36a5b1f6ac5cb0e635fabacf713266f46ceefba2102da0bf51/rpds_py-2026.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a", size = 583530, upload-time = "2026-10-04T16:29:11.072Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/464ed4b98d3c8c6a334f3b5e24d3886de04b05658a95fc40a37e0e6e4775/rpds_py-2026.9.1-cp311-cp311-win32.whl", hash = "sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1", size = 208125, upload-time = "2026-10-04T16:29:12.408Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/384a9b62f8cd89c8e3e609b8c9052c140e8c01d807261e0517c904b8bf29/rpds_py-2026.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f", size = 226045, upload-time = "2026-10-04T16:29:13.751Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/0a691f0dcb2301fa9c736346da2d0d4991778373283301c5c12b8f952f3d/rpds_py-2026.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12", size = 222636, upload-time = "2026-10-04T16:29:14.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/34/a828586ea3329fbb50895b50e9cf98ca3d924a9f41a0b26d443bff1b3794/rpds_py-2026.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af", size = 346822, upload-time = "2026-10-04T16:29:16.258Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/ac6a0d064982251856ce009c9e1dd51a34110b3c055aa7d1aad18b4899a3/rpds_py-2026.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b", size = 341380, upload-time = "2026-10-04T16:29:17.706Z" }, + { url = "https://files.pythonhosted.org/packages/42/ff/bf7d54f362748fd6a49277b9d6531c394074110fedf50ad791ec59133fbb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a", size = 373053, upload-time = "2026-10-04T16:29:19.063Z" }, + { url = "https://files.pythonhosted.org/packages/60/de/74b0ccdbbd28687b9b5fdb34c1cabed88352182facaced9d6f5486b8b9ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07", size = 377742, upload-time = "2026-10-04T16:29:20.543Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6d/b979775a3057b2a5c26d75ecbff60a20a24ef081db6dd76836fca3f20247/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69", size = 487289, upload-time = "2026-10-04T16:29:21.958Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5d/7c34734ce3ece943d9d6ee0122a74a21bbc75b47acb5b7053c83f6efb1ed/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d", size = 390710, upload-time = "2026-10-04T16:29:23.378Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/b26eb1e75395925b3a142ed351cbed2ec8212f5c9d937aee3df32c701baa/rpds_py-2026.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab", size = 371261, upload-time = "2026-10-04T16:29:25.1Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/b2fdfe10301a9e27af21e634337fbba7baac0bc17fe44619a17c26bba2cb/rpds_py-2026.9.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e", size = 376075, upload-time = "2026-10-04T16:29:26.748Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/c4b8d954e49c3c69cf8063c0c9e0919c99f3d2cecc46311606a1cbe835dc/rpds_py-2026.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237", size = 399358, upload-time = "2026-10-04T16:29:28.282Z" }, + { url = "https://files.pythonhosted.org/packages/b3/59/9559a7293c97dff0cbb293efffbd36644103883ecc27741af8ef90c84ca8/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3", size = 550267, upload-time = "2026-10-04T16:29:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/6dc60e49511de4c8b00e74f9c1d43aed4d27f712cdb1ade92f2c199cbf64/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc", size = 614018, upload-time = "2026-10-04T16:29:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bc/92a4ecf27301b886232a413360f6b348a81d55ffef654f2ebda5970ebc3d/rpds_py-2026.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6", size = 578509, upload-time = "2026-10-04T16:29:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/08/53/efb97f2e6589b7ab8394385a73870e0b2ab2a195281c117b21fd6d7d4855/rpds_py-2026.9.1-cp312-cp312-win32.whl", hash = "sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3", size = 205401, upload-time = "2026-10-04T16:29:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/1c/84/1700cc748d0eaa747486e4e82882d2575224e6e9a3148df583058850c629/rpds_py-2026.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f", size = 222923, upload-time = "2026-10-04T16:29:35.709Z" }, + { url = "https://files.pythonhosted.org/packages/c3/89/0302215373c2f8b4408b0fdfee955368cc378a98ff59508d47a7e0dc2dbf/rpds_py-2026.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02", size = 218415, upload-time = "2026-10-04T16:29:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/ee88fd9e756ff93fb6b1182a47ec09504a242620e33ce1d20679efefe841/rpds_py-2026.9.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9", size = 346229, upload-time = "2026-10-04T16:29:38.82Z" }, + { url = "https://files.pythonhosted.org/packages/57/71/a097d6552f837500fc36e6b23d09cfb9890c3cc47531f9ca64e149799615/rpds_py-2026.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858", size = 340514, upload-time = "2026-10-04T16:29:40.405Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b7/497e85768bf4e0d8ddbaa096a4cac31d1509251dee2728a8490aa367e0b5/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063", size = 372611, upload-time = "2026-10-04T16:29:41.778Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/74ab4108916250b6e198e0d3af05bc6835eb046315f22f7a0ceb49667c5a/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025", size = 377712, upload-time = "2026-10-04T16:29:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/0c/8e/067e77d9d7b3cc793c9d909b7e97e7aadbbb1fb094093b6876902cc96d38/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5", size = 488106, upload-time = "2026-10-04T16:29:44.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b4/c5aae6c2dde269bf955f6b7d35065c655a57e47750d9668052ad67e74dda/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0", size = 390795, upload-time = "2026-10-04T16:29:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/76fab39973ee11e7f9f357c55138197bb01c86f6502cb76487e3b4f42db0/rpds_py-2026.9.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b", size = 370794, upload-time = "2026-10-04T16:29:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/cd2a80e6d7b871937a60e935c5d507aa390d143f4ff3636f640b9733d5df/rpds_py-2026.9.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33", size = 375673, upload-time = "2026-10-04T16:29:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/7464a9953724e55a3b3206062fa0ffdeaa519584c6aabf65d3956d94f131/rpds_py-2026.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83", size = 398758, upload-time = "2026-10-04T16:29:50.601Z" }, + { url = "https://files.pythonhosted.org/packages/c0/86/1534b436700fd49ff411063b7c4d7e938adfabf90895b6cf1622d5a7d1f6/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c", size = 550370, upload-time = "2026-10-04T16:29:52.002Z" }, + { url = "https://files.pythonhosted.org/packages/57/1c/e1fa82a8a01e3c5820f3ba98a8b2673f642128eb368fa88871b01dd2c909/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36", size = 613106, upload-time = "2026-10-04T16:29:53.655Z" }, + { url = "https://files.pythonhosted.org/packages/29/55/b20b8c4c3dde8755bfcd5b08492a02d0cd2e26929aedf6199ca2a377d42a/rpds_py-2026.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d", size = 578235, upload-time = "2026-10-04T16:29:55.157Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/df3f7bbc3f7ab37a8a9db8d6c2ff2c985422899f1f7926afbf7ec3c0b8b4/rpds_py-2026.9.1-cp313-cp313-win32.whl", hash = "sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c", size = 205293, upload-time = "2026-10-04T16:29:56.669Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/ba5a9569d719bfdd6ce863df4133ac6a1658cf1b07cc3534c31db729fbbc/rpds_py-2026.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9", size = 222409, upload-time = "2026-10-04T16:29:58.049Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/f28ca566f6c23c35bbf7445f65eb0364577b25a026305995e24f78b83d94/rpds_py-2026.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f", size = 217681, upload-time = "2026-10-04T16:29:59.449Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f2/67b94be1532767803415c1c5a1fd88ea487643d74a673cec1ba140af77bb/rpds_py-2026.9.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b", size = 347178, upload-time = "2026-10-04T16:30:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/b751de2b59b0197a1d92a5dd491de88e8a5e928c2e6562581974f1e85263/rpds_py-2026.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35", size = 341878, upload-time = "2026-10-04T16:30:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/72/e2/5873bc4643c250db9e05d48dc0c93763d4aa68bc3b81164cb1af3b45b284/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0", size = 373963, upload-time = "2026-10-04T16:30:04.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/03/5acf7632158247f3f6386ff0af3a1ee48167d575037e8d0920594b76d92b/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477", size = 377975, upload-time = "2026-10-04T16:30:05.555Z" }, + { url = "https://files.pythonhosted.org/packages/e6/00/63fda451b8bffa5808fc8bb311ee7c073b340974b09f273c7b2a145d3d62/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4", size = 490347, upload-time = "2026-10-04T16:30:07.156Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/c093ffd070ba0fb02f76c565d06fecc65ad6e4afdbae78f7031076d3cdac/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef", size = 392962, upload-time = "2026-10-04T16:30:08.77Z" }, + { url = "https://files.pythonhosted.org/packages/22/9d/d08a1128ab199b2f0cf25bfeb0639bd05119fff4b7c47bec24ef9a8ec23f/rpds_py-2026.9.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd", size = 371623, upload-time = "2026-10-04T16:30:10.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/c7/4758ddcbb75609414bbccfcb11d612436f9b3ee821bd2f33f0f1604ee648/rpds_py-2026.9.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35", size = 376997, upload-time = "2026-10-04T16:30:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/87/e4/947bd7f608ff60faf46dc9d389c3dffd0e3d767d78a0be19978448ef0ce7/rpds_py-2026.9.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86", size = 400169, upload-time = "2026-10-04T16:30:13.804Z" }, + { url = "https://files.pythonhosted.org/packages/4b/35/fe93e020a0543b5670472c18d7e6af3197c08da571240ce1965c84f85c0f/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e", size = 551615, upload-time = "2026-10-04T16:30:15.332Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4f/5d2a0136bb03b2a56a39dc6ff92d58a6e3e53a2e17079238b86228882f16/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785", size = 614655, upload-time = "2026-10-04T16:30:16.92Z" }, + { url = "https://files.pythonhosted.org/packages/09/1c/3f1025aaf70d9bf7272cc41f8b64ee76b48bf01726248138430e16f23b38/rpds_py-2026.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118", size = 579602, upload-time = "2026-10-04T16:30:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/53/0d/5c72e6204f76610608706da32b6b7e11ef7e10317558a7bb15a122e008dc/rpds_py-2026.9.1-cp314-cp314-win32.whl", hash = "sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc", size = 206315, upload-time = "2026-10-04T16:30:20.05Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0b/489d48abbcc7d70cf3fbf662d9d22abf1f4650761c0a9ae05260800800d3/rpds_py-2026.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c", size = 222852, upload-time = "2026-10-04T16:30:21.604Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/bbb05a7e6a10cf79ba639be7f799d770ee15f64175cc61d081b218dd402a/rpds_py-2026.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31", size = 218709, upload-time = "2026-10-04T16:30:23.036Z" }, + { url = "https://files.pythonhosted.org/packages/22/ac/ac507a0a4ec478ca470440a09583db4be5259ba7670aeba0620822f1e57a/rpds_py-2026.9.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e", size = 349596, upload-time = "2026-10-04T16:30:24.558Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f2/817a46b658d5070f477f722c298ee9a24525b0e4017347964146ef5fdd0e/rpds_py-2026.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96", size = 343114, upload-time = "2026-10-04T16:30:26.048Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/6ade976b13ac1b4cb3bf2eb603f1be2fe74df19a29988d4c2b386be59d6f/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445", size = 374197, upload-time = "2026-10-04T16:30:27.699Z" }, + { url = "https://files.pythonhosted.org/packages/d9/70/77cdf1d3f1a07faabe936016ae623aec7981f73108a8fe7a203ed2e21998/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69", size = 378025, upload-time = "2026-10-04T16:30:29.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/18a44a3beaa9b7931acb04af7bd9539836477c630a794452d4826d6185d4/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495", size = 491969, upload-time = "2026-10-04T16:30:30.995Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/fb39cfd6bddaf741b024f813890374578ff8ac1f1adc473659c048b03b05/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c", size = 393905, upload-time = "2026-10-04T16:30:32.628Z" }, + { url = "https://files.pythonhosted.org/packages/ed/71/0fa7bb77b57af0d710273964180d11b503f62b8a5c358eb2d8c3f62feff6/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e", size = 374569, upload-time = "2026-10-04T16:30:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/f41cfac269af3b449513ef1bc3d7f32fde52abbbd2d01c7e76b47743acd9/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346", size = 377482, upload-time = "2026-10-04T16:30:35.997Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/312b49006e7f8f9ca5f96647577b8aa6f3df30519c46bd448f5c425af0b2/rpds_py-2026.9.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299", size = 400040, upload-time = "2026-10-04T16:30:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a6/18cca7a878dc7fa95165a83343fd4d7b65643fd22e54e47340a451121d5c/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531", size = 551849, upload-time = "2026-10-04T16:30:39.443Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/1f5685e20f39604691bdc3c05aaa6b8bd2f954e9e996477adf2376768e33/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb", size = 614842, upload-time = "2026-10-04T16:30:41.231Z" }, + { url = "https://files.pythonhosted.org/packages/c6/25/98652109fd9f7e10268dd4571aa52b81987001b806f37ef1a18260de714a/rpds_py-2026.9.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa", size = 582606, upload-time = "2026-10-04T16:30:43.345Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/11ca09099bab5f53373a80a334c424ec917c13d050760a59499d2af5171e/rpds_py-2026.9.1-cp314-cp314t-win32.whl", hash = "sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569", size = 203341, upload-time = "2026-10-04T16:30:44.954Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8a/88909e3ffd9f47f5b58211473875d8c3c09079f0058c46fb72c55a702a26/rpds_py-2026.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e", size = 222843, upload-time = "2026-10-04T16:30:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/a662f367d4896dd0a10cef2fc91f10b7f08af1c10858e287e019563f338d/rpds_py-2026.9.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9", size = 347463, upload-time = "2026-10-04T16:30:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3f/ad45d03df4f84ebae5439577ee81f3999d182711e82235c8037b6528890e/rpds_py-2026.9.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25", size = 342051, upload-time = "2026-10-04T16:30:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/3dcd68c13d4bcc59c1f7eb33ac8e80698f06f3eb0d8a1e06419836071c20/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871", size = 374143, upload-time = "2026-10-04T16:30:51.508Z" }, + { url = "https://files.pythonhosted.org/packages/cf/0d/68c1f058a250fbd1380ebda9fc227cbf50117adf8ffe8161ef383ea79f68/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91", size = 378604, upload-time = "2026-10-04T16:30:53.206Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d6/2d4c59b85397cb4800594fadf692688ccf5ce556adc930e7a5bf21061a5e/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f", size = 490790, upload-time = "2026-10-04T16:30:54.925Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/7e05dc3aebaf52f4e026766bd668fdd09a9d0e23f64a14686b36b3501892/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d", size = 393146, upload-time = "2026-10-04T16:30:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/e2e9a0a46a74ed51a0498ba1fe10f4ea6b2d9a155f372a2f91e51f18cf10/rpds_py-2026.9.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945", size = 372022, upload-time = "2026-10-04T16:30:58.295Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/8f8774df5134e23424372838bcc5c7ed4127d723e1ff52f7bebd4dcb2563/rpds_py-2026.9.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297", size = 377092, upload-time = "2026-10-04T16:30:59.984Z" }, + { url = "https://files.pythonhosted.org/packages/90/02/8d7095d73bf9114219be40230baa5df00611e0a82ed9517779ff9c19f82b/rpds_py-2026.9.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0", size = 400830, upload-time = "2026-10-04T16:31:01.721Z" }, + { url = "https://files.pythonhosted.org/packages/ec/02/8206856f8f363cd042a8315dc86b3912f5dcb6d3c66bbb24b69c2bfb0775/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9", size = 551478, upload-time = "2026-10-04T16:31:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/41/6b/36211f1bb1f0b0313f496d92f5905b74ea107f27cb16fa3355a82f04575e/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6", size = 615173, upload-time = "2026-10-04T16:31:05.281Z" }, + { url = "https://files.pythonhosted.org/packages/35/77/cda0c4a6f055446b692f0ed5692f73707cafd3dff82672ede31b1a9b59de/rpds_py-2026.9.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7", size = 579653, upload-time = "2026-10-04T16:31:07.065Z" }, + { url = "https://files.pythonhosted.org/packages/74/ec/d8385f446240aed643b9e92a5055cff3015cc04a13c61f73b2883c478ed5/rpds_py-2026.9.1-cp315-cp315-win32.whl", hash = "sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429", size = 206498, upload-time = "2026-10-04T16:31:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a5/71b5cd00e0521e3b6b81828baea368c62b6b700ebdd9554cd7d41ddf12fa/rpds_py-2026.9.1-cp315-cp315-win_amd64.whl", hash = "sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15", size = 223121, upload-time = "2026-10-04T16:31:10.84Z" }, + { url = "https://files.pythonhosted.org/packages/33/58/dba857c3bc8221b31b62eb170a3080f4f191de79f047200389b7ed1b06a7/rpds_py-2026.9.1-cp315-cp315-win_arm64.whl", hash = "sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009", size = 218741, upload-time = "2026-10-04T16:31:12.592Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d0/320ab28ccc1415eeb509d68682b0014fb74690cd49f1c2d29a232475af50/rpds_py-2026.9.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0", size = 349719, upload-time = "2026-10-04T16:31:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/56/88/f5b12f1358f443c08b7ce3cc8391d82d335f4872580e2e097847fd36087a/rpds_py-2026.9.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a", size = 343267, upload-time = "2026-10-04T16:31:16.827Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0c/c765b0059d532acb3b9c45d781ccc22f15a96dbe443d00903f643ba9df10/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d", size = 374555, upload-time = "2026-10-04T16:31:18.931Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8a/cafddfda77564a10cd21184640c3bffda6a2b8d20972fb5dedd5e0166328/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db", size = 378346, upload-time = "2026-10-04T16:31:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/b9/01/5e626016eff72c183bf6c96539240cace15d402468647a453ec08415b2fc/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250", size = 492373, upload-time = "2026-10-04T16:31:22.614Z" }, + { url = "https://files.pythonhosted.org/packages/3b/9c/15a2469e9389242f46896b3f0a01d68caea8a5a35c011fcb05ef333aae73/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f", size = 394371, upload-time = "2026-10-04T16:31:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/63/f5/c100ff77e1e6366e947c75969c258fdfc4b7bc5bbfe7351e62ffbf2a1228/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018", size = 375132, upload-time = "2026-10-04T16:31:26.588Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f4/fe0269c9de253e99c81cabc12b8971a5feaa083debdaff1221e06264d9e3/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d", size = 377642, upload-time = "2026-10-04T16:31:28.677Z" }, + { url = "https://files.pythonhosted.org/packages/05/65/b34a7b257baccff8f4a24a722933166d4941d5ebdc9c3f4bc4ffcd5ce4f4/rpds_py-2026.9.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b", size = 400855, upload-time = "2026-10-04T16:31:30.802Z" }, + { url = "https://files.pythonhosted.org/packages/98/32/844e54176b6071b90b38a564e6940bc6eb8f97b2890dc709c19db9dec0f4/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6", size = 552573, upload-time = "2026-10-04T16:31:32.709Z" }, + { url = "https://files.pythonhosted.org/packages/17/73/6041d20729dffbfdf155c02d65be58bc225a1c1fb548fd87c23ef306138f/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad", size = 615840, upload-time = "2026-10-04T16:31:34.565Z" }, + { url = "https://files.pythonhosted.org/packages/af/9e/418094adaee6b056ce199051b255448ed872829051e341b2294c80da0977/rpds_py-2026.9.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903", size = 582764, upload-time = "2026-10-04T16:31:36.665Z" }, + { url = "https://files.pythonhosted.org/packages/3d/9b/1698ebf6b840ddfe8b472198abbed6ace35c6e398faeecd6c47dae742a4e/rpds_py-2026.9.1-cp315-cp315t-win32.whl", hash = "sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b", size = 203795, upload-time = "2026-10-04T16:31:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e2/91f70d804c61f8eac39a417e82aa1024f655ab9e8bdd7393b196242bc41b/rpds_py-2026.9.1-cp315-cp315t-win_amd64.whl", hash = "sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567", size = 223037, upload-time = "2026-10-04T16:31:40.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/f0eba548e297987964ef8088403db875b333963bf8189f3f563b6936b20e/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30", size = 384112, upload-time = "2026-10-04T16:31:42.643Z" }, + { url = "https://files.pythonhosted.org/packages/33/89/891fa4d81f1eae7fe262ecd0aba2c51de94a116c7f3ccfb034fa75f4c537/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050", size = 389662, upload-time = "2026-10-04T16:31:44.642Z" }, + { url = "https://files.pythonhosted.org/packages/4f/46/912cf623b2263f9c9c2d04e8508fc38dbe89983faeba5fb36ca4558ae550/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb", size = 504637, upload-time = "2026-10-04T16:31:46.667Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5c/7f3cfa7c5f01765c9f47c8398210ed0c1eb686c0e99c223cb1c6776d3fb9/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df", size = 402607, upload-time = "2026-10-04T16:31:49.068Z" }, + { url = "https://files.pythonhosted.org/packages/f0/76/2e915d4115ee453c33e6af17bc1df7e22ddc52974e178eeeb92a78c09983/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a", size = 382762, upload-time = "2026-10-04T16:31:51.24Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e5/844382925d7b84a4074ad79fc2b9d05eb6231ea243b34f247e68e519b606/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608", size = 389563, upload-time = "2026-10-04T16:31:53.549Z" }, + { url = "https://files.pythonhosted.org/packages/81/82/d6e3951602dc2e24790ef9d299aa24bf5e2493df5043f2c6da00480cc447/rpds_py-2026.9.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9", size = 416842, upload-time = "2026-10-04T16:31:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1c/8d248e4ea671d525ab7daa0703ad81cc2175076fa0ccd7ef088facbd2b01/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c", size = 562434, upload-time = "2026-10-04T16:31:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/e7ac0db898120d15dea25e3661b1242a40cb3b23d1f4db86c15a01fe0ba1/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f", size = 627909, upload-time = "2026-10-04T16:31:59.977Z" }, + { url = "https://files.pythonhosted.org/packages/03/c8/37e555514932cb1ee36c75349187aa882a98dee5d5654c761699ed8a0d27/rpds_py-2026.9.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e", size = 593658, upload-time = "2026-10-04T16:32:02.14Z" }, + { url = "https://files.pythonhosted.org/packages/12/5c/5f69a91add94a8a33acea68a2b034a464946a654ff7296af9d957ef0d6a5/rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_10_12_x86_64.whl", hash = "sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32", size = 351769, upload-time = "2026-10-04T16:32:04.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/22/35ea8bf3b2682b90ab687c74e4828418ff12298a5b0b0a0c033f61c86a30/rpds_py-2026.9.1-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31", size = 351719, upload-time = "2026-10-04T16:32:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/a9ba81408369d34c1aca73319c15adc91ed45c1d21e9e8ee4832ffb7fe0b/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4", size = 381109, upload-time = "2026-10-04T16:32:08.994Z" }, + { url = "https://files.pythonhosted.org/packages/b3/44/5192a0bed94cec86bd2a5e1cc5a1bb8f7eec3aec907eacd2deb13e87e326/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759", size = 388222, upload-time = "2026-10-04T16:32:11.321Z" }, + { url = "https://files.pythonhosted.org/packages/92/cd/5356549711448f18b52f7a11ffbe90f1228774996fbf3c6cd1b18d22abaf/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb", size = 500384, upload-time = "2026-10-04T16:32:13.478Z" }, + { url = "https://files.pythonhosted.org/packages/7a/88/ddda9d28adfe33119c75b2e733d4ba7e326f41d7e1b1093951771768ca48/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82", size = 401661, upload-time = "2026-10-04T16:32:15.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/c3/bb59ba16a6b4a57995d15b8c04c00f28df3b938c4dcdde48fbe0f73edb0c/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c", size = 380748, upload-time = "2026-10-04T16:32:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b8/0580faa1c5a32ddc160130dd7ae54d2d007272a89a40a6b5db3e50f9f585/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14", size = 385731, upload-time = "2026-10-04T16:32:19.855Z" }, + { url = "https://files.pythonhosted.org/packages/bf/70/f73564642bbe3322c7eeef2c2f258cf040b71c41a430b532e59d04a1b838/rpds_py-2026.9.1-pp312-pypy312_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c", size = 412131, upload-time = "2026-10-04T16:32:22.285Z" }, + { url = "https://files.pythonhosted.org/packages/8c/5d/17fff2e1f8f68721a2afb5cb48f7e442c751bb6b4883157c5abaf618cd08/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68", size = 558193, upload-time = "2026-10-04T16:32:24.4Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d0/869ab6fb08531f97aae4a780b4d61b4190d8aef48fe054ee28bcf7114466/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_i686.whl", hash = "sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da", size = 622448, upload-time = "2026-10-04T16:32:26.844Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a5/9672e532fe02cd3b92c78cfd8743abca939d96853006c63835009a1ecb6f/rpds_py-2026.9.1-pp312-pypy312_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448", size = 591126, upload-time = "2026-10-04T16:32:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/80/ef/3a9f8e4279c7920af561deed4cb7ebebed2794a8f608cf404d5eef14a105/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_10_12_x86_64.whl", hash = "sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0", size = 357108, upload-time = "2026-10-04T16:32:31.641Z" }, + { url = "https://files.pythonhosted.org/packages/b7/55/4b2fa381a583760aea5c92841e4e928a358e1c6511b129219e9c2826a226/rpds_py-2026.9.1-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37", size = 346877, upload-time = "2026-10-04T16:32:34.061Z" }, ] [[package]] @@ -1988,117 +2017,63 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/66/62/167a842aa0429d45f5e797354fd4343a96f6043d67d0513c675c7b8d36e6/tiktoken-0.14.0.tar.gz", hash = "sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874", size = 38898, upload-time = "2026-08-17T19:49:49.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/c5/9d848b7f408241171e1f843deb8bfa626086452bc9c78beee500829583e3/tiktoken-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79", size = 1094971, upload-time = "2026-08-17T19:48:40.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a9/d94302340304328961d6f0c35ca4e60617fbb57a5cf667e2ed1692cb9e57/tiktoken-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948", size = 1042916, upload-time = "2026-08-17T19:48:41.541Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b6/31da98ee871383509cae2ba96a9ddef1965e3c4f8cb6dc7bcda3379398db/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f", size = 1188650, upload-time = "2026-08-17T19:48:42.729Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/8c5dddd7cb67f6571d154a58d7c6e2f07da54bf84c49b6a1839965b7c35e/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513", size = 1206378, upload-time = "2026-08-17T19:48:44.013Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/522ec59d30dd9a2f3ab837011cd4fc5d1178dc4a2fa07c9fa4b90af6ba9d/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78", size = 1253694, upload-time = "2026-08-17T19:48:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/9019e272bad188a1c61ecf44f25a9ba2368744644e3ac1f3d6516f3c9e80/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e", size = 1317873, upload-time = "2026-08-17T19:48:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/fff1217240343c0c11b5938b98aeae0e3a266cacfac25f86f91cdcd748f0/tiktoken-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da", size = 944395, upload-time = "2026-08-17T19:48:48.028Z" }, + { url = "https://files.pythonhosted.org/packages/8c/da/e273746b9d24a63c776bc60fba914351573ad9c575b52601eb5e60632564/tiktoken-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36", size = 1094408, upload-time = "2026-08-17T19:48:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/69/9f/fe6b1aca23331aa5271df5a4bd07bf68a7059254d47faee1b8272592a777/tiktoken-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4", size = 1038499, upload-time = "2026-08-17T19:48:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/0b/35/e9f47647c9e163bd1de30fe1a491669b7248cfc67b7404c35c009a701e1a/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6", size = 1186355, upload-time = "2026-08-17T19:48:51.93Z" }, + { url = "https://files.pythonhosted.org/packages/51/11/9976ad86980a00cdef05e730a0127a2578a1bc6d11644d8d47246de2eb26/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d", size = 1204197, upload-time = "2026-08-17T19:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9c/7035b0bcfaa68d1ee4803fc5be5214ad865669b05bd20e7105ae8a18afc6/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482", size = 1250635, upload-time = "2026-08-17T19:48:54.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/69cabf18bed7f4366da076735816abce0d4db3fae491ae338a6612128777/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6", size = 1316085, upload-time = "2026-08-17T19:48:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/bd/bd/a2e884fb1402cba5be08836590320012b2d8ada0e2eef9911a64df4bcd2d/tiktoken-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3", size = 941208, upload-time = "2026-08-17T19:48:56.938Z" }, + { url = "https://files.pythonhosted.org/packages/50/53/ee1453623bf65f019328721ccb6587846d2c5b7b82f34e73ca09101f072e/tiktoken-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f", size = 1094198, upload-time = "2026-08-17T19:48:57.955Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/6448cfe278c3664ba9ec5b5ac08344341f7dc3d42888476e215a14eda2be/tiktoken-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94", size = 1038820, upload-time = "2026-08-17T19:48:59.015Z" }, + { url = "https://files.pythonhosted.org/packages/69/3b/d67eac1bcce9dee3abe23aff5e3ded3116bbebaf67b80a0811c06d3806fc/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06", size = 1186175, upload-time = "2026-08-17T19:49:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/37/62/cae690d9783146b0f81f564ada0f8f611de68178c0c9c7e1e969f0516b48/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d", size = 1203884, upload-time = "2026-08-17T19:49:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1e/633e30237b94e383cf814145499079f3bb9cdd4aeafc1bc42e01b0f810a6/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010", size = 1250980, upload-time = "2026-08-17T19:49:02.274Z" }, + { url = "https://files.pythonhosted.org/packages/cb/56/4c12f07b812f84206f38d723eb1ebfdd34bad9309b5dbc0bee6bbcff4cbf/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632", size = 1315434, upload-time = "2026-08-17T19:49:03.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e0/c65603f0c44811def666d3fbf611bf2af3b5e1ef613e06c19411419830b3/tiktoken-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1", size = 940883, upload-time = "2026-08-17T19:49:04.583Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/1cf129f4af8fc513931f931023def596b7c4bfc77026513cd9d851da9e88/tiktoken-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450", size = 1096273, upload-time = "2026-08-17T19:49:05.807Z" }, + { url = "https://files.pythonhosted.org/packages/62/85/2ae74575e321148484147e10b53c3b1717c59ebaa9edb4fe18b1f5c055f8/tiktoken-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b", size = 1040269, upload-time = "2026-08-17T19:49:06.943Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/92a1120a12e4bcf2d5464350d1a91b68a433d63ce656bb7f806c27aec09c/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e", size = 1186101, upload-time = "2026-08-17T19:49:08.102Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7d/144af98dc5ad68108451a82e2f5a17f80e2663f5115058b8dfd215c1ad02/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42", size = 1204457, upload-time = "2026-08-17T19:49:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1f/be7cb06ab2108f612f3e92e7b76cf391e192db0db37a984616f0cc32aafc/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c", size = 1251716, upload-time = "2026-08-17T19:49:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6b/81f158d0f90adb826cd704069c2129a046cb784a2a09861009519fc41cf4/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771", size = 1315432, upload-time = "2026-08-17T19:49:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/f5fa35ec13f07279fdcaf3cc9c04bbb154ea591d23978651f2b672593e8a/tiktoken-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098", size = 988046, upload-time = "2026-08-17T19:49:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/68/c9/7756717408d3d0dfea3f046c9466144b28afde39ff69d5808f2475dcd7f5/tiktoken-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438", size = 1096261, upload-time = "2026-08-17T19:49:14.351Z" }, + { url = "https://files.pythonhosted.org/packages/79/29/46ad8061f57bd9f8b2ea0aa82bf574e0f2aa040b0857a1582adba9957899/tiktoken-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa", size = 1040183, upload-time = "2026-08-17T19:49:15.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7c/3184d17b868456f17b60b1a75f5ec0405618a43aa753336df341d8f11781/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037", size = 1186719, upload-time = "2026-08-17T19:49:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/46de4400d5bf859f640feee85bd7e32235f68ddf25db53c63be78e581e3a/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef", size = 1204660, upload-time = "2026-08-17T19:49:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, + { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/b73b7e319179e0f60b32475f783b044f9cece872c53b6662664e9084b0d0/tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232", size = 1096280, upload-time = "2026-08-17T19:49:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6b/09999a9bf1d559670d1680e8f8e419ac0e2c5f6aac82e9bfdf70f260b30a/tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695", size = 1040433, upload-time = "2026-08-17T19:49:23.998Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/8537be0836f3df99b2a636b44399bfa43cd757f2b8b4097dacb794cf24a7/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49", size = 1186989, upload-time = "2026-08-17T19:49:25.021Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/f9c56d7a943a4468abf9ef37661bb9b8e0cd3aa8aa87368c7146cc3f3222/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4", size = 1204615, upload-time = "2026-08-17T19:49:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/98a38579db25c4a8a84e31dd95d9072ec5f21f7e70de591da0412e29b25b/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871", size = 1251828, upload-time = "2026-08-17T19:49:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/0c/83/467be424746c039c5493c0f4102feab16b9b48eb6f5c089b2a2438e3cde2/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f", size = 1316260, upload-time = "2026-08-17T19:49:29.101Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/ddf46ca78e371f5890e96b6e7d089a85b3536432be219851eb0481786ca8/tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea", size = 988230, upload-time = "2026-08-17T19:49:30.246Z" }, + { url = "https://files.pythonhosted.org/packages/2a/00/5162e90c851a28da18ed382d34898b79a8022548e5619a64e14c03ce7c3d/tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890", size = 1096186, upload-time = "2026-08-17T19:49:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/65/97/a5a7bfccf25b1bb65e82bae8edff11ac3c9c041c374b7b4a823d60c38133/tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5", size = 1039947, upload-time = "2026-08-17T19:49:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/ef427fc638f1439181c5e12dd26b70e881861f89c007aa7e5b36300f8342/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae", size = 1186997, upload-time = "2026-08-17T19:49:34.121Z" }, + { url = "https://files.pythonhosted.org/packages/3e/88/2f3f85a968cdc514152129af0a060ebcccb067005a2f29b0d5ef3c838514/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1", size = 1205211, upload-time = "2026-08-17T19:49:35.284Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f6/80760e98a08e6649d2d68afb6035af713121dfb615acce8c4f73810ec438/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89", size = 1251479, upload-time = "2026-08-17T19:49:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/84/50966fb6918a0fb9b32721277e5342bf729a2d74350074d662fbedf9772e/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3", size = 1316673, upload-time = "2026-08-17T19:49:37.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" }, ] [[package]] @@ -2115,23 +2090,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] @@ -2181,7 +2156,6 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } wheels = [ @@ -2206,16 +2180,6 @@ version = "1.17.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, @@ -2271,120 +2235,173 @@ wheels = [ [[package]] name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, - { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, - { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, - { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, - { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, - { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, - { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, - { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/a5/1386f35da1475fcaeef42581deae73417c6d2a6a0b2d2e8914de18844dcd/xxhash-4.0.1.tar.gz", hash = "sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7", size = 101513, upload-time = "2026-08-17T08:24:08.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/58/bc81e25cceab76ce4b400441e3a43312bf3887fedbb2e5f80cc5a7dd7f75/xxhash-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c", size = 38473, upload-time = "2026-08-17T08:20:34.303Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/d95e810c9a2930906f1fbd0e38f77554abb8e042a190aa9cbb24da39e9a2/xxhash-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c", size = 36228, upload-time = "2026-08-17T08:20:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/72/be/ebcded2a32ba664a17a1737a91b6baa298bcda6942acdad81af81660b74c/xxhash-4.0.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794", size = 253292, upload-time = "2026-08-17T08:21:27.206Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/72d31d787d988d1130253bc34d249c553f02c9387e7dda789b6d5aa7963b/xxhash-4.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286", size = 276545, upload-time = "2026-08-17T08:20:26.726Z" }, + { url = "https://files.pythonhosted.org/packages/39/ae/048f3b1f283a340bef3697fe5a9d4f10de1696a379af9af6b2352c24d82a/xxhash-4.0.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf", size = 296295, upload-time = "2026-08-17T08:20:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/e9a593c81445c8e8d669402edb8264144624e7e5e2406df7d33e3c164d90/xxhash-4.0.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191", size = 279966, upload-time = "2026-08-17T08:21:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/29da2b166955a300521c0abd498ad4481916c3743949b106192b781f58fc/xxhash-4.0.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa", size = 509850, upload-time = "2026-08-17T08:20:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/ff/e39e1900179ce7f0f23e7000d9fc672471a8d05b6b2dc657cb496c8975d0/xxhash-4.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24", size = 261005, upload-time = "2026-08-17T08:20:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/89/79/76ee26720d13219458f4b2ec0b22f539fe2bcb1f83dc22a24be4cda4e285/xxhash-4.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79", size = 339620, upload-time = "2026-08-17T08:20:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c7/56b53252d64ecb2f3a0d283b41033561b1bec9c268095150153b694c2e52/xxhash-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c", size = 272558, upload-time = "2026-08-17T08:21:33.883Z" }, + { url = "https://files.pythonhosted.org/packages/fc/76/83101a2f2ad3eb6b4b5d571e0aa394a576e3e23121707d507d80197ac385/xxhash-4.0.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c", size = 300417, upload-time = "2026-08-17T08:20:36.183Z" }, + { url = "https://files.pythonhosted.org/packages/34/5d/5be1166ec4fc4bc896e508dc51189a2dad200b373269a430e214fb152693/xxhash-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a", size = 259286, upload-time = "2026-08-17T08:20:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/77/6c/42f6201f13278787c58a6b3a1cd597b47e8665a7d4f68a3440d001c05a75/xxhash-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1", size = 278230, upload-time = "2026-08-17T08:21:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/3cb7f149a5a469c628604c33bec6d79764698b315700a4bbf1c6fb15622b/xxhash-4.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925", size = 329846, upload-time = "2026-08-17T08:35:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ee/934eb1e11f4d0e95f3828825f8da4b6d59e338235d63edc3d929769262d2/xxhash-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df", size = 477285, upload-time = "2026-08-17T08:20:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/77835dcd7aec970db74f5724c94207ada83b3e2f5e1474ab44b9c8fe5ea5/xxhash-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a", size = 257552, upload-time = "2026-08-17T08:20:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a3ce7a1a9c4ec9ad8babba591a996a71c474ff9630c5fb04c8c9d8b995ca/xxhash-4.0.1-cp311-cp311-win32.whl", hash = "sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399", size = 34630, upload-time = "2026-08-17T08:21:53.323Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/60d2170e8cda0891aab25dcaa74797b420c3c6cde8a5bc8372f17b30c0cf/xxhash-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0", size = 36992, upload-time = "2026-08-17T08:20:41.973Z" }, + { url = "https://files.pythonhosted.org/packages/ac/de/b229a39f9bbbe30cbcf9afaaa8993cb65286715c90a3b058c3769196ae02/xxhash-4.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e", size = 33289, upload-time = "2026-08-17T08:20:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/26/6c/dc7cffeadd06336cd934947187cd38abb263103bbc552ca0f55fe4ff595a/xxhash-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428", size = 38444, upload-time = "2026-08-17T08:21:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/75/c9/cf736f6db8c3273af18925061572db0d4357818a9ce425f4b5fb0021918e/xxhash-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc", size = 36195, upload-time = "2026-08-17T08:35:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/da/a2/ca1929354b6851529d0148f7f335b5e2b0281f83bab3e19f0896dc579796/xxhash-4.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451", size = 253113, upload-time = "2026-08-17T08:20:52.152Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/542005206af59518bc8d78a210f1e0172217bc53beb32f64a5b632e72b6b/xxhash-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e", size = 276525, upload-time = "2026-08-17T08:21:01.886Z" }, + { url = "https://files.pythonhosted.org/packages/1b/df/607cff25dcb0f1d35c3b04493f6ad8471edb03fd4eacbdcc5ceddef1f3e9/xxhash-4.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684", size = 297703, upload-time = "2026-08-17T08:21:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/15/ba/9d2275eea0b9d9c6b02921be23f7588356c60df95c763b25f0e045894d43/xxhash-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec", size = 280252, upload-time = "2026-08-17T08:20:47.299Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/2299d9f6369e550aef2abb64945e39daa34412725aa46a20d99b74d76f67/xxhash-4.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d", size = 511041, upload-time = "2026-08-17T08:20:36.771Z" }, + { url = "https://files.pythonhosted.org/packages/83/97/31bd8b8279e6935a0719f6910ced15e9d5a2cd554b253f6027ce1b5a1c2c/xxhash-4.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee", size = 261812, upload-time = "2026-08-17T08:22:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/d180a2da23c105d8e0b02d54f9f5841013fc81c233010ec781e31f1aee4c/xxhash-4.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88", size = 339878, upload-time = "2026-08-17T08:35:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3d/f584cd3172fe934f0f5a0a3917d0d7ce781f74d794fd43bb72be71c3ef6f/xxhash-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9", size = 272871, upload-time = "2026-08-17T08:20:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/2c7956b2b551682e00b9aebce9ceb0a991a131d65f9850c09f5f9760be2e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3", size = 301440, upload-time = "2026-08-17T08:21:35.911Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/0739f6482184a8026f4b022718f5f815d352059312e80696825433f0a8e7/xxhash-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329", size = 260157, upload-time = "2026-08-17T08:22:01.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b31a7bcf1d7d116842812e54f9b944843b4236ea4fa85634e8259f342212/xxhash-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592", size = 278233, upload-time = "2026-08-17T08:21:15.674Z" }, + { url = "https://files.pythonhosted.org/packages/db/e8/5293bae090fc6119dbc5fcf5c4cc0e1536394b52d73b7904d033836c73db/xxhash-4.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1", size = 330270, upload-time = "2026-08-17T08:20:51.128Z" }, + { url = "https://files.pythonhosted.org/packages/72/9e/e2ab12d40921f3f34c9317637d65e011aeababf8288356ea8d527de2c1d0/xxhash-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d", size = 478555, upload-time = "2026-08-17T08:22:04.183Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/c6148d39a49efa95f39b4cf0d41ef35a487f3b30f6fb1fc8fe8d8eab577e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377", size = 258174, upload-time = "2026-08-17T08:35:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fb/0b04b68d6c5bc71c7a2c344f1287327b67e607f28fbcfd937697caca64b6/xxhash-4.0.1-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b", size = 20767, upload-time = "2026-08-17T08:21:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/a6/be/476092aba34d1fcd313e1613a3bb3bc692f253d167b54bc90049043b5034/xxhash-4.0.1-cp312-cp312-win32.whl", hash = "sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433", size = 34669, upload-time = "2026-08-17T08:21:49.486Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/f9413d94fae43cec6d1a74c4f12156c6f4a7f5fd50e1d34defebdee3dec9/xxhash-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e", size = 37073, upload-time = "2026-08-17T08:22:04.886Z" }, + { url = "https://files.pythonhosted.org/packages/c1/83/6fe93c1b95acf962bc61a246df09dc2dcce895ccfc1080c9f48d0b652b92/xxhash-4.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a", size = 33299, upload-time = "2026-08-17T08:35:12.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/c707286b527722f776e1fb81dd202c45623355ba1a2972337a2a26075b2b/xxhash-4.0.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a", size = 43639, upload-time = "2026-08-17T08:20:54.945Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3b/bb71639a0f95635f61936a6f2653599c4261b645ddddd8d00f9dfe3613e2/xxhash-4.0.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256", size = 40657, upload-time = "2026-08-17T08:22:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/76f3f5385faa9886a36f21fcc603f40b4c0c40ce622382f133160c48b4d9/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d", size = 34708, upload-time = "2026-08-17T08:35:24.614Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4a/f48f0e3e1b1ab072979fff2a5be899234e28090883e8b519d0b10215d708/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d", size = 35650, upload-time = "2026-08-17T08:21:09.337Z" }, + { url = "https://files.pythonhosted.org/packages/c4/53/b73d7472b196101ad1f57ed0674af3af803ac3e9ec2feadd650a7b262562/xxhash-4.0.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5", size = 37958, upload-time = "2026-08-17T08:22:10.616Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/024946ad8fa532074af4e4380179da54b7ec9facc8bd0b279ec0fac4e63a/xxhash-4.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf", size = 38032, upload-time = "2026-08-17T08:22:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/934af8d99bb5885711006bec30a691f728edd513d2c40f053f887d8e7577/xxhash-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276", size = 35895, upload-time = "2026-08-17T08:35:16.53Z" }, + { url = "https://files.pythonhosted.org/packages/20/5f/a8011f6a1558f7ca66d9077bb4f192b1871afcea62fbd5733605d2015755/xxhash-4.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794", size = 259464, upload-time = "2026-08-17T08:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/9665a44397547e7a3d58c0942425a976d58dcfd4b538f33220a312bf6912/xxhash-4.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd", size = 283949, upload-time = "2026-08-17T08:22:17.003Z" }, + { url = "https://files.pythonhosted.org/packages/34/2d/78774141266457468f29f3f5803092df4db87d8148ba74e4debd041649db/xxhash-4.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360", size = 303898, upload-time = "2026-08-17T08:35:27.951Z" }, + { url = "https://files.pythonhosted.org/packages/59/48/d78d22de576b42528bff87c14207de50de4f0b888221a50ff7c9d675d670/xxhash-4.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51", size = 287241, upload-time = "2026-08-17T08:21:13.042Z" }, + { url = "https://files.pythonhosted.org/packages/4c/de/7a1755a59c59fd46176f293bbdd99e399a6537ba9537fc723aa4d1bf6e27/xxhash-4.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68", size = 519856, upload-time = "2026-08-17T08:22:15.35Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fb/76580c08e916507859b0f335393cb5fdc59452c4402edbc6bcca6e47e7df/xxhash-4.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349", size = 268572, upload-time = "2026-08-17T08:22:14.533Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/1abde3e07b8f2077a38b4fbfaf764115008bfe0ff03bc7756a52c9fd0607/xxhash-4.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57", size = 344967, upload-time = "2026-08-17T08:35:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/80b6ddf0732eef48a8b5fe717398274794392bd6dbe82af38d189d214772/xxhash-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f", size = 279956, upload-time = "2026-08-17T08:21:24.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/11cbc43c205bf81fad50d69c7319cd1b1ccc01a66cd4fb8766357126c43d/xxhash-4.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa", size = 307583, upload-time = "2026-08-17T08:22:22.541Z" }, + { url = "https://files.pythonhosted.org/packages/1c/11/cf0bc07feb2791045b6ac075d4bf64f1a5beedef2f46ae70d7104d63a19f/xxhash-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91", size = 265848, upload-time = "2026-08-17T08:35:31.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c4/7ada4bea2a2795073dfc42d96842930efbe7a0c1857ef4b522e4e90e5d83/xxhash-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54", size = 284409, upload-time = "2026-08-17T08:21:23.156Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f4/d8ce83dd6b99ccfbdadaf2db968ae40334d2e5f73a0297e593b9ddb3df39/xxhash-4.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53", size = 335921, upload-time = "2026-08-17T08:22:21.802Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9f/f47d8724bd8bc45b395b06b7cacea2dae0d00031af1b707184a091161df6/xxhash-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d", size = 487023, upload-time = "2026-08-17T08:22:19.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/2d87098f3371cc1e42dd04d2285ad56bca4c56667bc501bff02d2b9fd6b5/xxhash-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb", size = 264333, upload-time = "2026-08-17T08:35:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/93795ca5898ec7d7d0455283ad261c0fc76b4f0c0a69e86233bd7badb0bd/xxhash-4.0.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670", size = 20581, upload-time = "2026-08-17T08:21:39.207Z" }, + { url = "https://files.pythonhosted.org/packages/b6/96/926f7335a0a1647952c00421e8da877f658094f61336306c7cadc335c94d/xxhash-4.0.1-cp313-cp313-win32.whl", hash = "sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788", size = 34449, upload-time = "2026-08-17T08:22:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/8a5aeb811de093bab3434e77eff0e9461624a1a56a6a93d315d080aab2aa/xxhash-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375", size = 36520, upload-time = "2026-08-17T08:35:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/14/97f3c74000ca36955e9cb86f6d270dcd5848b5c65afa623453f5cf2d83d6/xxhash-4.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a", size = 33428, upload-time = "2026-08-17T08:21:31.713Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/ea406a02b561d3275232ccfdb3e29df80f7a65414940e3a15721c7bea40f/xxhash-4.0.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df", size = 43747, upload-time = "2026-08-17T08:22:31.37Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/b0c94d61ccf6b5d1f8847b58ef8f923125ac4919ed5bd0eb082750ca7cbd/xxhash-4.0.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e", size = 40749, upload-time = "2026-08-17T08:22:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c5/8085881a538983be0fd1c865d5df236242fea496044e2c8ca32b9f2ba39c/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d", size = 34734, upload-time = "2026-08-17T08:35:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/d3/94/8803d13c968fc75ca434eea991d29ac5fd8a36b4afc9a6a9803c53933db4/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e", size = 35671, upload-time = "2026-08-17T08:21:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/ad91d7f0fd294190d37c08236fe661f5c4e3f83dcd1a121877a2e64681ce/xxhash-4.0.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269", size = 38094, upload-time = "2026-08-17T08:22:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2b7ebdc1869caca5f02c4cba8379b631050d3c3d4adb9187e4dc1a6b8d3c/xxhash-4.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3", size = 38244, upload-time = "2026-08-17T08:35:38.081Z" }, + { url = "https://files.pythonhosted.org/packages/90/9d/f66cf6935f528e575f1ae4d6560d376e7587569747186f4fae8777cadc1b/xxhash-4.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873", size = 35904, upload-time = "2026-08-17T08:21:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/34569d7b482f0dc060074faafd163c588f915cbc3e3e218f1ffd8a3ad340/xxhash-4.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9", size = 259595, upload-time = "2026-08-17T08:22:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/a2370acfcd48732cf5c2b87f06cfbf7fa51c0ce0dd736bde42939eb9ebf7/xxhash-4.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2", size = 284279, upload-time = "2026-08-17T08:22:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/08/15/17d33c24e6c4a1c0b9ddc5584f0c25d51d48b34bacde1416a2235a19db4b/xxhash-4.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6", size = 303973, upload-time = "2026-08-17T08:35:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e0/4ec0d69ad5738729098a61e631b7ed2df22a922b0e03014b597c72bd863d/xxhash-4.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f", size = 287535, upload-time = "2026-08-17T08:21:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4f9b17e7a9eb71c65548ecddd9c18b84e3c18ca41c4d436ad2a3000d3f7b/xxhash-4.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa", size = 519257, upload-time = "2026-08-17T08:22:43.272Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/3276b3e743b8ddbed9c3f71c76d9dd6a75d72aa4e678b1447b635cfd92e0/xxhash-4.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d", size = 268190, upload-time = "2026-08-17T08:35:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/f1555de3c96721320930dbb7988c8482d82b85970076aba1a8d40e83ad43/xxhash-4.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd", size = 345553, upload-time = "2026-08-17T08:21:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c28908f27007087b61139d290f908dd827ffd40b88af0c43f9e1a1a7ffd5/xxhash-4.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736", size = 280499, upload-time = "2026-08-17T08:22:52.236Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/3ef57622c65816348f8196273485baab4752aae064959901e85cd867e067/xxhash-4.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f", size = 307211, upload-time = "2026-08-17T08:22:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/5804504bbc808968e57d6a50286dd8f8cc06e0ddd6e4ab4b1dc89ae42f35/xxhash-4.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8", size = 265865, upload-time = "2026-08-17T08:35:42.727Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ee/8572fdfd70e7aaaf150af899871c2cc0bb88c3295ca82172a31e04ca5168/xxhash-4.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d", size = 284545, upload-time = "2026-08-17T08:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f8/6eadcca0904660c848b466524e82a233d16c9d2d5258433aaf3546142d86/xxhash-4.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6", size = 336022, upload-time = "2026-08-17T08:22:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/4aa107b81602d6d6d09ab5a607c530d2d3a6b28e2e9a59b01875bd877c54/xxhash-4.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7", size = 486671, upload-time = "2026-08-17T08:35:49.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/b7/b2bf9b5301e9cd5f2e335fea8da0f5cf209a6594cb1fe77754774ad4a6fd/xxhash-4.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc", size = 263744, upload-time = "2026-08-17T08:21:56.165Z" }, + { url = "https://files.pythonhosted.org/packages/0b/96/35b1c02177ae26234892c2310fb4822ba62411acccbf425ab8f9fd99354a/xxhash-4.0.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848", size = 20563, upload-time = "2026-08-17T08:35:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/a06300b165fbd6b0cb4a9742987f2e997a9f447ce3bf7c6ac97b862ce62a/xxhash-4.0.1-cp314-cp314-win32.whl", hash = "sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd", size = 35151, upload-time = "2026-08-17T08:22:44.283Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/c5b37296b78f80fc97124c0fee0c7bbd1bdb6f3b18bcd8748bb113b2d8fc/xxhash-4.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2", size = 37156, upload-time = "2026-08-17T08:35:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5e/248f9cd169c2fb62236bedfba246d213bce728f74901e99047e3f3c55875/xxhash-4.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056", size = 34379, upload-time = "2026-08-17T08:21:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/58/c8/db1d37c0da0324d0298f6abd931ca1d4736e049d9f2081230a8421da74d2/xxhash-4.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128", size = 38656, upload-time = "2026-08-17T08:22:49.119Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e18998ec465fb977bc74272e5bf3c2e886c13b014cbef916cd607802c709/xxhash-4.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f", size = 36306, upload-time = "2026-08-17T08:35:52.853Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1a/b83f86f8a987a3cbcb7e005a6824ff64aecae35abc1395a0d44ee16c3319/xxhash-4.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1", size = 273729, upload-time = "2026-08-17T08:21:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/2db15aa8508e0cd5b632927a53b98234f24039ea65377e6cf996c06d2d4f/xxhash-4.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc", size = 301083, upload-time = "2026-08-17T08:35:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/ed759787ffe802bd8e31cfcdad3755cbeca2dcdafd2f790cd6f25d195199/xxhash-4.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a", size = 312745, upload-time = "2026-08-17T08:22:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/f64b4a4cc8b51e950709207f55f7f56ae9c5af6631dd31d7fb443312418c/xxhash-4.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f", size = 301419, upload-time = "2026-08-17T08:35:50.143Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/bac313b8de073569b8db3152044a7cfcce87a3fa9698c18fe9f914dee6b1/xxhash-4.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e", size = 534485, upload-time = "2026-08-17T08:22:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/16b5e419f24e59507ee05626d2bb0deafdb03f9f27783bc0785a9849602e/xxhash-4.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb", size = 279605, upload-time = "2026-08-17T08:22:52.934Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/5787dd6e2d8d5b61256a5039f6b18c2193c7c1de4a2fd2413288d0d9c604/xxhash-4.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c", size = 358924, upload-time = "2026-08-17T08:35:58.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/89be41991f3b0a2e91f940bdf3128852c3ed571cf560d98ad0f67024afe4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c", size = 295305, upload-time = "2026-08-17T08:22:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5a/52ff0a0cc361aad393ff9a46ffe3aabbcf9c03d6c8f2612da7d553048276/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56", size = 320228, upload-time = "2026-08-17T08:35:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b5/91c60ff22c7f6cd5f6d7a5bad5a2cdcb4c33987dfa50bf13f0d856279b2e/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a", size = 279414, upload-time = "2026-08-17T08:23:03.212Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/9685954804d47d0390871a64bec606a0d536406382d71a784df3a5883fb4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914", size = 297594, upload-time = "2026-08-17T08:35:57.881Z" }, + { url = "https://files.pythonhosted.org/packages/89/62/b67ac9412907b7a07a2a0c08c3440b9e4480231a7b3de0767e87011e4564/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd", size = 348526, upload-time = "2026-08-17T08:22:18.571Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/6723cc49a9f567d52d01fd7c1741b0f2e3a13e71d15f7ac49d753a20c115/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f", size = 499307, upload-time = "2026-08-17T08:22:56.52Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/7b10e101ab988d93b791023be7191d7661271d6ab31ac082276b9091042a/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e", size = 274989, upload-time = "2026-08-17T08:36:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/7eabcc8d29cce40621443cff24c07d7306ef574b8956c47ac59f21098005/xxhash-4.0.1-cp314-cp314t-win32.whl", hash = "sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089", size = 35482, upload-time = "2026-08-17T08:22:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/2a4268e1971f63038b79fb75e3b9c8de942cd77acabbb0c5625352a31940/xxhash-4.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6", size = 37490, upload-time = "2026-08-17T08:35:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/90/7b/950ecab1fe4cf421d0a6211ddd9a0ac82e39e55c45a111ceb90953dc6c9a/xxhash-4.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f", size = 34596, upload-time = "2026-08-17T08:23:10.001Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/7dc3b85fac10751613bfedb0e120734e0e8710054abad3f931e9d3843a14/xxhash-4.0.1-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b", size = 43749, upload-time = "2026-08-17T08:36:00.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/bfac071c5b1c6d6a3d48ab1ab96a15e958a1d7061f4afc97804292d87264/xxhash-4.0.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d", size = 40758, upload-time = "2026-08-17T08:22:30.209Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/49a260e685d1a74c56a69432a8ee0527ddcbd684a3c51f87edc3b75639c5/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea", size = 34788, upload-time = "2026-08-17T08:23:09.014Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/50d72ed2170dae872e1c0fe333d0908e0a2afbffe74c5c9037d5406a4b89/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0", size = 35746, upload-time = "2026-08-17T08:36:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/66/f0/969deaa2bab3bfd5ad5b023442124d2255b9961eef6f797ec74eb8683bdf/xxhash-4.0.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c", size = 38098, upload-time = "2026-08-17T08:22:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/86/aa/45ed7d7b8d7b66202a47bf8ff3b77cea28d2ea54dfcdd202b4cfe043e3dc/xxhash-4.0.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c", size = 38251, upload-time = "2026-08-17T08:35:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9d/45e7520a7856e13800a5dc8cd038d34c6372429465b163af0c5722f16918/xxhash-4.0.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34", size = 35986, upload-time = "2026-08-17T08:23:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/5ad466e5fea18c9f9bdc5828c0506f62190061b4a1b0e688aa54969d0a9e/xxhash-4.0.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706", size = 264609, upload-time = "2026-08-17T08:36:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/8f269f85217e3dbd45e31e25e46cc26f3aff0e159ef05d228b4b982c778c/xxhash-4.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6", size = 285910, upload-time = "2026-08-17T08:22:38.589Z" }, + { url = "https://files.pythonhosted.org/packages/ca/30/2fc1a16ee0f9501d074b798ebfae52e24fa602c7117f5c4b81de71eada72/xxhash-4.0.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59", size = 306566, upload-time = "2026-08-17T08:23:16.895Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a7/08375cf2b997e1903663fe7525c5973b1987a4f8ad2b8d47463e9143f2ee/xxhash-4.0.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b", size = 287978, upload-time = "2026-08-17T08:36:09.296Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/90a7b404c11add9e53a497d06236152852490c3b2f21e468d97a58f26afe/xxhash-4.0.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a", size = 520098, upload-time = "2026-08-17T08:22:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/11/02/7fba10b1b17eb46308f09cc0a4ed513d74dff16b1e22a1c439f011c77129/xxhash-4.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b", size = 268273, upload-time = "2026-08-17T08:35:29.043Z" }, + { url = "https://files.pythonhosted.org/packages/54/49/c21b228877357a3be43eeeaa22182ad1685796f415390ada475922c084e4/xxhash-4.0.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535", size = 350164, upload-time = "2026-08-17T08:23:29.494Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/c15bb4aa33d94b78a5553b52e7fa1070565f0199925aeadec3871de20ce9/xxhash-4.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824", size = 281975, upload-time = "2026-08-17T08:36:08.139Z" }, + { url = "https://files.pythonhosted.org/packages/18/7a/b1d0388315fe7752b7725b68a912667526a1dd48ed492fcc031ac03f4b52/xxhash-4.0.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a", size = 307872, upload-time = "2026-08-17T08:22:42.206Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a1/037cb2dd8cf725c9565dfe3712b2915c0e0276a9154913dbfcbcecbeb672/xxhash-4.0.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8", size = 268241, upload-time = "2026-08-17T08:23:23.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/67c44422d0ee082169b238ce24bd2796b82d7c21ed953471365df8c508d8/xxhash-4.0.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0", size = 284970, upload-time = "2026-08-17T08:36:13.476Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/254a5f51c4014cacc77a26f321372338b924f54e89efb730164ee336d850/xxhash-4.0.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9", size = 338074, upload-time = "2026-08-17T08:22:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/64/03/f21c4830118d72ef3a958ce8bf2152f49e0d4cf200907616c9be6caf372a/xxhash-4.0.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0", size = 487626, upload-time = "2026-08-17T08:35:32.768Z" }, + { url = "https://files.pythonhosted.org/packages/45/1f/268a689d741d7da649317eb4ce41760140beb4179aaf43a7216fdbe8100c/xxhash-4.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2", size = 263852, upload-time = "2026-08-17T08:23:41.871Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/adaf8101cd7f143191a0b390600294d83924b32cb13770fde8803dce27a2/xxhash-4.0.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9", size = 20569, upload-time = "2026-08-17T08:36:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/56a5eb8c993420fc07114c08f447a2b66ee996510b4764cb368b9b44c9f0/xxhash-4.0.1-cp315-cp315-win32.whl", hash = "sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330", size = 35145, upload-time = "2026-08-17T08:22:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/67/c7/65f210db43e62157d0fef3b4d4d7b394821e7733c8bb4ece49f91410a725/xxhash-4.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37", size = 37161, upload-time = "2026-08-17T08:23:32.438Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/1a641d1d60ba219756d9ebe907ff0ecf4445adcf4fa96f6e3da57b91d439/xxhash-4.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea", size = 34378, upload-time = "2026-08-17T08:36:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/7698b320b251806d1249e513922a626f19027e104c829a611272250350eb/xxhash-4.0.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f", size = 38610, upload-time = "2026-08-17T08:22:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/436497e775b647b3b3e9a4ffe8c76c59fa4aa7a9fab6447cb59acf1b50ea/xxhash-4.0.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0", size = 36378, upload-time = "2026-08-17T08:35:36.18Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/17a4f8182b9257898aa2a77c2a45f70233eb8e50681a280e8e09d2ee76e9/xxhash-4.0.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0", size = 273559, upload-time = "2026-08-17T08:23:51.075Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/121bd5a5c5adb88e0da772c7bef61964cf9da92956a7a237c7d24c4351b8/xxhash-4.0.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621", size = 299133, upload-time = "2026-08-17T08:36:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/57c7b6e04642ed738a0d08a31bed7fc63fdacb661d665f98739cc9751b62/xxhash-4.0.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5", size = 314527, upload-time = "2026-08-17T08:22:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/42793917dbab0ea1ff71458aea4875e17a7263f2797b798af048dc81e867/xxhash-4.0.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a", size = 299199, upload-time = "2026-08-17T08:23:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/51dc92443923d8e908d5614f1145d8d696450f9d6c8f1abe243c6f2a0222/xxhash-4.0.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619", size = 531967, upload-time = "2026-08-17T08:36:18.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/c5/d0de77de09661fac71742c4155b1cd65e274f7cc277819d702b6c8ff2db5/xxhash-4.0.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32", size = 278764, upload-time = "2026-08-17T08:22:58.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/9a/589929c655aba1bfb2c41ee03e50eec1547c39c3042a66bda9c173a9614b/xxhash-4.0.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60", size = 359876, upload-time = "2026-08-17T08:35:40.35Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a8/c1d8c94d54d91db2215565f4b4151c1593af3e6d27ac4c00fd1e8d714a02/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478", size = 293548, upload-time = "2026-08-17T08:23:54.951Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/85d8abca94508a4dd10561d9dea3e6e68843c6986dd6d9c1b3729c8622e4/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00", size = 318780, upload-time = "2026-08-17T08:36:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/1b/16/2b920ed456b9cdcfc99ddc20c3afe42f9f807ee5850773c12fd891f3c08d/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a", size = 277582, upload-time = "2026-08-17T08:22:57.156Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cc/5811b5997aebb8452047f5800d32fc50eaa29d0ba08d4e426f84450b9c2f/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579", size = 295116, upload-time = "2026-08-17T08:23:40.868Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/c2f3f9c2f4d6aadb79f17a9f1c9a7ee82638cc873680da044cf29537d2ee/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1", size = 347065, upload-time = "2026-08-17T08:36:21.618Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4c/750cc642c92252e10772ec09e1a1d995581ba4c3ceb24f6e2d57c7ce47ca/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3", size = 497208, upload-time = "2026-08-17T08:23:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2d/58693cb13d6395f39b6b9bb40c5e0db53a5df7c9fce805aa7e792f64a1a5/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513", size = 274338, upload-time = "2026-08-17T08:35:44.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/08/9aa9787586d9b3e92d63343ce7dc24f0f445fd9e74ff5d6e85dd82233df5/xxhash-4.0.1-cp315-cp315t-win32.whl", hash = "sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503", size = 35471, upload-time = "2026-08-17T08:24:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ab/4615789c333bee331ac417885c50105715eeb8244bfc68d2bc37dcfd63ca/xxhash-4.0.1-cp315-cp315t-win_amd64.whl", hash = "sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd", size = 37488, upload-time = "2026-08-17T08:36:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/49f718beb0c55d0411bc4bd90b50a3fbe5863a0e97a2f4d11682ba13d298/xxhash-4.0.1-cp315-cp315t-win_arm64.whl", hash = "sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875", size = 34590, upload-time = "2026-08-17T08:23:19.597Z" }, + { url = "https://files.pythonhosted.org/packages/86/79/9127ff42a887a348dc4ce3211cf1a962836887adee6f57078132bfba78b4/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80", size = 31836, upload-time = "2026-08-17T08:36:28.261Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/f238693bfdd642adb59c99683964d46d9947fe721ff44d3bd850ae675407/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b", size = 34453, upload-time = "2026-08-17T08:23:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/796ace33cdfb75c91ba6d11615c3bd436355b9f3103e05865bbee9abce57/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2", size = 38488, upload-time = "2026-08-17T08:23:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/2d549e5d5d7759eaf9ac2d2d2ab81ff60f1bb2b52cdaae8e5ec5c6524354/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f", size = 38206, upload-time = "2026-08-17T08:36:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/1ee576b27f78e6107ee4ea8ac03e8a52888dff256e57d560f8282c195563/xxhash-4.0.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626", size = 37127, upload-time = "2026-08-17T08:23:42.705Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4f/e0648288a17d0d1084ca4f7bef206097831988fc86af74aa1dff8f1fbd68/xxhash-4.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6", size = 36333, upload-time = "2026-08-17T08:23:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/22/15/34b7f72e9b5a8bfd7e6178de9e1e342bc3de9f07111a5ae26c00506d9edf/xxhash-4.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1", size = 33519, upload-time = "2026-08-17T08:36:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/4b/e0af324ccf701bf84a7a060bef11c915d45d9e3c5b9caf5b94d62ecb040b/xxhash-4.0.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee", size = 47995, upload-time = "2026-08-17T08:24:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/cc7130e6ca6b41ab72eb6a03177b933c7c74145545c99a8610ecc208c449/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468", size = 42725, upload-time = "2026-08-17T08:36:34.144Z" }, + { url = "https://files.pythonhosted.org/packages/1c/24/4f26ff9a7dd0998f6d1036bdddef7ce3e78972a74f7fffa7967e7bc3b7e4/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d", size = 39532, upload-time = "2026-08-17T08:23:57.556Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/1ac078fc8fceadcf066469acecacb35d2821cbfaf7d6fc5ac2107c7a314d/xxhash-4.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58", size = 37252, upload-time = "2026-08-17T08:24:06.652Z" }, ] [[package]] @@ -2393,22 +2410,6 @@ version = "0.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, - { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, - { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, - { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" },