From 4ec61493d855d4b0f91db4c1939043913af696c1 Mon Sep 17 00:00:00 2001 From: CaringNihilistic Date: Thu, 18 Jun 2026 01:59:02 +0530 Subject: [PATCH 1/2] feat(deepseek): add DeepSeek instrumentation with R1 reasoning_content support Adds opentelemetry-instrumentation-deepseek package for tracing DeepSeek API calls. Closes #2597. DeepSeek has no native tracing support in OpenLLMetry. Users calling DeepSeek-R1 also lose visibility into reasoning_content (chain-of-thought output) which is not captured by the existing OpenAI instrumentation. - Patches openai.resources.chat.completions (DeepSeek is OpenAI-compatible) - Only activates when base_url contains "deepseek" via _is_deepseek_client() - Captures reasoning_content as gen_ai.deepseek.reasoning_content span attribute - Accumulates reasoning_content across streaming chunks - Follows the Groq instrumentation structure exactly --- .cz.toml | 2 + .../.python-version | 1 + .../README.md | 53 + .../instrumentation/deepseek/__init__.py | 623 ++++++++++++ .../instrumentation/deepseek/config.py | 7 + .../instrumentation/deepseek/event_emitter.py | 147 +++ .../instrumentation/deepseek/event_models.py | 41 + .../instrumentation/deepseek/span_utils.py | 308 ++++++ .../instrumentation/deepseek/utils.py | 91 ++ .../instrumentation/deepseek/version.py | 1 + .../poetry.toml | 2 + .../project.json | 77 ++ .../pyproject.toml | 75 ++ .../tests/__init__.py | 0 .../tests/data/.gitkeep | 0 .../tests/traces/conftest.py | 138 +++ .../tests/traces/test_event_emitter.py | 198 ++++ .../tests/traces/test_finish_reasons.py | 84 ++ .../tests/traces/test_init.py | 644 +++++++++++++ .../tests/traces/test_span_utils.py | 652 +++++++++++++ .../tests/traces/test_utils.py | 99 ++ .../uv.lock | 905 ++++++++++++++++++ packages/sample-app/pyproject.toml | 2 + packages/sample-app/uv.lock | 130 ++- packages/traceloop-sdk/pyproject.toml | 2 + packages/traceloop-sdk/uv.lock | 104 +- 26 files changed, 4315 insertions(+), 71 deletions(-) create mode 100644 packages/opentelemetry-instrumentation-deepseek/.python-version create mode 100644 packages/opentelemetry-instrumentation-deepseek/README.md create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/__init__.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/config.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_emitter.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_models.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/utils.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/version.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/poetry.toml create mode 100644 packages/opentelemetry-instrumentation-deepseek/project.json create mode 100644 packages/opentelemetry-instrumentation-deepseek/pyproject.toml create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/__init__.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/data/.gitkeep create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/traces/conftest.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/traces/test_event_emitter.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/traces/test_finish_reasons.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/traces/test_init.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/traces/test_span_utils.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/tests/traces/test_utils.py create mode 100644 packages/opentelemetry-instrumentation-deepseek/uv.lock diff --git a/.cz.toml b/.cz.toml index 18b34bc743..a3060950d8 100644 --- a/.cz.toml +++ b/.cz.toml @@ -10,6 +10,8 @@ version_files = [ "packages/opentelemetry-instrumentation-mcp/opentelemetry/instrumentation/mcp/version.py", "packages/opentelemetry-instrumentation-groq/pyproject.toml:^version", "packages/opentelemetry-instrumentation-groq/opentelemetry/instrumentation/groq/version.py", + "packages/opentelemetry-instrumentation-deepseek/pyproject.toml:^version", + "packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/version.py", "packages/opentelemetry-instrumentation-agno/pyproject.toml:^version", "packages/opentelemetry-instrumentation-agno/opentelemetry/instrumentation/agno/version.py", "packages/opentelemetry-instrumentation-alephalpha/pyproject.toml:^version", diff --git a/packages/opentelemetry-instrumentation-deepseek/.python-version b/packages/opentelemetry-instrumentation-deepseek/.python-version new file mode 100644 index 0000000000..c8cfe39591 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/packages/opentelemetry-instrumentation-deepseek/README.md b/packages/opentelemetry-instrumentation-deepseek/README.md new file mode 100644 index 0000000000..5a8ca803ed --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/README.md @@ -0,0 +1,53 @@ +# OpenTelemetry DeepSeek Instrumentation + + + PyPI version + + +This library allows tracing prompts and completions sent to [DeepSeek](https://platform.deepseek.com/) using the official [OpenAI SDK](https://github.com/openai/openai-python), since DeepSeek's API is OpenAI-compatible. + +## Installation + +```bash +pip install opentelemetry-instrumentation-deepseek +``` + +## Example usage + +```python +from opentelemetry.instrumentation.deepseek import DeepSeekInstrumentor + +DeepSeekInstrumentor().instrument() +``` + +This instrumentor patches `openai.resources.chat.completions.Completions.create` and `AsyncCompletions.create`, but only creates spans for requests made through a client configured with a DeepSeek `base_url`. Regular OpenAI clients are left untouched, so this instrumentation can be safely enabled alongside [opentelemetry-instrumentation-openai](https://pypi.org/project/opentelemetry-instrumentation-openai/). + +```python +from openai import OpenAI + +client = OpenAI( + api_key="", + base_url="https://api.deepseek.com", +) + +response = client.chat.completions.create( + model="deepseek-chat", + messages=[{"role": "user", "content": "Hello!"}], +) +``` + +## DeepSeek-R1 reasoning content + +The `deepseek-reasoner` model (DeepSeek-R1) returns its chain-of-thought in a `reasoning_content` field on the response message, in addition to the final `content`. This instrumentation captures `reasoning_content` as the `gen_ai.deepseek.reasoning_content` span attribute, for both streaming and non-streaming responses. + +## Privacy + +**By default, this instrumentation logs prompts, completions, and embeddings to span attributes**. This gives you a clear visibility into how your LLM application is working, and can make it easy to debug and evaluate the quality of the outputs. + +However, you may want to disable this logging for privacy reasons, as they may contain highly sensitive data from your users. You may also simply want to reduce the size of your traces. + +To disable logging, set the `TRACELOOP_TRACE_CONTENT` environment variable to `false`. + +```bash +TRACELOOP_TRACE_CONTENT=false +``` diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/__init__.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/__init__.py new file mode 100644 index 0000000000..3426a0db63 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/__init__.py @@ -0,0 +1,623 @@ +"""OpenTelemetry DeepSeek instrumentation""" + +import logging +import os +import time +import warnings +from typing import Callable, Collection, Optional, Union + +from opentelemetry import context as context_api +from opentelemetry._logs import Logger, get_logger +from opentelemetry.instrumentation.deepseek.config import Config +from opentelemetry.instrumentation.deepseek.event_emitter import ( + emit_choice_events, + emit_message_events, + emit_streaming_response_events, +) +from opentelemetry.instrumentation.deepseek.span_utils import ( + set_input_attributes, + set_model_input_attributes, + set_model_response_attributes, + set_model_streaming_response_attributes, + set_response_attributes, + set_streaming_response_attributes, +) +from opentelemetry.instrumentation.deepseek.utils import ( + error_metrics_attributes, + shared_metrics_attributes, + should_emit_events, +) +from opentelemetry.instrumentation.deepseek.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap +from opentelemetry.metrics import Counter, Histogram, Meter, get_meter +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv_ai import ( + SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, + Meters, +) +from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE +from wrapt import wrap_function_wrapper + +from openai._streaming import AsyncStream, Stream +from openai.types.completion_usage import CompletionUsage + +logger = logging.getLogger(__name__) + +_instruments = ("openai >= 1.0.0",) + +_DEEPSEEK = GenAIAttributes.GenAiProviderNameValues.DEEPSEEK.value +_CHAT = GenAIAttributes.GenAiOperationNameValues.CHAT.value + +WRAPPED_METHODS = [ + { + "package": "openai.resources.chat.completions", + "object": "Completions", + "method": "create", + }, +] +WRAPPED_AMETHODS = [ + { + "package": "openai.resources.chat.completions", + "object": "AsyncCompletions", + "method": "create", + }, +] + + +def is_streaming_response(response): + return isinstance(response, Stream) or isinstance(response, AsyncStream) + + +def _is_deepseek_client(instance) -> bool: + """DeepSeek is accessed via the OpenAI SDK pointed at a custom base_url. + + Only instrument calls made through a client configured for the DeepSeek API, + so this instrumentation can be safely enabled alongside the OpenAI instrumentation. + """ + client = getattr(instance, "_client", None) + base_url = getattr(client, "base_url", "") if client else "" + return "deepseek" in str(base_url).lower() + + +def _with_chat_telemetry_wrapper(func): + """Helper for providing tracer for wrapper functions. Includes metric collectors.""" + + def _with_chat_telemetry( + tracer, + token_histogram, + choice_counter, + duration_histogram, + event_logger, + to_wrap, + ): + def wrapper(wrapped, instance, args, kwargs): + return func( + tracer, + token_histogram, + choice_counter, + duration_histogram, + event_logger, + to_wrap, + wrapped, + instance, + args, + kwargs, + ) + + return wrapper + + return _with_chat_telemetry + + +def _create_metrics(meter: Meter): + token_histogram = meter.create_histogram( + name=Meters.LLM_TOKEN_USAGE, + unit="token", + description="Measures number of input and output tokens used", + ) + + choice_counter = meter.create_counter( + name=Meters.LLM_GENERATION_CHOICES, + unit="choice", + description="Number of choices returned by chat completions call", + ) + + duration_histogram = meter.create_histogram( + name=Meters.LLM_OPERATION_DURATION, + unit="s", + description="GenAI operation duration", + ) + + return token_histogram, choice_counter, duration_histogram + + +def _process_streaming_chunk(chunk): + """Extract content, reasoning_content, tool_calls_delta, finish_reasons and usage from a streaming chunk.""" + if not chunk.choices: + return None, "", [], [], None + + content = "" + reasoning_content = "" + tool_calls_delta = [] + finish_reasons = [] + for choice in chunk.choices: + delta = choice.delta + if delta.content: + content += delta.content + # DeepSeek-R1 (deepseek-reasoner) streams chain-of-thought via `delta.reasoning_content`. + delta_reasoning_content = getattr(delta, "reasoning_content", None) + if delta_reasoning_content: + reasoning_content += delta_reasoning_content + if delta.tool_calls: + tool_calls_delta.extend(delta.tool_calls) + if choice.finish_reason: + finish_reasons.append(choice.finish_reason) + + # Usage is only present (on the final chunk) when stream_options={"include_usage": True}. + usage = chunk.usage if getattr(chunk, "usage", None) else None + + return content, reasoning_content, tool_calls_delta, finish_reasons, usage + + +def _accumulate_tool_calls(accumulated: dict, tool_calls_delta: list) -> None: + """Merge a list of streaming tool_call delta objects into the accumulator dict. + + The accumulator maps tool call index → {id, function: {name, arguments}}. + Arguments arrive as JSON fragments and are concatenated across chunks. + """ + for tc in tool_calls_delta: + idx = tc.index or 0 + tc_id = tc.id or "" + fn = tc.function + fn_name = (fn.name or "") if fn else "" + fn_args = (fn.arguments or "") if fn else "" + + if idx not in accumulated: + accumulated[idx] = {"id": tc_id, "function": {"name": fn_name, "arguments": ""}} + else: + if tc_id: + accumulated[idx]["id"] = tc_id + if fn_name: + accumulated[idx]["function"]["name"] = fn_name + accumulated[idx]["function"]["arguments"] += fn_args + + +def _handle_streaming_response( + span: Span, + accumulated_content: str, + accumulated_reasoning: str, + tool_calls: dict, + finish_reasons: list[str], + usage: Union[CompletionUsage, None], + event_logger: Union[Logger, None], +) -> None: + # finish_reasons is a list; use first entry for message-level finish_reason + finish_reason = finish_reasons[0] if finish_reasons else None + set_model_streaming_response_attributes(span, usage, finish_reasons) + if should_emit_events() and event_logger: + emit_streaming_response_events(accumulated_content, finish_reason, event_logger, tool_calls=tool_calls) + else: + set_streaming_response_attributes( + span, + accumulated_content, + finish_reason, + tool_calls=tool_calls, + accumulated_reasoning=accumulated_reasoning, + ) + + +def _create_stream_processor(response, span, event_logger): + """Create a generator that processes a stream while collecting telemetry.""" + accumulated_content = "" + accumulated_reasoning = "" + accumulated_tool_calls: dict = {} + accumulated_finish_reasons: list = [] + usage = None + + try: + for chunk in response: + content, reasoning_content, tool_calls_delta, chunk_finish_reasons, chunk_usage = ( + _process_streaming_chunk(chunk) + ) + if content: + accumulated_content += content + if reasoning_content: + accumulated_reasoning += reasoning_content + if tool_calls_delta: + _accumulate_tool_calls(accumulated_tool_calls, tool_calls_delta) + accumulated_finish_reasons.extend(chunk_finish_reasons) + if chunk_usage: + usage = chunk_usage + yield chunk + except Exception as e: + span.set_attribute(ERROR_TYPE, e.__class__.__name__) + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + else: + tool_calls = [accumulated_tool_calls[i] for i in sorted(accumulated_tool_calls)] or None + _handle_streaming_response( + span, + accumulated_content, + accumulated_reasoning, + tool_calls, + accumulated_finish_reasons, + usage, + event_logger, + ) + if span.is_recording(): + span.set_status(Status(StatusCode.OK)) + finally: + span.end() + + +async def _create_async_stream_processor(response, span, event_logger): + """Create an async generator that processes a stream while collecting telemetry.""" + accumulated_content = "" + accumulated_reasoning = "" + accumulated_tool_calls: dict = {} + accumulated_finish_reasons: list = [] + usage = None + + try: + async for chunk in response: + content, reasoning_content, tool_calls_delta, chunk_finish_reasons, chunk_usage = ( + _process_streaming_chunk(chunk) + ) + if content: + accumulated_content += content + if reasoning_content: + accumulated_reasoning += reasoning_content + if tool_calls_delta: + _accumulate_tool_calls(accumulated_tool_calls, tool_calls_delta) + accumulated_finish_reasons.extend(chunk_finish_reasons) + if chunk_usage: + usage = chunk_usage + yield chunk + except Exception as e: + span.set_attribute(ERROR_TYPE, e.__class__.__name__) + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + else: + tool_calls = [accumulated_tool_calls[i] for i in sorted(accumulated_tool_calls)] or None + _handle_streaming_response( + span, + accumulated_content, + accumulated_reasoning, + tool_calls, + accumulated_finish_reasons, + usage, + event_logger, + ) + if span.is_recording(): + span.set_status(Status(StatusCode.OK)) + finally: + span.end() + + +def _handle_input(span, kwargs, event_logger): + set_model_input_attributes(span, kwargs) + if should_emit_events() and event_logger: + emit_message_events(kwargs, event_logger) + else: + set_input_attributes(span, kwargs) + + +def _handle_response(span, response, token_histogram, event_logger): + set_model_response_attributes(span, response, token_histogram) + if should_emit_events() and event_logger: + emit_choice_events(response, event_logger) + else: + set_response_attributes(span, response) + + +@_with_chat_telemetry_wrapper +def _wrap( + tracer: Tracer, + token_histogram: Histogram, + choice_counter: Counter, + duration_histogram: Histogram, + event_logger: Union[Logger, None], + to_wrap, + wrapped, + instance, + args, + kwargs, +): + """Instruments and calls every function defined in TO_WRAP.""" + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value( + SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY + ): + return wrapped(*args, **kwargs) + + if not _is_deepseek_client(instance): + return wrapped(*args, **kwargs) + + llm_model = kwargs.get("model", "") + span = tracer.start_span( + f"{_CHAT} {llm_model}", + kind=SpanKind.CLIENT, + attributes={ + GenAIAttributes.GEN_AI_PROVIDER_NAME: _DEEPSEEK, + GenAIAttributes.GEN_AI_OPERATION_NAME: _CHAT, + GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model, + }, + ) + + _handle_input(span, kwargs, event_logger) + + start_time = time.time() + try: + response = wrapped(*args, **kwargs) + except Exception as e: + end_time = time.time() + attributes = error_metrics_attributes(e) + + if duration_histogram: + duration = end_time - start_time + duration_histogram.record(duration, attributes=attributes) + + span.set_attribute(ERROR_TYPE, e.__class__.__name__) + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + span.end() + raise + + end_time = time.time() + + if is_streaming_response(response): + try: + return _create_stream_processor(response, span, event_logger) + except Exception as ex: + logger.warning( + "Failed to process streaming response for deepseek span, error: %s", + str(ex), + ) + span.set_status(Status(StatusCode.ERROR)) + span.end() + raise + elif response: + try: + metric_attributes = shared_metrics_attributes(response) + + if duration_histogram: + duration = time.time() - start_time + duration_histogram.record( + duration, + attributes=metric_attributes, + ) + + _handle_response(span, response, token_histogram, event_logger) + + except Exception as ex: + logger.warning( + "Failed to set response attributes for deepseek span, error: %s", + str(ex), + ) + + if span.is_recording(): + span.set_status(Status(StatusCode.OK)) + span.end() + return response + + +@_with_chat_telemetry_wrapper +async def _awrap( + tracer, + token_histogram: Histogram, + choice_counter: Counter, + duration_histogram: Histogram, + event_logger: Union[Logger, None], + to_wrap, + wrapped, + instance, + args, + kwargs, +): + """Instruments and calls every function defined in TO_WRAP.""" + if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value( + SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY + ): + return await wrapped(*args, **kwargs) + + if not _is_deepseek_client(instance): + return await wrapped(*args, **kwargs) + + llm_model = kwargs.get("model", "") + span = tracer.start_span( + f"{_CHAT} {llm_model}", + kind=SpanKind.CLIENT, + attributes={ + GenAIAttributes.GEN_AI_PROVIDER_NAME: _DEEPSEEK, + GenAIAttributes.GEN_AI_OPERATION_NAME: _CHAT, + GenAIAttributes.GEN_AI_REQUEST_MODEL: llm_model, + }, + ) + + _handle_input(span, kwargs, event_logger) + + start_time = time.time() + + try: + response = await wrapped(*args, **kwargs) + except Exception as e: + end_time = time.time() + attributes = error_metrics_attributes(e) + + if duration_histogram: + duration = end_time - start_time + duration_histogram.record(duration, attributes=attributes) + + span.set_attribute(ERROR_TYPE, e.__class__.__name__) + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + span.end() + raise + + end_time = time.time() + + if is_streaming_response(response): + try: + return _create_async_stream_processor(response, span, event_logger) + except Exception as ex: + logger.warning( + "Failed to process streaming response for deepseek span, error: %s", + str(ex), + ) + span.set_status(Status(StatusCode.ERROR)) + span.end() + raise + elif response: + try: + metric_attributes = shared_metrics_attributes(response) + + if duration_histogram: + duration = time.time() - start_time + duration_histogram.record( + duration, + attributes=metric_attributes, + ) + + _handle_response(span, response, token_histogram, event_logger) + + except Exception as ex: + logger.warning( + "Failed to set response attributes for deepseek span, error: %s", + str(ex), + ) + + if span.is_recording(): + span.set_status(Status(StatusCode.OK)) + span.end() + return response + + +def is_metrics_enabled() -> bool: + return (os.getenv("TRACELOOP_METRICS_ENABLED") or "true").lower() == "true" + + +class DeepSeekInstrumentor(BaseInstrumentor): + """An instrumentor for DeepSeek's chat completions API, accessed via the OpenAI SDK.""" + + def __init__( + self, + exception_logger=None, + use_attributes: Optional[bool] = None, + get_common_metrics_attributes: Callable[[], dict] = lambda: {}, + use_legacy_attributes: Optional[bool] = None, + ): + super().__init__() + if use_attributes is not None and use_legacy_attributes is not None: + raise TypeError( + "Cannot pass both `use_attributes` and `use_legacy_attributes`; " + "`use_legacy_attributes` is deprecated, use `use_attributes` instead." + ) + if use_legacy_attributes is not None: + warnings.warn( + "`use_legacy_attributes` is deprecated and will be removed in a " + "future release; use `use_attributes` instead. The current OTel " + "GenAI spec emits prompts/completions as span attributes " + "(`gen_ai.input.messages` / `gen_ai.output.messages`), which is " + "what `use_attributes=True` (the default) does. " + "`use_attributes=False` opts into the events path instead.", + DeprecationWarning, + stacklevel=2, + ) + use_attributes = use_legacy_attributes + if use_attributes is None: + use_attributes = True + Config.exception_logger = exception_logger + Config.get_common_metrics_attributes = get_common_metrics_attributes + Config.use_legacy_attributes = use_attributes + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + tracer_provider = kwargs.get("tracer_provider") + tracer = get_tracer(__name__, __version__, tracer_provider) + + # meter and counters are inited here + meter_provider = kwargs.get("meter_provider") + meter = get_meter(__name__, __version__, meter_provider) + + if is_metrics_enabled(): + ( + token_histogram, + choice_counter, + duration_histogram, + ) = _create_metrics(meter) + else: + ( + token_histogram, + choice_counter, + duration_histogram, + ) = (None, None, None) + + event_logger = None + if not Config.use_legacy_attributes: + logger_provider = kwargs.get("logger_provider") + event_logger = get_logger(__name__, __version__, logger_provider=logger_provider) + + for wrapped_method in WRAPPED_METHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + wrap_method = wrapped_method.get("method") + + try: + wrap_function_wrapper( + wrap_package, + f"{wrap_object}.{wrap_method}", + _wrap( + tracer, + token_histogram, + choice_counter, + duration_histogram, + event_logger, + wrapped_method, + ), + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist + + for wrapped_method in WRAPPED_AMETHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + wrap_method = wrapped_method.get("method") + try: + wrap_function_wrapper( + wrap_package, + f"{wrap_object}.{wrap_method}", + _awrap( + tracer, + token_histogram, + choice_counter, + duration_histogram, + event_logger, + wrapped_method, + ), + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist + + def _uninstrument(self, **kwargs): + for wrapped_method in WRAPPED_METHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + unwrap( + f"{wrap_package}.{wrap_object}", + wrapped_method.get("method"), + ) + for wrapped_method in WRAPPED_AMETHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + unwrap( + f"{wrap_package}.{wrap_object}", + wrapped_method.get("method"), + ) diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/config.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/config.py new file mode 100644 index 0000000000..f78d2f0d60 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/config.py @@ -0,0 +1,7 @@ +from typing import Callable + + +class Config: + exception_logger = None + get_common_metrics_attributes: Callable[[], dict] = lambda: {} + use_legacy_attributes = True diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_emitter.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_emitter.py new file mode 100644 index 0000000000..79853cb0f2 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_emitter.py @@ -0,0 +1,147 @@ +from dataclasses import asdict +from enum import Enum +from typing import Union + +from opentelemetry._logs import Logger, LogRecord +from opentelemetry.instrumentation.deepseek.event_models import ChoiceEvent, MessageEvent +from opentelemetry.instrumentation.deepseek.span_utils import _map_deepseek_finish_reason +from opentelemetry.instrumentation.deepseek.utils import ( + dont_throw, + should_emit_events, + should_send_prompts, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) + +from openai.types.chat.chat_completion import ChatCompletion + + +class Roles(Enum): + USER = "user" + ASSISTANT = "assistant" + SYSTEM = "system" + TOOL = "tool" + + +VALID_MESSAGE_ROLES = {role.value for role in Roles} +"""The valid roles for naming the message event.""" + +EVENT_ATTRIBUTES = {GenAIAttributes.GEN_AI_PROVIDER_NAME: GenAIAttributes.GenAiProviderNameValues.DEEPSEEK.value} +"""The attributes to be used for the event.""" + + +@dont_throw +def emit_message_events(kwargs: dict, event_logger): + for message in kwargs.get("messages", []): + emit_event( + MessageEvent( + content=message.get("content"), + role=message.get("role", "unknown"), + tool_calls=message.get("tool_calls"), + ), + event_logger=event_logger, + ) + + +@dont_throw +def emit_choice_events(response: ChatCompletion, event_logger): + for choice in response.choices: + emit_event( + ChoiceEvent( + index=choice.index, + message={ + "content": choice.message.content, + "role": choice.message.role or "unknown", + }, + finish_reason=_map_deepseek_finish_reason(choice.finish_reason), + tool_calls=choice.message.tool_calls or None, + ), + event_logger=event_logger, + ) + + +@dont_throw +def emit_streaming_response_events( + accumulated_content: str, + finish_reason: Union[str, None], + event_logger, + tool_calls=None, +): + """Emit events for streaming response.""" + emit_event( + ChoiceEvent( + index=0, + message={"content": accumulated_content, "role": "assistant"}, + finish_reason=_map_deepseek_finish_reason(finish_reason), + tool_calls=tool_calls, + ), + event_logger, + ) + + +def emit_event(event: Union[MessageEvent, ChoiceEvent], event_logger: Union[Logger, None]) -> None: + """ + Emit an event to the OpenTelemetry SDK. + + Args: + event: The event to emit. + """ + if not should_emit_events() or event_logger is None: + return + + if isinstance(event, MessageEvent): + _emit_message_event(event, event_logger) + elif isinstance(event, ChoiceEvent): + _emit_choice_event(event, event_logger) + else: + raise TypeError("Unsupported event type") + + +def _emit_message_event(event: MessageEvent, event_logger: Logger) -> None: + body = asdict(event) + + if event.role in VALID_MESSAGE_ROLES: + name = "gen_ai.{}.message".format(event.role) + # According to the semantic conventions, the role is conditionally required if available + # and not equal to the "role" in the message name. So, remove the role from the body if + # it is the same as the in the event name. + body.pop("role", None) + else: + name = "gen_ai.user.message" + + # According to the semantic conventions, only the assistant role has tool call + if event.role != Roles.ASSISTANT.value and event.tool_calls is not None: + del body["tool_calls"] + elif event.tool_calls is None: + del body["tool_calls"] + + if not should_send_prompts(): + del body["content"] + if body.get("tool_calls") is not None: + for tool_call in body["tool_calls"]: + tool_call["function"].pop("arguments", None) + + log_record = LogRecord(body=body, attributes=EVENT_ATTRIBUTES, event_name=name) + event_logger.emit(log_record) + + +def _emit_choice_event(event: ChoiceEvent, event_logger: Logger) -> None: + body = asdict(event) + if event.message["role"] == Roles.ASSISTANT.value: + # According to the semantic conventions, the role is conditionally required if available + # and not equal to "assistant", so remove the role from the body if it is "assistant". + body["message"].pop("role", None) + + if event.tool_calls is None: + del body["tool_calls"] + + if not should_send_prompts(): + body["message"].pop("content", None) + body["message"].pop("role", None) + if body.get("tool_calls") is not None: + for tool_call in body["tool_calls"]: + tool_call["function"].pop("arguments", None) + + log_record = LogRecord(body=body, attributes=EVENT_ATTRIBUTES, event_name="gen_ai.choice") + event_logger.emit(log_record) diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_models.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_models.py new file mode 100644 index 0000000000..b4f68e58f7 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/event_models.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass +from typing import Any, List, Literal, Optional, TypedDict + + +class _FunctionToolCall(TypedDict): + function_name: str + arguments: Optional[dict[str, Any]] + + +class ToolCall(TypedDict): + """Represents a tool call in the AI model.""" + + id: str + function: _FunctionToolCall + type: Literal["function"] + + +class CompletionMessage(TypedDict): + """Represents a message in the AI model.""" + + content: Any + role: str + + +@dataclass +class MessageEvent: + """Represents an input event for the AI model.""" + + content: Any + role: str = "user" + tool_calls: Optional[List[ToolCall]] = None + + +@dataclass +class ChoiceEvent: + """Represents a completion event for the AI model.""" + + index: int + message: CompletionMessage + finish_reason: str = "" + tool_calls: Optional[List[ToolCall]] = None diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py new file mode 100644 index 0000000000..e33ccc4f2c --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py @@ -0,0 +1,308 @@ +import json + +from opentelemetry.instrumentation.deepseek.utils import ( + dont_throw, + model_as_dict, + set_span_attribute, + should_send_prompts, +) +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( + GEN_AI_RESPONSE_ID, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv_ai import ( + SpanAttributes, +) + +_DEEPSEEK_PROVIDER = GenAIAttributes.GenAiProviderNameValues.DEEPSEEK.value +_CHAT_OPERATION = GenAIAttributes.GenAiOperationNameValues.CHAT.value + +GEN_AI_DEEPSEEK_REASONING_CONTENT = "gen_ai.deepseek.reasoning_content" + +# DeepSeek's API is OpenAI-compatible and returns finish_reason as an OpenAI-compatible string. +# Map to OTel standard values; unknown / None → "". +# Note: DeepSeek "tool_calls" (plural, OpenAI-compatible) → OTel "tool_call" (singular). +_FINISH_REASON_MAP = { + "stop": "stop", + "length": "length", + "content_filter": "content_filter", + "tool_calls": "tool_call", +} + + +def _map_deepseek_finish_reason(finish_reason): + if not finish_reason: + return "" + return _FINISH_REASON_MAP.get(str(finish_reason), str(finish_reason)) + + +def _collect_finish_reasons_from_response(response): + if response is None: + return [] + choices = getattr(response, "choices", None) or [] + return [_map_deepseek_finish_reason(getattr(c, "finish_reason", None)) for c in choices] + + +def _content_to_parts(content): + """Convert OpenAI-compatible message content to OTel parts array.""" + if content is None: + return [] + if isinstance(content, str): + return [{"type": "text", "content": content}] if content else [] + # List of content blocks (multimodal) + parts = [] + for block in content: + if not isinstance(block, dict): + continue + block_type = block.get("type") + if block_type == "text": + parts.append({"type": "text", "content": block.get("text", "")}) + elif block_type == "image_url": + url = (block.get("image_url") or {}).get("url", "") + if url.startswith("data:"): + try: + header, data = url.split(",", 1) + mime_type = header.split(":")[1].split(";")[0] + parts.append({"type": "blob", "modality": "image", "mime_type": mime_type, "content": data}) + except Exception: + parts.append({"type": "uri", "modality": "image", "uri": url}) + else: + parts.append({"type": "uri", "modality": "image", "uri": url}) + else: + parts.append({"type": block_type or "unknown", **{k: v for k, v in block.items() if k != "type"}}) + return parts + + +def _tool_calls_to_parts(tool_calls): + """Convert OpenAI tool_calls list to OTel tool_call parts. + + Handles both dict representations (user kwargs) and object representations + (e.g. Pydantic models returned by the OpenAI SDK). + """ + parts = [] + for tc in tool_calls or []: + if isinstance(tc, dict): + tc_id = tc.get("id") or "" + fn = tc.get("function") or {} + fn_name = fn.get("name") or "" + args_raw = fn.get("arguments") + elif hasattr(tc, "function"): + tc_id = getattr(tc, "id", "") or "" + fn = tc.function + fn_name = getattr(fn, "name", "") or "" + args_raw = getattr(fn, "arguments", None) + else: + continue + if isinstance(args_raw, str): + try: + args = json.loads(args_raw) + except (json.JSONDecodeError, TypeError): + args = args_raw + elif isinstance(args_raw, dict): + args = args_raw + else: + args = None + part = {"type": "tool_call", "name": fn_name} + if tc_id: + part["id"] = tc_id + if args is not None: + part["arguments"] = args + parts.append(part) + return parts + + +@dont_throw +def set_input_attributes(span, kwargs): + if not span.is_recording() or not should_send_prompts(): + return + + messages = [] + for msg in kwargs.get("messages") or []: + role = msg.get("role", "user") + + if role == "tool": + parts = [ + { + "type": "tool_call_response", + "id": msg.get("tool_call_id") or "", + "response": msg.get("content") or "", + } + ] + else: + parts = _content_to_parts(msg.get("content")) + tool_calls = msg.get("tool_calls") + if tool_calls: + parts.extend(_tool_calls_to_parts(tool_calls)) + + messages.append({"role": role, "parts": parts}) + + if messages: + set_span_attribute(span, GenAIAttributes.GEN_AI_INPUT_MESSAGES, json.dumps(messages)) + + +@dont_throw +def set_model_input_attributes(span, kwargs): + if not span.is_recording(): + return + + set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, kwargs.get("model")) + set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, kwargs.get("max_tokens")) + set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, kwargs.get("temperature")) + set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TOP_P, kwargs.get("top_p")) + set_span_attribute( + span, + GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY, + kwargs.get("frequency_penalty"), + ) + set_span_attribute( + span, + GenAIAttributes.GEN_AI_REQUEST_PRESENCE_PENALTY, + kwargs.get("presence_penalty"), + ) + set_span_attribute(span, SpanAttributes.GEN_AI_IS_STREAMING, kwargs.get("stream") or False) + + if should_send_prompts(): + tools = kwargs.get("tools") + if tools: + try: + set_span_attribute(span, GenAIAttributes.GEN_AI_TOOL_DEFINITIONS, json.dumps(tools)) + except Exception: + pass + + +def set_streaming_response_attributes( + span, + accumulated_content, + finish_reason=None, + tool_calls=None, + accumulated_reasoning=None, +): + """Set gen_ai.output.messages span attribute for accumulated streaming response.""" + if not span.is_recording() or not should_send_prompts(): + return + + mapped_reason = _map_deepseek_finish_reason(finish_reason) + parts = [{"type": "text", "content": accumulated_content}] if accumulated_content else [] + if tool_calls: + parts.extend(_tool_calls_to_parts(tool_calls)) + message = {"role": "assistant", "parts": parts, "finish_reason": mapped_reason} + set_span_attribute(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES, json.dumps([message])) + + if accumulated_reasoning: + set_span_attribute(span, GEN_AI_DEEPSEEK_REASONING_CONTENT, accumulated_reasoning) + + +def set_model_streaming_response_attributes(span, usage, finish_reasons=None): + if not span.is_recording(): + return + + if usage: + set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, usage.completion_tokens) + set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, usage.prompt_tokens) + set_span_attribute(span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, usage.total_tokens) + + if finish_reasons: + mapped = [_map_deepseek_finish_reason(fr) for fr in finish_reasons] + mapped = [m for m in mapped if m] + if mapped: + set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS, mapped) + + +@dont_throw +def set_model_response_attributes(span, response, token_histogram): + if not span.is_recording(): + return + + reasons = [r for r in _collect_finish_reasons_from_response(response) if r] + if reasons: + set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS, reasons) + + response = model_as_dict(response) + set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.get("model")) + set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id")) + + usage = response.get("usage") or {} + prompt_tokens = usage.get("prompt_tokens") + completion_tokens = usage.get("completion_tokens") + if usage: + set_span_attribute(span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, usage.get("total_tokens")) + set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens) + set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens) + + if isinstance(prompt_tokens, int) and prompt_tokens >= 0 and token_histogram is not None: + token_histogram.record( + prompt_tokens, + attributes={ + GenAIAttributes.GEN_AI_PROVIDER_NAME: _DEEPSEEK_PROVIDER, + GenAIAttributes.GEN_AI_OPERATION_NAME: _CHAT_OPERATION, + GenAIAttributes.GEN_AI_REQUEST_MODEL: response.get("model"), + GenAIAttributes.GEN_AI_TOKEN_TYPE: "input", + GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.get("model"), + }, + ) + + if isinstance(completion_tokens, int) and completion_tokens >= 0 and token_histogram is not None: + token_histogram.record( + completion_tokens, + attributes={ + GenAIAttributes.GEN_AI_PROVIDER_NAME: _DEEPSEEK_PROVIDER, + GenAIAttributes.GEN_AI_OPERATION_NAME: _CHAT_OPERATION, + GenAIAttributes.GEN_AI_REQUEST_MODEL: response.get("model"), + GenAIAttributes.GEN_AI_TOKEN_TYPE: "output", + GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.get("model"), + }, + ) + + +def set_response_attributes(span, response): + if not span.is_recording() or not should_send_prompts(): + return + + choices = model_as_dict(response).get("choices") or [] + messages = [] + reasoning_contents = [] + + for choice in choices: + message = choice.get("message") or {} + finish_reason = _map_deepseek_finish_reason(choice.get("finish_reason")) + role = message.get("role") or "assistant" + + parts = _content_to_parts(message.get("content")) + + # tool_calls (modern OpenAI format) + tool_calls = message.get("tool_calls") + if tool_calls: + parts.extend(_tool_calls_to_parts(tool_calls)) + + # function_call (legacy OpenAI format) + function_call = message.get("function_call") + if function_call: + args_raw = function_call.get("arguments") + if isinstance(args_raw, str): + try: + args = json.loads(args_raw) + except (json.JSONDecodeError, TypeError): + args = args_raw + elif isinstance(args_raw, dict): + args = args_raw + else: + args = None + part = {"type": "tool_call", "name": function_call.get("name") or ""} + if args is not None: + part["arguments"] = args + parts.append(part) + + messages.append({"role": role, "parts": parts, "finish_reason": finish_reason}) + + # DeepSeek-R1 (deepseek-reasoner) returns chain-of-thought in `reasoning_content`. + reasoning_content = message.get("reasoning_content") + if reasoning_content: + reasoning_contents.append(reasoning_content) + + if messages: + set_span_attribute(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES, json.dumps(messages)) + + if reasoning_contents: + set_span_attribute(span, GEN_AI_DEEPSEEK_REASONING_CONTENT, "\n---\n".join(reasoning_contents)) diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/utils.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/utils.py new file mode 100644 index 0000000000..a62b44417b --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/utils.py @@ -0,0 +1,91 @@ +import logging +import os +import traceback +from importlib.metadata import version + +from opentelemetry import context as context_api +from opentelemetry.instrumentation.deepseek.config import Config +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) + +_PYDANTIC_VERSION = version("pydantic") + +TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT" + + +def set_span_attribute(span, name, value): + if value is not None and value != "": + span.set_attribute(name, value) + + +def should_send_prompts(): + return (os.getenv(TRACELOOP_TRACE_CONTENT) or "true").lower() == "true" or context_api.get_value( + "override_enable_content_tracing" + ) + + +def dont_throw(func): + """ + A decorator that wraps the passed in function and logs exceptions instead of throwing them. + + @param func: The function to wrap + @return: The wrapper function + """ + # Obtain a logger specific to the function's module + logger = logging.getLogger(func.__module__) + + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + logger.debug( + "OpenLLMetry failed to trace in %s, error: %s", + func.__name__, + traceback.format_exc(), + ) + if Config.exception_logger: + Config.exception_logger(e) + + return wrapper + + +@dont_throw +def shared_metrics_attributes(response): + response_dict = model_as_dict(response) + + common_attributes = Config.get_common_metrics_attributes() + + return { + **common_attributes, + GenAIAttributes.GEN_AI_PROVIDER_NAME: GenAIAttributes.GenAiProviderNameValues.DEEPSEEK.value, + GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_dict.get("model"), + } + + +@dont_throw +def error_metrics_attributes(exception): + return { + GenAIAttributes.GEN_AI_PROVIDER_NAME: GenAIAttributes.GenAiProviderNameValues.DEEPSEEK.value, + "error.type": exception.__class__.__name__, + } + + +def model_as_dict(model): + if _PYDANTIC_VERSION < "2.0.0": + return model.dict() + if hasattr(model, "model_dump"): + return model.model_dump() + elif hasattr(model, "parse"): # Raw API response + return model_as_dict(model.parse()) + else: + return model + + +def should_emit_events() -> bool: + """ + Checks if the instrumentation isn't using the legacy attributes + and if the event logger is not None. + """ + + return not Config.use_legacy_attributes diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/version.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/version.py new file mode 100644 index 0000000000..a076e5d90e --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/version.py @@ -0,0 +1 @@ +__version__ = "0.61.0" diff --git a/packages/opentelemetry-instrumentation-deepseek/poetry.toml b/packages/opentelemetry-instrumentation-deepseek/poetry.toml new file mode 100644 index 0000000000..ab1033bd37 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/packages/opentelemetry-instrumentation-deepseek/project.json b/packages/opentelemetry-instrumentation-deepseek/project.json new file mode 100644 index 0000000000..94d0372162 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/project.json @@ -0,0 +1,77 @@ +{ + "name": "opentelemetry-instrumentation-deepseek", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "sourceRoot": "packages/opentelemetry-instrumentation-deepseek/opentelemetry_instrumentation_deepseek", + "targets": { + "lock": { + "executor": "nx:run-commands", + "options": { + "command": "uv lock", + "cwd": "packages/opentelemetry-instrumentation-deepseek" + } + }, + "add": { + "executor": "@nxlv/python:add", + "options": {} + }, + "update": { + "executor": "@nxlv/python:update", + "options": {} + }, + "remove": { + "executor": "@nxlv/python:remove", + "options": {} + }, + "build": { + "executor": "@nxlv/python:build", + "outputs": [ + "{projectRoot}/dist" + ], + "options": { + "outputPath": "packages/opentelemetry-instrumentation-deepseek/dist", + "publish": false, + "lockedVersions": true, + "bundleLocalDependencies": true + } + }, + "install": { + "executor": "nx:run-commands", + "options": { + "command": "uv sync --all-groups", + "cwd": "packages/opentelemetry-instrumentation-deepseek" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "uv run ruff check .", + "cwd": "packages/opentelemetry-instrumentation-deepseek" + } + }, + "test": { + "executor": "nx:run-commands", + "outputs": [ + "{workspaceRoot}/reports/packages/opentelemetry-instrumentation-deepseek/unittests", + "{workspaceRoot}/coverage/packages/opentelemetry-instrumentation-deepseek" + ], + "options": { + "command": "uv run pytest tests/", + "cwd": "packages/opentelemetry-instrumentation-deepseek" + } + }, + "build-release": { + "executor": "nx:run-commands", + "options": { + "commands": [ + "chmod +x ../../scripts/build-release.sh", + "../../scripts/build-release.sh" + ], + "cwd": "packages/opentelemetry-instrumentation-deepseek" + } + } + }, + "tags": [ + "instrumentation" + ] +} diff --git a/packages/opentelemetry-instrumentation-deepseek/pyproject.toml b/packages/opentelemetry-instrumentation-deepseek/pyproject.toml new file mode 100644 index 0000000000..f4888d46c0 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/pyproject.toml @@ -0,0 +1,75 @@ +[project] +name = "opentelemetry-instrumentation-deepseek" +version = "0.61.0" +description = "OpenTelemetry DeepSeek instrumentation" +authors = [ + { name = "Gal Kleinman", email = "gal@traceloop.com" }, + { name = "Nir Gazit", email = "nir@traceloop.com" }, +] +license = "Apache-2.0" +readme = "README.md" +requires-python = ">=3.10,<4" +dependencies = [ + "opentelemetry-api>=1.38.0,<2", + "opentelemetry-instrumentation>=0.59b0", + "opentelemetry-semantic-conventions-ai>=0.5.1,<0.6.0", + "opentelemetry-semantic-conventions>=0.59b0", +] + +[project.urls] +Repository = "https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-deepseek" + +[project.optional-dependencies] +instruments = ["openai>=1.0.0"] + +[project.entry-points."opentelemetry_instrumentor"] +deepseek = "opentelemetry.instrumentation.deepseek:DeepSeekInstrumentor" + +[dependency-groups] +dev = [ + "autopep8>=2.2.0,<3", + "pytest-sugar==1.0.0", + "pytest>=8.2.2,<9", + "ruff>=0.4.0", +] +test = [ + "openai>=1.0.0", + "opentelemetry-sdk>=1.38.0,<2", + "pytest-asyncio>=0.23.7,<0.24.0", + "pytest-recording>=0.13.1,<0.14.0", + "pytest-sugar==1.0.0", + "pytest>=8.2.2,<9", + "vcrpy>=8.0.0,<9", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["opentelemetry"] + +[tool.coverage.run] +branch = true +source = ["opentelemetry/instrumentation/deepseek"] + +[tool.coverage.report] +exclude_lines = ["if TYPE_CHECKING:"] +show_missing = true + +[tool.ruff] +line-length = 120 +exclude = [ + ".git", + "__pycache__", + "build", + "dist", + ".venv", + ".pytest_cache", +] + +[tool.ruff.lint] +select = ["E", "F", "W"] + +[tool.uv] +constraint-dependencies = ["urllib3>=2.6.3", "pip>=25.3"] diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/__init__.py b/packages/opentelemetry-instrumentation-deepseek/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/data/.gitkeep b/packages/opentelemetry-instrumentation-deepseek/tests/data/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/traces/conftest.py b/packages/opentelemetry-instrumentation-deepseek/tests/traces/conftest.py new file mode 100644 index 0000000000..5a4cfbf42e --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/tests/traces/conftest.py @@ -0,0 +1,138 @@ +"""Unit tests configuration module.""" + +import os + +import pytest +from openai import AsyncOpenAI, OpenAI +from opentelemetry.instrumentation.deepseek import DeepSeekInstrumentor +from opentelemetry.instrumentation.deepseek.utils import TRACELOOP_TRACE_CONTENT +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs.export import ( + InMemoryLogExporter, + SimpleLogRecordProcessor, +) +from opentelemetry.sdk.metrics import Counter, Histogram, MeterProvider +from opentelemetry.sdk.metrics.export import ( + AggregationTemporality, + InMemoryMetricReader, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter + + +@pytest.fixture(scope="function", name="span_exporter") +def fixture_span_exporter(): + exporter = InMemorySpanExporter() + yield exporter + + +@pytest.fixture(scope="function", name="tracer_provider") +def fixture_tracer_provider(span_exporter): + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(span_exporter)) + return provider + + +@pytest.fixture(scope="function", name="log_exporter") +def fixture_log_exporter(): + exporter = InMemoryLogExporter() + yield exporter + + +@pytest.fixture(scope="function", name="logger_provider") +def fixture_logger_provider(log_exporter): + provider = LoggerProvider() + provider.add_log_record_processor(SimpleLogRecordProcessor(log_exporter)) + return provider + + +@pytest.fixture(scope="function", name="reader") +def fixture_reader(): + reader = InMemoryMetricReader({Counter: AggregationTemporality.DELTA, Histogram: AggregationTemporality.DELTA}) + return reader + + +@pytest.fixture(scope="function", name="meter_provider") +def fixture_meter_provider(reader): + resource = Resource.create() + meter_provider = MeterProvider(metric_readers=[reader], resource=resource) + + return meter_provider + + +@pytest.fixture +def deepseek_client(): + return OpenAI( + api_key=os.environ.get("DEEPSEEK_API_KEY"), + base_url="https://api.deepseek.com", + ) + + +@pytest.fixture +def async_deepseek_client(): + return AsyncOpenAI( + api_key=os.environ.get("DEEPSEEK_API_KEY"), + base_url="https://api.deepseek.com", + ) + + +@pytest.fixture(scope="function") +def instrument_legacy(reader, tracer_provider, meter_provider): + instrumentor = DeepSeekInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + + yield instrumentor + + instrumentor.uninstrument() + + +@pytest.fixture(scope="function") +def instrument_with_content(reader, tracer_provider, logger_provider, meter_provider): + os.environ.update({TRACELOOP_TRACE_CONTENT: "True"}) + + instrumentor = DeepSeekInstrumentor( + use_legacy_attributes=False, + ) + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + + yield instrumentor + + os.environ.pop(TRACELOOP_TRACE_CONTENT, None) + instrumentor.uninstrument() + + +@pytest.fixture(scope="function") +def instrument_with_no_content(reader, tracer_provider, logger_provider, meter_provider): + os.environ.update({TRACELOOP_TRACE_CONTENT: "False"}) + + instrumentor = DeepSeekInstrumentor(use_legacy_attributes=False) + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + + yield instrumentor + + os.environ.pop(TRACELOOP_TRACE_CONTENT, None) + instrumentor.uninstrument() + + +@pytest.fixture(autouse=True) +def environment(): + if not os.environ.get("DEEPSEEK_API_KEY"): + os.environ["DEEPSEEK_API_KEY"] = "api-key" + + +@pytest.fixture(scope="module") +def vcr_config(): + return {"filter_headers": ["authorization", "api-key"]} diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_event_emitter.py b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_event_emitter.py new file mode 100644 index 0000000000..edf854d66b --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_event_emitter.py @@ -0,0 +1,198 @@ +""" +Unit tests for event_emitter helpers. + +Covers: + emit_event, _emit_message_event, _emit_choice_event. +All tests are pure unit tests — no network calls, no cassettes. +""" + +import os +from unittest.mock import MagicMock, patch + +import pytest + +from opentelemetry.instrumentation.deepseek.event_emitter import ( + _emit_choice_event, + _emit_message_event, + emit_event, + emit_message_events, +) +from opentelemetry.instrumentation.deepseek.event_models import ChoiceEvent, MessageEvent +from opentelemetry.instrumentation.deepseek.utils import TRACELOOP_TRACE_CONTENT + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _logger(): + logger = MagicMock() + logger.emit = MagicMock() + return logger + + +def _emitted_body(logger): + """Return the body of the first emitted LogRecord.""" + return logger.emit.call_args[0][0].body + + +# --------------------------------------------------------------------------- +# emit_event +# --------------------------------------------------------------------------- + + +class TestEmitEvent: + def test_none_logger_returns_without_emitting(self): + # Should not raise; nothing emitted + emit_event(MessageEvent(content="hello", role="user"), event_logger=None) + + def test_events_disabled_returns_without_emitting(self): + logger = _logger() + with patch( + "opentelemetry.instrumentation.deepseek.event_emitter.should_emit_events", + return_value=False, + ): + emit_event(MessageEvent(content="hello", role="user"), event_logger=logger) + logger.emit.assert_not_called() + + def test_unsupported_event_type_raises_type_error(self): + logger = _logger() + with patch( + "opentelemetry.instrumentation.deepseek.event_emitter.should_emit_events", + return_value=True, + ): + with pytest.raises(TypeError, match="Unsupported event type"): + emit_event("not_an_event_object", event_logger=logger) + + +# --------------------------------------------------------------------------- +# emit_message_events +# --------------------------------------------------------------------------- + + +class TestEmitMessageEvents: + def test_tool_calls_forwarded_from_assistant_message(self): + """emit_message_events must pass tool_calls from kwargs to MessageEvent.""" + logger = _logger() + tool_calls = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{}"}}] + kwargs = { + "messages": [{"role": "assistant", "content": None, "tool_calls": tool_calls}] + } + with patch( + "opentelemetry.instrumentation.deepseek.event_emitter.should_emit_events", + return_value=True, + ): + emit_message_events(kwargs, logger) + body = logger.emit.call_args[0][0].body + assert "tool_calls" in body + assert body["tool_calls"][0]["id"] == "c1" + + def test_messages_without_tool_calls_do_not_include_tool_calls_key(self): + logger = _logger() + kwargs = {"messages": [{"role": "user", "content": "Hello"}]} + with patch( + "opentelemetry.instrumentation.deepseek.event_emitter.should_emit_events", + return_value=True, + ): + emit_message_events(kwargs, logger) + body = logger.emit.call_args[0][0].body + assert "tool_calls" not in body + + +# --------------------------------------------------------------------------- +# _emit_message_event +# --------------------------------------------------------------------------- + + +class TestEmitMessageEvent: + def test_invalid_role_uses_fallback_event_name(self): + logger = _logger() + event = MessageEvent(content="hi", role="some_unknown_role") + _emit_message_event(event, logger) + log_record = logger.emit.call_args[0][0] + assert log_record.event_name == "gen_ai.user.message" + + def test_non_assistant_role_with_tool_calls_removes_tool_calls(self): + logger = _logger() + tool_calls = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{}"}}] + event = MessageEvent(content="some", role="user", tool_calls=tool_calls) + _emit_message_event(event, logger) + body = _emitted_body(logger) + assert "tool_calls" not in body + + def test_assistant_role_with_tool_calls_keeps_tool_calls(self): + logger = _logger() + tool_calls = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{}"}}] + event = MessageEvent(content="", role="assistant", tool_calls=tool_calls) + _emit_message_event(event, logger) + body = _emitted_body(logger) + assert "tool_calls" in body + + def test_no_prompts_removes_content_and_tool_call_arguments(self): + logger = _logger() + tool_calls = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": '{"x": 1}'}}] + event = MessageEvent(content="some text", role="assistant", tool_calls=tool_calls) + with patch.dict(os.environ, {TRACELOOP_TRACE_CONTENT: "False"}): + _emit_message_event(event, logger) + body = _emitted_body(logger) + assert "content" not in body + assert "arguments" not in body["tool_calls"][0]["function"] + + +# --------------------------------------------------------------------------- +# _emit_choice_event +# --------------------------------------------------------------------------- + + +class TestEmitChoiceEvent: + def test_non_assistant_role_keeps_role_in_message(self): + logger = _logger() + event = ChoiceEvent( + index=0, + message={"role": "system", "content": "oops"}, + finish_reason="stop", + ) + _emit_choice_event(event, logger) + body = _emitted_body(logger) + assert body["message"].get("role") == "system" + + def test_not_none_tool_calls_kept_in_body(self): + logger = _logger() + tool_calls = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": "{}"}}] + event = ChoiceEvent( + index=0, + message={"role": "assistant", "content": None}, + finish_reason="tool_call", + tool_calls=tool_calls, + ) + _emit_choice_event(event, logger) + body = _emitted_body(logger) + assert "tool_calls" in body + + def test_no_prompts_removes_content_and_role_from_message(self): + logger = _logger() + event = ChoiceEvent( + index=0, + message={"role": "assistant", "content": "hello"}, + finish_reason="stop", + ) + with patch.dict(os.environ, {TRACELOOP_TRACE_CONTENT: "False"}): + _emit_choice_event(event, logger) + body = _emitted_body(logger) + assert "content" not in body["message"] + assert "role" not in body["message"] + + def test_no_prompts_removes_tool_call_arguments(self): + logger = _logger() + tool_calls = [{"id": "c1", "type": "function", "function": {"name": "f", "arguments": '{"x": 1}'}}] + event = ChoiceEvent( + index=0, + message={"role": "assistant", "content": None}, + finish_reason="tool_call", + tool_calls=tool_calls, + ) + with patch.dict(os.environ, {TRACELOOP_TRACE_CONTENT: "False"}): + _emit_choice_event(event, logger) + body = _emitted_body(logger) + assert "arguments" not in body["tool_calls"][0]["function"] diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_finish_reasons.py b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_finish_reasons.py new file mode 100644 index 0000000000..5ce836ea75 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_finish_reasons.py @@ -0,0 +1,84 @@ +""" +Unit tests for DeepSeek finish reason mapping and collection. + +These tests define the expected behavior for: +- _map_deepseek_finish_reason() — maps DeepSeek's (OpenAI-compatible) finish_reason to OTel string +- _collect_finish_reasons_from_response() — extracts finish reasons from all choices + +All tests here are pure unit tests (no cassettes, no network calls). +""" + +from unittest.mock import MagicMock + +from opentelemetry.instrumentation.deepseek.span_utils import ( + _collect_finish_reasons_from_response, + _map_deepseek_finish_reason, +) + + +# --------------------------------------------------------------------------- +# _map_deepseek_finish_reason +# --------------------------------------------------------------------------- + + +class TestMapDeepSeekFinishReason: + """DeepSeek's API is OpenAI-compatible and returns finish_reason as a plain string.""" + + def test_tool_calls(self): + # DeepSeek returns "tool_calls" (plural, OpenAI-compatible) → OTel expects "tool_call" (singular) + assert _map_deepseek_finish_reason("tool_calls") == "tool_call" + + def test_none_returns_empty_string(self): + assert _map_deepseek_finish_reason(None) == "" + + def test_empty_string_returns_empty_string(self): + assert _map_deepseek_finish_reason("") == "" + + def test_unknown_value_is_preserved(self): + assert _map_deepseek_finish_reason("something_unexpected") == "something_unexpected" + + +# --------------------------------------------------------------------------- +# _collect_finish_reasons_from_response +# --------------------------------------------------------------------------- + + +def _make_response(finish_reasons: list): + """Build a mock OpenAI-compatible ChatCompletion with given finish_reasons per choice.""" + response = MagicMock() + choices = [] + for fr in finish_reasons: + choice = MagicMock() + choice.finish_reason = fr + choices.append(choice) + response.choices = choices + return response + + +class TestCollectFinishReasonsFromResponse: + def test_single_stop(self): + response = _make_response(["stop"]) + assert _collect_finish_reasons_from_response(response) == ["stop"] + + def test_single_length(self): + response = _make_response(["length"]) + assert _collect_finish_reasons_from_response(response) == ["length"] + + def test_multiple_choices(self): + response = _make_response(["stop", "length"]) + assert _collect_finish_reasons_from_response(response) == ["stop", "length"] + + def test_none_finish_reason_maps_to_empty(self): + response = _make_response([None]) + assert _collect_finish_reasons_from_response(response) == [""] + + def test_mixed_known_and_none(self): + response = _make_response(["stop", None]) + assert _collect_finish_reasons_from_response(response) == ["stop", ""] + + def test_empty_choices_returns_empty_list(self): + response = _make_response([]) + assert _collect_finish_reasons_from_response(response) == [] + + def test_none_response_returns_empty_list(self): + assert _collect_finish_reasons_from_response(None) == [] diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_init.py b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_init.py new file mode 100644 index 0000000000..11b2722c73 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_init.py @@ -0,0 +1,644 @@ +""" +Unit tests for __init__.py helpers and instrumentation paths. + +Covers: + _is_deepseek_client, _process_streaming_chunk, _create_stream_processor, + _create_async_stream_processor, _wrap, _awrap, + DeepSeekInstrumentor._instrument edge cases. +No cassettes needed — all paths tested with mocks. +""" + +import os +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from opentelemetry import context as context_api +from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY +from opentelemetry.semconv_ai import SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY + +from opentelemetry.instrumentation.deepseek import ( + DeepSeekInstrumentor, + _accumulate_tool_calls, + _awrap, + _create_async_stream_processor, + _create_stream_processor, + _is_deepseek_client, + _process_streaming_chunk, + _wrap, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _span(recording: bool = True) -> MagicMock: + s = MagicMock() + s.is_recording.return_value = recording + return s + + +def _deepseek_instance() -> MagicMock: + """A mock OpenAI SDK client instance pointed at the DeepSeek API.""" + instance = MagicMock() + instance._client.base_url = "https://api.deepseek.com/v1/" + return instance + + +def _openai_instance() -> MagicMock: + """A mock OpenAI SDK client instance pointed at the regular OpenAI API.""" + instance = MagicMock() + instance._client.base_url = "https://api.openai.com/v1/" + return instance + + +def _make_sync_wrapper( + *, + tracer=None, + token_histogram=None, + choice_counter=None, + duration_histogram=None, + event_logger=None, +): + tracer = tracer or MagicMock() + span = _span() + tracer.start_span.return_value = span + return ( + span, + _wrap(tracer, token_histogram, choice_counter, duration_histogram, event_logger, {}), + ) + + +def _make_async_wrapper( + *, + tracer=None, + token_histogram=None, + choice_counter=None, + duration_histogram=None, + event_logger=None, +): + tracer = tracer or MagicMock() + span = _span() + tracer.start_span.return_value = span + return ( + span, + _awrap(tracer, token_histogram, choice_counter, duration_histogram, event_logger, {}), + ) + + +# --------------------------------------------------------------------------- +# _is_deepseek_client +# --------------------------------------------------------------------------- + + +class TestIsDeepSeekClient: + def test_deepseek_base_url_returns_true(self): + assert _is_deepseek_client(_deepseek_instance()) is True + + def test_openai_base_url_returns_false(self): + assert _is_deepseek_client(_openai_instance()) is False + + def test_none_instance_returns_false(self): + assert _is_deepseek_client(None) is False + + +# --------------------------------------------------------------------------- +# _process_streaming_chunk +# --------------------------------------------------------------------------- + + +class TestProcessStreamingChunk: + def test_empty_choices_returns_none_quintuple(self): + chunk = MagicMock() + chunk.choices = [] + assert _process_streaming_chunk(chunk) == (None, "", [], [], None) + + def test_multiple_choices_accumulates_content(self): + chunk = MagicMock() + chunk.usage = None + choice0 = MagicMock() + choice0.delta.content = "Hello" + choice0.delta.reasoning_content = None + choice0.delta.tool_calls = None + choice0.finish_reason = None + choice1 = MagicMock() + choice1.delta.content = " World" + choice1.delta.reasoning_content = None + choice1.delta.tool_calls = None + choice1.finish_reason = "stop" + chunk.choices = [choice0, choice1] + content, reasoning_content, tool_calls_delta, finish_reasons, usage = _process_streaming_chunk(chunk) + assert content == "Hello World" + assert reasoning_content == "" + assert tool_calls_delta == [] + assert finish_reasons == ["stop"] + assert usage is None + + def test_reasoning_content_accumulated_for_deepseek_reasoner(self): + """DeepSeek-R1 (deepseek-reasoner) streams chain-of-thought via delta.reasoning_content.""" + chunk = MagicMock() + chunk.usage = None + choice0 = MagicMock() + choice0.delta.content = None + choice0.delta.reasoning_content = "Let me " + choice0.delta.tool_calls = None + choice0.finish_reason = None + choice1 = MagicMock() + choice1.delta.content = "42" + choice1.delta.reasoning_content = "think..." + choice1.delta.tool_calls = None + choice1.finish_reason = "stop" + chunk.choices = [choice0, choice1] + content, reasoning_content, tool_calls_delta, finish_reasons, usage = _process_streaming_chunk(chunk) + assert content == "42" + assert reasoning_content == "Let me think..." + assert finish_reasons == ["stop"] + + def test_tool_calls_delta_extracted(self): + chunk = MagicMock() + chunk.usage = None + tc = MagicMock() + tc.index = 0 + tc.id = "call_123" + tc.function.name = "get_weather" + tc.function.arguments = '{"loc' + choice = MagicMock() + choice.delta.content = None + choice.delta.reasoning_content = None + choice.delta.tool_calls = [tc] + choice.finish_reason = None + chunk.choices = [choice] + content, reasoning_content, tool_calls_delta, finish_reasons, usage = _process_streaming_chunk(chunk) + assert content == "" + assert reasoning_content == "" + assert len(tool_calls_delta) == 1 + assert tool_calls_delta[0].id == "call_123" + + def test_usage_extracted_from_final_chunk(self): + chunk = MagicMock() + usage = MagicMock() + chunk.usage = usage + choice = MagicMock() + choice.delta.content = "done" + choice.delta.reasoning_content = None + choice.delta.tool_calls = None + choice.finish_reason = "stop" + chunk.choices = [choice] + _, _, _, _, extracted_usage = _process_streaming_chunk(chunk) + assert extracted_usage is usage + + def test_no_usage_on_intermediate_chunk(self): + chunk = MagicMock() + chunk.usage = None + choice = MagicMock() + choice.delta.content = "partial" + choice.delta.reasoning_content = None + choice.delta.tool_calls = None + choice.finish_reason = None + chunk.choices = [choice] + _, _, _, _, usage = _process_streaming_chunk(chunk) + assert usage is None + + +# --------------------------------------------------------------------------- +# _accumulate_tool_calls +# --------------------------------------------------------------------------- + + +class TestAccumulateToolCalls: + def _make_delta(self, index, tc_id=None, name=None, arguments=""): + tc = MagicMock() + tc.index = index + tc.id = tc_id + fn = MagicMock() + fn.name = name + fn.arguments = arguments + tc.function = fn + return tc + + def test_single_chunk_creates_entry(self): + acc = {} + tc = self._make_delta(0, tc_id="call_1", name="ping", arguments='{"x"') + _accumulate_tool_calls(acc, [tc]) + assert acc[0]["id"] == "call_1" + assert acc[0]["function"]["name"] == "ping" + assert acc[0]["function"]["arguments"] == '{"x"' + + def test_fragments_are_concatenated(self): + acc = {} + _accumulate_tool_calls(acc, [self._make_delta(0, tc_id="call_1", name="fn", arguments='{"a"')]) + _accumulate_tool_calls(acc, [self._make_delta(0, arguments=': 1}')]) + assert acc[0]["function"]["arguments"] == '{"a": 1}' + + def test_multiple_tool_calls_tracked_by_index(self): + acc = {} + _accumulate_tool_calls(acc, [ + self._make_delta(0, tc_id="c0", name="fn0", arguments=""), + self._make_delta(1, tc_id="c1", name="fn1", arguments=""), + ]) + assert 0 in acc and 1 in acc + assert acc[0]["id"] == "c0" + assert acc[1]["id"] == "c1" + + +# --------------------------------------------------------------------------- +# _create_stream_processor (sync) +# --------------------------------------------------------------------------- + + +class TestCreateStreamProcessor: + def test_span_not_recording_skips_set_status(self): + span = _span(recording=False) + chunk = MagicMock() + chunk.choices = [] # _process_streaming_chunk returns (None, "", [], [], None) + + # Consume the generator to trigger cleanup + list(_create_stream_processor(iter([chunk]), span, None)) + + span.set_status.assert_not_called() + span.end.assert_called_once() + + def test_accumulated_reasoning_passed_to_streaming_attributes(self): + """Reasoning content is accumulated across chunks and forwarded for the final span attribute.""" + span = _span() + + chunk1 = MagicMock() + chunk1.usage = None + chunk1.choices = [MagicMock()] + chunk1.choices[0].delta.content = None + chunk1.choices[0].delta.reasoning_content = "Thinking " + chunk1.choices[0].delta.tool_calls = None + chunk1.choices[0].finish_reason = None + + chunk2 = MagicMock() + chunk2.usage = None + chunk2.choices = [MagicMock()] + chunk2.choices[0].delta.content = "Answer" + chunk2.choices[0].delta.reasoning_content = "more..." + chunk2.choices[0].delta.tool_calls = None + chunk2.choices[0].finish_reason = "stop" + + with patch("opentelemetry.instrumentation.deepseek.set_streaming_response_attributes") as mock_set: + list(_create_stream_processor(iter([chunk1, chunk2]), span, None)) + + mock_set.assert_called_once() + _, kwargs = mock_set.call_args + assert kwargs.get("accumulated_reasoning") == "Thinking more..." + + +# --------------------------------------------------------------------------- +# _create_async_stream_processor +# --------------------------------------------------------------------------- + + +class TestCreateAsyncStreamProcessor: + @pytest.mark.asyncio + async def test_processes_chunks_and_ends_span(self): + span = _span() + + chunk = MagicMock() + chunk.usage = None + chunk.choices = [MagicMock()] + chunk.choices[0].delta.content = "hi" + chunk.choices[0].delta.reasoning_content = None + chunk.choices[0].delta.tool_calls = None + chunk.choices[0].finish_reason = "stop" + + async def _response(): + yield chunk + + chunks = [c async for c in _create_async_stream_processor(_response(), span, None)] + assert len(chunks) == 1 + span.end.assert_called_once() + + @pytest.mark.asyncio + async def test_span_not_recording_skips_set_status(self): + span = _span(recording=False) + + chunk = MagicMock() + chunk.choices = [] + + async def _response(): + yield chunk + + [c async for c in _create_async_stream_processor(_response(), span, None)] + span.set_status.assert_not_called() + span.end.assert_called_once() + + +# --------------------------------------------------------------------------- +# _wrap (sync) +# --------------------------------------------------------------------------- + + +class TestWrap: + def test_suppression_key_skips_span(self): + tracer = MagicMock() + wrapped = MagicMock(return_value="result") + wrapper = _wrap(tracer, None, None, None, None, {}) + + token = context_api.attach(context_api.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) + try: + result = wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + finally: + context_api.detach(token) + + assert result == "result" + tracer.start_span.assert_not_called() + + def test_non_deepseek_client_passes_through_without_span(self): + """Calls made through a regular OpenAI client must not be instrumented.""" + tracer = MagicMock() + wrapped = MagicMock(return_value="result") + wrapper = _wrap(tracer, None, None, None, None, {}) + + result = wrapper(wrapped, _openai_instance(), [], {"model": "gpt-4"}) + + assert result == "result" + tracer.start_span.assert_not_called() + wrapped.assert_called_once() + + def test_none_instance_passes_through_without_span(self): + tracer = MagicMock() + wrapped = MagicMock(return_value="result") + wrapper = _wrap(tracer, None, None, None, None, {}) + + result = wrapper(wrapped, None, [], {"model": "m"}) + + assert result == "result" + tracer.start_span.assert_not_called() + + def test_api_exception_records_duration_and_reraises(self): + span, wrapper = _make_sync_wrapper(duration_histogram=MagicMock()) + error = ValueError("API down") + wrapped = MagicMock(side_effect=error) + + with pytest.raises(ValueError, match="API down"): + wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + span.end.assert_called_once() # span must be ended even on exception + span.set_status.assert_called_once() + + def test_api_exception_records_duration_histogram(self): + tracer = MagicMock() + span = _span() + tracer.start_span.return_value = span + duration_histogram = MagicMock() + wrapped = MagicMock(side_effect=RuntimeError("fail")) + + wrapper = _wrap(tracer, None, None, duration_histogram, None, {}) + with pytest.raises(RuntimeError): + wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + duration_histogram.record.assert_called_once() + + def test_falsy_response_ends_span_without_setting_status(self): + span, wrapper = _make_sync_wrapper() + wrapped = MagicMock(return_value=None) + + result = wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + assert result is None + span.end.assert_called_once() + span.set_status.assert_not_called() + + def test_handle_response_exception_is_swallowed(self): + span, wrapper = _make_sync_wrapper() + response = MagicMock() + wrapped = MagicMock(return_value=response) + + with patch("opentelemetry.instrumentation.deepseek._handle_response", side_effect=Exception("oops")): + with patch("opentelemetry.instrumentation.deepseek.shared_metrics_attributes", return_value={}): + result = wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + assert result is response + span.end.assert_called_once() + + def test_no_duration_histogram_skips_duration_record(self): + span, wrapper = _make_sync_wrapper(duration_histogram=None) + response = MagicMock() + wrapped = MagicMock(return_value=response) + + with patch("opentelemetry.instrumentation.deepseek._handle_response"): + with patch("opentelemetry.instrumentation.deepseek.shared_metrics_attributes", return_value={}): + result = wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + assert result is response + span.end.assert_called_once() + + def test_stream_processor_exception_sets_error_status(self): + from openai._streaming import Stream + + tracer = MagicMock() + span = _span() + tracer.start_span.return_value = span + response = MagicMock(spec=Stream) + wrapped = MagicMock(return_value=response) + + wrapper = _wrap(tracer, None, None, None, None, {}) + with patch( + "opentelemetry.instrumentation.deepseek._create_stream_processor", + side_effect=RuntimeError("stream error"), + ): + with pytest.raises(RuntimeError, match="stream error"): + wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + span.end.assert_called_once() + + def test_span_not_recording_after_response_skips_set_status(self): + """Covers span.is_recording() False at the final OK status check.""" + tracer = MagicMock() + span = MagicMock() + # Calls: set_model_input_attributes, set_input_attributes, final check + span.is_recording.side_effect = [True, True, False] + tracer.start_span.return_value = span + + response = MagicMock() + wrapped = MagicMock(return_value=response) + + with patch("opentelemetry.instrumentation.deepseek._handle_response"): + with patch("opentelemetry.instrumentation.deepseek.shared_metrics_attributes", return_value={}): + wrapper = _wrap(tracer, None, None, None, None, {}) + wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + span.set_status.assert_not_called() + span.end.assert_called_once() + + +# --------------------------------------------------------------------------- +# _awrap (async) +# --------------------------------------------------------------------------- + + +class TestAwrap: + @pytest.mark.asyncio + async def test_suppression_key_skips_span(self): + tracer = MagicMock() + wrapped = AsyncMock(return_value="async_result") + wrapper = _awrap(tracer, None, None, None, None, {}) + + token = context_api.attach(context_api.set_value(SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY, True)) + try: + result = await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + finally: + context_api.detach(token) + + assert result == "async_result" + tracer.start_span.assert_not_called() + + @pytest.mark.asyncio + async def test_otel_suppression_key_skips_span(self): + tracer = MagicMock() + wrapped = AsyncMock(return_value="async_result") + wrapper = _awrap(tracer, None, None, None, None, {}) + + token = context_api.attach(context_api.set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) + try: + result = await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + finally: + context_api.detach(token) + + assert result == "async_result" + tracer.start_span.assert_not_called() + + @pytest.mark.asyncio + async def test_non_deepseek_client_passes_through_without_span(self): + """Calls made through a regular OpenAI client must not be instrumented.""" + tracer = MagicMock() + wrapped = AsyncMock(return_value="async_result") + wrapper = _awrap(tracer, None, None, None, None, {}) + + result = await wrapper(wrapped, _openai_instance(), [], {"model": "gpt-4"}) + + assert result == "async_result" + tracer.start_span.assert_not_called() + wrapped.assert_awaited_once() + + @pytest.mark.asyncio + async def test_api_exception_records_duration_and_reraises(self): + tracer = MagicMock() + span = _span() + tracer.start_span.return_value = span + duration_histogram = MagicMock() + + wrapped = AsyncMock(side_effect=RuntimeError("async fail")) + wrapper = _awrap(tracer, None, None, duration_histogram, None, {}) + + with pytest.raises(RuntimeError, match="async fail"): + await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + duration_histogram.record.assert_called_once() + + @pytest.mark.asyncio + async def test_falsy_response_ends_span(self): + span, wrapper = _make_async_wrapper() + wrapped = AsyncMock(return_value=None) + + result = await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + assert result is None + span.end.assert_called_once() + span.set_status.assert_not_called() + + @pytest.mark.asyncio + async def test_response_with_duration_histogram(self): + tracer = MagicMock() + span = _span() + tracer.start_span.return_value = span + duration_histogram = MagicMock() + + response = MagicMock() + wrapped = AsyncMock(return_value=response) + + wrapper = _awrap(tracer, None, None, duration_histogram, None, {}) + with patch("opentelemetry.instrumentation.deepseek._handle_response"): + with patch("opentelemetry.instrumentation.deepseek.shared_metrics_attributes", return_value={}): + result = await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + assert result is response + duration_histogram.record.assert_called_once() + span.end.assert_called_once() + + @pytest.mark.asyncio + async def test_response_without_duration_histogram(self): + span, wrapper = _make_async_wrapper(duration_histogram=None) + response = MagicMock() + wrapped = AsyncMock(return_value=response) + + with patch("opentelemetry.instrumentation.deepseek._handle_response"): + with patch("opentelemetry.instrumentation.deepseek.shared_metrics_attributes", return_value={}): + result = await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + assert result is response + span.end.assert_called_once() + + @pytest.mark.asyncio + async def test_async_streaming_exception_sets_error_status(self): + """Covers the async streaming path raising and setting ERROR status.""" + from openai._streaming import AsyncStream + + tracer = MagicMock() + span = _span() + tracer.start_span.return_value = span + response = MagicMock(spec=AsyncStream) + wrapped = AsyncMock(return_value=response) + + wrapper = _awrap(tracer, None, None, None, None, {}) + with patch( + "opentelemetry.instrumentation.deepseek._create_async_stream_processor", + side_effect=RuntimeError("async stream error"), + ): + with pytest.raises(RuntimeError, match="async stream error"): + await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + span.end.assert_called_once() + + @pytest.mark.asyncio + async def test_span_not_recording_after_response_skips_set_status(self): + """Covers span.is_recording() False at the final OK status check.""" + tracer = MagicMock() + span = MagicMock() + span.is_recording.side_effect = [True, True, False] + tracer.start_span.return_value = span + + response = MagicMock() + wrapped = AsyncMock(return_value=response) + + with patch("opentelemetry.instrumentation.deepseek._handle_response"): + with patch("opentelemetry.instrumentation.deepseek.shared_metrics_attributes", return_value={}): + wrapper = _awrap(tracer, None, None, None, None, {}) + await wrapper(wrapped, _deepseek_instance(), [], {"model": "m"}) + + span.set_status.assert_not_called() + span.end.assert_called_once() + + +# --------------------------------------------------------------------------- +# DeepSeekInstrumentor._instrument edge cases +# --------------------------------------------------------------------------- + + +class TestDeepSeekInstrumentor: + def test_metrics_disabled_sets_histograms_to_none(self, tracer_provider, meter_provider): + with patch.dict(os.environ, {"TRACELOOP_METRICS_ENABLED": "false"}): + instrumentor = DeepSeekInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() + + def test_module_not_found_for_sync_wrap_is_swallowed(self, tracer_provider, meter_provider): + instrumentor = DeepSeekInstrumentor() + with patch( + "opentelemetry.instrumentation.deepseek.wrap_function_wrapper", + side_effect=ModuleNotFoundError("openai not installed"), + ): + # Should not raise + instrumentor.instrument( + tracer_provider=tracer_provider, + meter_provider=meter_provider, + ) + instrumentor.uninstrument() diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_span_utils.py b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_span_utils.py new file mode 100644 index 0000000000..556aa14743 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_span_utils.py @@ -0,0 +1,652 @@ +""" +Unit tests for span_utils helpers. + +Covers: + _content_to_parts, _tool_calls_to_parts, and every set_* span-attribute function, + including the DeepSeek-R1 `reasoning_content` -> gen_ai.deepseek.reasoning_content attribute. +All tests use mock spans — no network calls, no cassettes. +""" + +import json +from unittest.mock import MagicMock + +import pytest + +from opentelemetry.semconv._incubating.attributes import gen_ai_attributes as GenAIAttributes + +from opentelemetry.instrumentation.deepseek.span_utils import ( + GEN_AI_DEEPSEEK_REASONING_CONTENT, + _content_to_parts, + _tool_calls_to_parts, + set_input_attributes, + set_model_input_attributes, + set_model_response_attributes, + set_model_streaming_response_attributes, + set_response_attributes, + set_streaming_response_attributes, +) +from opentelemetry.instrumentation.deepseek.utils import TRACELOOP_TRACE_CONTENT + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _span(recording: bool = True) -> MagicMock: + s = MagicMock() + s.is_recording.return_value = recording + return s + + +def _attr(span: MagicMock, name: str): + """Return the value passed to span.set_attribute(name, …).""" + for call in span.set_attribute.call_args_list: + if call[0][0] == name: + return call[0][1] + return None + + +# --------------------------------------------------------------------------- +# _content_to_parts +# --------------------------------------------------------------------------- + + +class TestContentToParts: + def test_none_returns_empty(self): + assert _content_to_parts(None) == [] + + def test_empty_string_returns_empty(self): + assert _content_to_parts("") == [] + + def test_plain_string_returns_text_part(self): + assert _content_to_parts("hello") == [{"type": "text", "content": "hello"}] + + def test_list_with_text_block(self): + content = [{"type": "text", "text": "hello world"}] + assert _content_to_parts(content) == [{"type": "text", "content": "hello world"}] + + def test_list_with_image_url_block(self): + content = [{"type": "image_url", "image_url": {"url": "https://example.com/img.jpg"}}] + assert _content_to_parts(content) == [ + {"type": "uri", "modality": "image", "uri": "https://example.com/img.jpg"} + ] + + def test_list_with_mixed_content(self): + content = [ + {"type": "text", "text": "Describe:"}, + {"type": "image_url", "image_url": {"url": "https://example.com/cat.jpg"}}, + ] + result = _content_to_parts(content) + assert len(result) == 2 + assert result[0] == {"type": "text", "content": "Describe:"} + assert result[1] == {"type": "uri", "modality": "image", "uri": "https://example.com/cat.jpg"} + + def test_list_with_non_dict_items_skipped(self): + content = ["not a dict", {"type": "text", "text": "hello"}] + assert _content_to_parts(content) == [{"type": "text", "content": "hello"}] + + def test_list_with_unknown_block_type_preserved_as_generic(self): + content = [{"type": "audio", "data": "..."}] + assert _content_to_parts(content) == [{"type": "audio", "data": "..."}] + + def test_list_with_data_url_image(self): + content = [{"type": "image_url", "image_url": {"url": "data:image/png;base64,ABC123"}}] + assert _content_to_parts(content) == [ + {"type": "blob", "modality": "image", "mime_type": "image/png", "content": "ABC123"} + ] + + +# --------------------------------------------------------------------------- +# _tool_calls_to_parts +# --------------------------------------------------------------------------- + + +class TestToolCallsToParts: + def test_none_returns_empty(self): + assert _tool_calls_to_parts(None) == [] + + def test_empty_list_returns_empty(self): + assert _tool_calls_to_parts([]) == [] + + def test_string_arguments_parsed_as_json(self): + tool_calls = [ + { + "id": "call_123", + "type": "function", + "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, + } + ] + result = _tool_calls_to_parts(tool_calls) + assert result == [ + { + "type": "tool_call", + "id": "call_123", + "name": "get_weather", + "arguments": {"location": "SF"}, + } + ] + + def test_dict_arguments_used_as_is(self): + tool_calls = [ + { + "id": "call_456", + "type": "function", + "function": {"name": "add", "arguments": {"a": 1, "b": 2}}, + } + ] + result = _tool_calls_to_parts(tool_calls) + assert result[0]["arguments"] == {"a": 1, "b": 2} + + def test_invalid_json_string_returns_raw(self): + tool_calls = [ + { + "id": "call_789", + "type": "function", + "function": {"name": "foo", "arguments": "not valid json {{"}, + } + ] + result = _tool_calls_to_parts(tool_calls) + assert result[0]["arguments"] == "not valid json {{" + + def test_no_arguments_omits_arguments_key(self): + tool_calls = [ + { + "id": "call_000", + "type": "function", + "function": {"name": "ping"}, + } + ] + result = _tool_calls_to_parts(tool_calls) + assert "arguments" not in result[0] + + def test_non_dict_items_skipped(self): + tool_calls = [ + "not a dict", + {"id": "call_ok", "type": "function", "function": {"name": "foo", "arguments": "{}"}}, + ] + result = _tool_calls_to_parts(tool_calls) + assert len(result) == 1 + assert result[0]["name"] == "foo" + + def test_pydantic_style_object_handled(self): + """The OpenAI SDK may return tool_calls as Pydantic objects, not dicts.""" + fn = MagicMock() + fn.name = "get_weather" + fn.arguments = '{"city": "Paris"}' + + tc = MagicMock() + tc.id = "call_pydantic" + tc.function = fn + + result = _tool_calls_to_parts([tc]) + assert len(result) == 1 + assert result[0]["id"] == "call_pydantic" + assert result[0]["name"] == "get_weather" + assert result[0]["arguments"] == {"city": "Paris"} + + def test_multiple_tool_calls(self): + tool_calls = [ + {"id": "call_1", "type": "function", "function": {"name": "func1", "arguments": '{"x": 1}'}}, + {"id": "call_2", "type": "function", "function": {"name": "func2", "arguments": '{"y": 2}'}}, + ] + result = _tool_calls_to_parts(tool_calls) + assert len(result) == 2 + assert result[0]["name"] == "func1" + assert result[1]["name"] == "func2" + + +# --------------------------------------------------------------------------- +# set_input_attributes +# --------------------------------------------------------------------------- + + +class TestSetInputAttributes: + def test_non_recording_span_returns_early(self): + span = _span(recording=False) + set_input_attributes(span, {"messages": [{"role": "user", "content": "hello"}]}) + span.set_attribute.assert_not_called() + + def test_empty_messages_does_not_set_attribute(self): + span = _span() + set_input_attributes(span, {"messages": []}) + span.set_attribute.assert_not_called() + + def test_tool_role_creates_tool_call_response_part(self): + span = _span() + kwargs = { + "messages": [ + { + "role": "tool", + "tool_call_id": "call_123", + "content": "20°C in SF", + } + ] + } + set_input_attributes(span, kwargs) + value = _attr(span, GenAIAttributes.GEN_AI_INPUT_MESSAGES) + assert value is not None + messages = json.loads(value) + assert messages[0]["role"] == "tool" + assert messages[0]["parts"] == [ + { + "type": "tool_call_response", + "id": "call_123", + "response": "20°C in SF", + } + ] + + def test_message_with_tool_calls_adds_tool_call_parts(self): + span = _span() + kwargs = { + "messages": [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_456", + "type": "function", + "function": {"name": "get_weather", "arguments": '{"location": "Paris"}'}, + } + ], + } + ] + } + set_input_attributes(span, kwargs) + value = _attr(span, GenAIAttributes.GEN_AI_INPUT_MESSAGES) + assert value is not None + messages = json.loads(value) + parts = messages[0]["parts"] + assert any(p["type"] == "tool_call" and p["name"] == "get_weather" for p in parts) + + +# --------------------------------------------------------------------------- +# set_model_input_attributes +# --------------------------------------------------------------------------- + + +class TestSetModelInputAttributes: + def test_non_recording_span_returns_early(self): + span = _span(recording=False) + set_model_input_attributes(span, {"model": "deepseek-chat"}) + span.set_attribute.assert_not_called() + + def test_with_tools_sets_tool_definitions(self): + span = _span() + tools = [{"type": "function", "function": {"name": "ping", "description": "Ping"}}] + set_model_input_attributes(span, {"model": "deepseek-chat", "tools": tools}) + value = _attr(span, GenAIAttributes.GEN_AI_TOOL_DEFINITIONS) + assert value is not None + assert json.loads(value) == tools + + def test_unserializable_tools_silently_ignored(self): + span = _span() + # object() is not JSON-serializable — should not raise + set_model_input_attributes(span, {"model": "deepseek-chat", "tools": [object()]}) + # GEN_AI_TOOL_DEFINITIONS must NOT be set (exception swallowed) + assert _attr(span, GenAIAttributes.GEN_AI_TOOL_DEFINITIONS) is None + + def test_tools_not_set_when_send_prompts_disabled(self): + """Tool definitions are Opt-In — must NOT be recorded when content tracing is off.""" + span = _span() + tools = [{"type": "function", "function": {"name": "ping"}}] + with pytest.MonkeyPatch().context() as mp: + mp.setenv(TRACELOOP_TRACE_CONTENT, "False") + set_model_input_attributes(span, {"model": "deepseek-chat", "tools": tools}) + assert _attr(span, GenAIAttributes.GEN_AI_TOOL_DEFINITIONS) is None + + +# --------------------------------------------------------------------------- +# set_streaming_response_attributes +# --------------------------------------------------------------------------- + + +class TestSetStreamingResponseAttributes: + def test_non_recording_span_returns_early(self): + span = _span(recording=False) + set_streaming_response_attributes(span, "some content") + span.set_attribute.assert_not_called() + + def test_empty_content_produces_empty_parts(self): + span = _span() + set_streaming_response_attributes(span, "", finish_reason="stop") + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + assert messages[0]["parts"] == [] + assert messages[0]["finish_reason"] == "stop" + + def test_with_content_produces_text_part(self): + span = _span() + set_streaming_response_attributes(span, "Hello!", finish_reason="stop") + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + assert messages[0]["parts"] == [{"type": "text", "content": "Hello!"}] + assert messages[0]["role"] == "assistant" + + def test_with_tool_calls_adds_tool_call_parts(self): + span = _span() + tool_calls = [{"id": "call_1", "function": {"name": "get_weather", "arguments": '{"city": "Paris"}'}}] + set_streaming_response_attributes(span, "", finish_reason="tool_calls", tool_calls=tool_calls) + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + parts = messages[0]["parts"] + assert any(p["type"] == "tool_call" and p["name"] == "get_weather" for p in parts) + assert messages[0]["finish_reason"] == "tool_call" + + def test_accumulated_reasoning_sets_reasoning_content_attribute(self): + """DeepSeek-R1 streaming: accumulated reasoning_content -> gen_ai.deepseek.reasoning_content.""" + span = _span() + set_streaming_response_attributes( + span, + "The answer is 42.", + finish_reason="stop", + accumulated_reasoning="Let me think step by step...", + ) + assert _attr(span, GEN_AI_DEEPSEEK_REASONING_CONTENT) == "Let me think step by step..." + + def test_no_reasoning_does_not_set_reasoning_content_attribute(self): + span = _span() + set_streaming_response_attributes(span, "Hi there!", finish_reason="stop") + attr_names = {c[0][0] for c in span.set_attribute.call_args_list} + assert GEN_AI_DEEPSEEK_REASONING_CONTENT not in attr_names + + +# --------------------------------------------------------------------------- +# set_model_streaming_response_attributes +# --------------------------------------------------------------------------- + + +class TestSetModelStreamingResponseAttributes: + def test_non_recording_span_returns_early(self): + span = _span(recording=False) + usage = MagicMock() + set_model_streaming_response_attributes(span, usage) + span.set_attribute.assert_not_called() + + def test_with_usage_sets_token_counts(self): + span = _span() + usage = MagicMock() + usage.completion_tokens = 25 + usage.prompt_tokens = 55 + usage.total_tokens = 80 + set_model_streaming_response_attributes(span, usage) + assert _attr(span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS) == 25 + assert _attr(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS) == 55 + + def test_none_usage_skips_token_counts(self): + span = _span() + set_model_streaming_response_attributes(span, None) + set_keys = [c[0][0] for c in span.set_attribute.call_args_list] + assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS not in set_keys + + def test_with_finish_reason_sets_mapped_reason(self): + span = _span() + set_model_streaming_response_attributes(span, None, finish_reasons=["stop"]) + assert _attr(span, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS) == ["stop"] + + def test_none_finish_reason_skips_finish_reasons(self): + span = _span() + set_model_streaming_response_attributes(span, None, finish_reasons=None) + set_keys = [c[0][0] for c in span.set_attribute.call_args_list] + assert GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS not in set_keys + + def test_unknown_finish_reason_is_preserved(self): + span = _span() + set_model_streaming_response_attributes(span, None, finish_reasons=["unknown_reason_xyz"]) + assert _attr(span, GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS) == ["unknown_reason_xyz"] + + +# --------------------------------------------------------------------------- +# set_model_response_attributes +# --------------------------------------------------------------------------- + + +class TestSetModelResponseAttributes: + """ + Uses plain dicts as response so model_as_dict() returns them unchanged. + _collect_finish_reasons_from_response() uses getattr(response, "choices", None) + which returns None for dicts → reasons = [] (covers the 'if reasons:' False branch). + """ + + def _response(self, prompt_tokens=18, completion_tokens=5): + return { + "id": "chatcmpl-test", + "model": "deepseek-chat", + "usage": { + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": prompt_tokens + completion_tokens, + }, + } + + def test_non_recording_span_returns_early(self): + span = _span(recording=False) + histogram = MagicMock() + set_model_response_attributes(span, self._response(), histogram) + span.set_attribute.assert_not_called() + histogram.record.assert_not_called() + + def test_response_without_usage_skips_histogram(self): + span = _span() + histogram = MagicMock() + response = {"id": "chatcmpl-test", "model": "deepseek-chat"} + set_model_response_attributes(span, response, histogram) + histogram.record.assert_not_called() + + def test_none_histogram_does_not_raise(self): + span = _span() + set_model_response_attributes(span, self._response(), None) + # Tokens are still set on span, just not recorded in histogram + assert _attr(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS) == 18 + + def test_histogram_records_input_and_output_tokens(self): + span = _span() + histogram = MagicMock() + set_model_response_attributes(span, self._response(prompt_tokens=10, completion_tokens=5), histogram) + assert histogram.record.call_count == 2 + first = histogram.record.call_args_list[0] + assert first[0][0] == 10 + assert first[1]["attributes"]["gen_ai.token.type"] == "input" + second = histogram.record.call_args_list[1] + assert second[0][0] == 5 + assert second[1]["attributes"]["gen_ai.token.type"] == "output" + + +# --------------------------------------------------------------------------- +# set_response_attributes +# --------------------------------------------------------------------------- + + +class TestSetResponseAttributes: + def test_non_recording_span_returns_early(self): + span = _span(recording=False) + response = {"choices": [{"message": {"role": "assistant", "content": "hi"}, "finish_reason": "stop"}]} + set_response_attributes(span, response) + span.set_attribute.assert_not_called() + + def test_empty_choices_does_not_set_attribute(self): + span = _span() + set_response_attributes(span, {"choices": []}) + span.set_attribute.assert_not_called() + + def test_with_tool_calls_in_response(self): + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "tool_calls", + "message": { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_123", + "type": "function", + "function": {"name": "get_weather", "arguments": '{"location": "SF"}'}, + } + ], + }, + } + ] + } + set_response_attributes(span, response) + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + assert messages[0]["finish_reason"] == "tool_call" + parts = messages[0]["parts"] + assert any(p["type"] == "tool_call" and p["name"] == "get_weather" for p in parts) + + def test_with_content_filter(self): + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "content_filter", + "message": {"role": "assistant", "content": "..."}, + } + ] + } + set_response_attributes(span, response) + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + assert messages[0]["finish_reason"] == "content_filter" + assert messages[0]["parts"] == [{"type": "text", "content": "..."}] + + def test_with_legacy_function_call(self): + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": None, + "function_call": { + "name": "get_weather", + "arguments": '{"location": "London"}', + }, + }, + } + ] + } + set_response_attributes(span, response) + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + parts = messages[0]["parts"] + assert any(p["type"] == "tool_call" and p["name"] == "get_weather" for p in parts) + + def test_legacy_function_call_with_invalid_json_arguments(self): + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": None, + "function_call": {"name": "foo", "arguments": "not valid json {{"}, + }, + } + ] + } + set_response_attributes(span, response) + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + part = next(p for p in messages[0]["parts"] if p["type"] == "tool_call") + assert part["arguments"] == "not valid json {{" + + def test_legacy_function_call_with_dict_arguments(self): + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": None, + "function_call": {"name": "bar", "arguments": {"x": 42}}, + }, + } + ] + } + set_response_attributes(span, response) + value = _attr(span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES) + messages = json.loads(value) + part = next(p for p in messages[0]["parts"] if p["type"] == "tool_call") + assert part["arguments"] == {"x": 42} + + def test_no_prompts_does_not_set_attribute(self): + span = _span() + response = {"choices": [{"message": {"role": "assistant", "content": "hi"}, "finish_reason": "stop"}]} + with pytest.MonkeyPatch().context() as mp: + mp.setenv(TRACELOOP_TRACE_CONTENT, "False") + set_response_attributes(span, response) + span.set_attribute.assert_not_called() + + def test_reasoning_content_present_for_deepseek_reasoner(self): + """deepseek-reasoner (R1) responses include reasoning_content -> span attribute is set.""" + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "The answer is 42.", + "reasoning_content": "Let me think step by step...", + }, + } + ] + } + set_response_attributes(span, response) + assert _attr(span, GEN_AI_DEEPSEEK_REASONING_CONTENT) == "Let me think step by step..." + + def test_reasoning_content_absent_for_deepseek_chat(self): + """deepseek-chat responses have no reasoning_content -> attribute is not set.""" + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": "Hi there!"}, + } + ] + } + set_response_attributes(span, response) + attr_names = {c[0][0] for c in span.set_attribute.call_args_list} + assert GEN_AI_DEEPSEEK_REASONING_CONTENT not in attr_names + + def test_reasoning_content_joined_across_multiple_choices(self): + span = _span() + response = { + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Answer A", + "reasoning_content": "Reasoning A", + }, + }, + { + "index": 1, + "finish_reason": "stop", + "message": { + "role": "assistant", + "content": "Answer B", + "reasoning_content": "Reasoning B", + }, + }, + ] + } + set_response_attributes(span, response) + assert _attr(span, GEN_AI_DEEPSEEK_REASONING_CONTENT) == "Reasoning A\n---\nReasoning B" diff --git a/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_utils.py b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_utils.py new file mode 100644 index 0000000000..835881b3f6 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/tests/traces/test_utils.py @@ -0,0 +1,99 @@ +""" +Unit tests for utils helpers. + +Covers: + dont_throw exception handler, error_metrics_attributes, model_as_dict edge cases. +All tests are pure unit tests — no network calls, no cassettes. +""" + +from unittest.mock import MagicMock, patch + +from opentelemetry.instrumentation.deepseek.config import Config +from opentelemetry.instrumentation.deepseek.utils import ( + dont_throw, + error_metrics_attributes, + model_as_dict, +) +from opentelemetry.semconv._incubating.attributes import gen_ai_attributes as GenAIAttributes + + +# --------------------------------------------------------------------------- +# dont_throw +# --------------------------------------------------------------------------- + + +class TestDontThrow: + def test_exception_logger_called_when_set(self): + callback = MagicMock() + original = Config.exception_logger + Config.exception_logger = callback + + @dont_throw + def failing(): + raise ValueError("boom") + + try: + failing() # must not raise + finally: + Config.exception_logger = original + + callback.assert_called_once() + assert isinstance(callback.call_args[0][0], ValueError) + + def test_exception_not_propagated_without_logger(self): + @dont_throw + def failing(): + raise RuntimeError("silent") + + # Should return None silently + result = failing() + assert result is None + + +# --------------------------------------------------------------------------- +# error_metrics_attributes +# --------------------------------------------------------------------------- + + +class TestErrorMetricsAttributes: + def test_returns_provider_name_and_error_type(self): + result = error_metrics_attributes(ValueError("oops")) + assert result[GenAIAttributes.GEN_AI_PROVIDER_NAME] == "deepseek" + assert result["error.type"] == "ValueError" + + +# --------------------------------------------------------------------------- +# model_as_dict +# --------------------------------------------------------------------------- + + +class TestModelAsDict: + def test_pydantic_v2_model_uses_model_dump(self): + model = MagicMock() + model.model_dump.return_value = {"key": "value"} + # hasattr returns True for MagicMock by default + result = model_as_dict(model) + assert result == {"key": "value"} + model.model_dump.assert_called_once() + + def test_pydantic_v1_model_uses_dict(self): + model = MagicMock(spec=["dict"]) # only has .dict(), no .model_dump + model.dict.return_value = {"key": "v1"} + with patch("opentelemetry.instrumentation.deepseek.utils._PYDANTIC_VERSION", "1.9.0"): + result = model_as_dict(model) + assert result == {"key": "v1"} + + def test_raw_api_response_with_parse_method(self): + # Simulate a raw API response that has .parse() but no .model_dump + inner = {"parsed": True} + + class RawResponse: + def parse(self): + return inner + + result = model_as_dict(RawResponse()) + assert result == {"parsed": True} + + def test_plain_dict_returned_as_is(self): + data = {"a": 1, "b": 2} + assert model_as_dict(data) is data diff --git a/packages/opentelemetry-instrumentation-deepseek/uv.lock b/packages/opentelemetry-instrumentation-deepseek/uv.lock new file mode 100644 index 0000000000..6dd8e8ad43 --- /dev/null +++ b/packages/opentelemetry-instrumentation-deepseek/uv.lock @@ -0,0 +1,905 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4" + +[manifest] +constraints = [ + { name = "pip", specifier = ">=25.3" }, + { name = "urllib3", specifier = ">=2.6.3" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +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'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "autopep8" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycodestyle" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/d8/30873d2b7b57dee9263e53d142da044c4600a46f2d28374b3e38b023df16/autopep8-2.3.2.tar.gz", hash = "sha256:89440a4f969197b69a995e4ce0661b031f455a9f776d2c5ba3dbd83466931758", size = 92210, upload-time = "2025-01-14T14:46:18.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl", hash = "sha256:ce8ad498672c845a0c3de2629c15b635ec2b05ef8177a6e7c91c74f3e9b51128", size = 45807, upload-time = "2025-01-14T14:46:15.466Z" }, +] + +[[package]] +name = "certifi" +version = "2026.5.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +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 = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" }, + { url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" }, + { url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" }, + { url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" }, + { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" }, + { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" }, + { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" }, + { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" }, + { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" }, + { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" }, + { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" }, + { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" }, + { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" }, + { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" }, + { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" }, + { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" }, + { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" }, + { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" }, + { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" }, + { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" }, + { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" }, + { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" }, + { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" }, + { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" }, +] + +[[package]] +name = "openai" +version = "2.41.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/a6/5815fe2e2aca74b36c650d1bd43b69827cee568073d0d2d9b6fc5aaac80c/openai-2.41.0.tar.gz", hash = "sha256:db5c362acd6604b84f076abbefa66826ea4b46ecba2954ed866e6a149a1352c0", size = 783525, upload-time = "2026-06-03T22:39:40.719Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/51/d82bb424e8aa372190c5233253a2ceb399a778747d18b42cff487411e663/openai-2.41.0-py3-none-any.whl", hash = "sha256:20cc7952e8501c7e5773dd2ef7be437bae9cb549044902e1041a83a54516e375", size = 1353378, upload-time = "2026-06-03T22:39:38.964Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-deepseek" +version = "0.61.0" +source = { editable = "." } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-semantic-conventions-ai" }, +] + +[package.optional-dependencies] +instruments = [ + { name = "openai" }, +] + +[package.dev-dependencies] +dev = [ + { name = "autopep8" }, + { name = "pytest" }, + { name = "pytest-sugar" }, + { name = "ruff" }, +] +test = [ + { name = "openai" }, + { name = "opentelemetry-sdk" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-recording" }, + { name = "pytest-sugar" }, + { name = "vcrpy" }, +] + +[package.metadata] +requires-dist = [ + { name = "openai", marker = "extra == 'instruments'", specifier = ">=1.0.0" }, + { name = "opentelemetry-api", specifier = ">=1.38.0,<2" }, + { name = "opentelemetry-instrumentation", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.5.1,<0.6.0" }, +] +provides-extras = ["instruments"] + +[package.metadata.requires-dev] +dev = [ + { name = "autopep8", specifier = ">=2.2.0,<3" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "ruff", specifier = ">=0.4.0" }, +] +test = [ + { name = "openai", specifier = ">=1.0.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.38.0,<2" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-asyncio", specifier = ">=0.23.7,<0.24.0" }, + { name = "pytest-recording", specifier = ">=0.13.1,<0.14.0" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "vcrpy", specifier = ">=8.0.0,<9" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.63b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +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 = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/b4/0b378b7bf26a8ae161c3890c0b48a91a04106c5713ce81b4b080ea2f4f18/pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3", size = 46920, upload-time = "2024-07-17T17:39:34.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", size = 17663, upload-time = "2024-07-17T17:39:32.478Z" }, +] + +[[package]] +name = "pytest-recording" +version = "0.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "vcrpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/9c/f4027c5f1693847b06d11caf4b4f6bb09f22c1581ada4663877ec166b8c6/pytest_recording-0.13.4.tar.gz", hash = "sha256:568d64b2a85992eec4ae0a419c855d5fd96782c5fb016784d86f18053792768c", size = 26576, upload-time = "2025-05-08T10:41:11.231Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723, upload-time = "2025-05-08T10:41:09.684Z" }, +] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pytest" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/ac/5754f5edd6d508bc6493bc37d74b928f102a5fff82d9a80347e180998f08/pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a", size = 14992, upload-time = "2024-02-01T18:30:36.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fb/889f1b69da2f13691de09a111c16c4766a433382d44aa0ecf221deded44a/pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd", size = 10171, upload-time = "2024-02-01T18:30:29.395Z" }, +] + +[[package]] +name = "pyyaml" +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" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" }, + { url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" }, + { url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" }, + { url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tqdm" +version = "4.68.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/05/0d5260f1f1ca784f4a4a0def9cbe6affe587f5b4025328d446c3d67765f4/tqdm-4.68.2.tar.gz", hash = "sha256:89c230e8dbc67c7615c142487111222f878c77427ea09549960f62389e258add", size = 171923, upload-time = "2026-06-09T13:26:42.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/75/1a0392bcc21c44dcdf87b3cf2d137e7829be2c083a1e38d44efca3d57a16/tqdm-4.68.2-py3-none-any.whl", hash = "sha256:d4240441fb5353290b87d6a85968c9decc131a99b8c7faa28269d829de669ede", size = 78578, upload-time = "2026-06-09T13:26:40.731Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.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" } +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" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +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" } +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" }, +] + +[[package]] +name = "vcrpy" +version = "8.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/07/bcfd5ebd7cb308026ab78a353e091bd699593358be49197d39d004e5ad83/vcrpy-8.1.1.tar.gz", hash = "sha256:58e3053e33b423f3594031cb758c3f4d1df931307f1e67928e30cf352df7709f", size = 85770, upload-time = "2026-01-04T19:22:03.886Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d7/f79b05a5d728f8786876a7d75dfb0c5cae27e428081b2d60152fb52f155f/vcrpy-8.1.1-py3-none-any.whl", hash = "sha256:2d16f31ad56493efb6165182dd99767207031b0da3f68b18f975545ede8ac4b9", size = 42445, upload-time = "2026-01-04T19:22:02.532Z" }, +] + +[[package]] +name = "wrapt" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/8b/84bc1ea68b620fe0e2696a8cff07e82f4b962d952ab14efee8955997bb70/wrapt-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0f68f478004475d97906686e702ddbddeaf717c0b68ad2794384308f2dc713ae", size = 80093, upload-time = "2026-05-22T14:47:27.074Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8f/64ec81194a0bc708d9720174c998c8a32116e82b5b32c04e20a7fe01176c/wrapt-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e422b2d647a65d6b080cad5accd09055d3809bdff00c76fba8dca00ca935572a", size = 81183, upload-time = "2026-05-22T14:47:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/3d186944aae923631d1def58f4c4ff8f0b6309906afc0b6978de3e69b3e0/wrapt-2.2.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:036dfb40128819a751c6f451c6b9c10172c49e4c401aebcdb8ecf2aec1683598", size = 152494, upload-time = "2026-05-22T14:47:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/01/d1/6b3d0ea995b867d2862aad5619bd5e17de09a9d64a821f46832dcd272d40/wrapt-2.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09ac16c081bebfd15d8e4dfa5bdc805990bbd52249ecff22530da7a129d6120b", size = 154310, upload-time = "2026-05-22T14:47:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4b/37ecb90a8c3753e580327fb40731a984b754e3df65d2ef932bf359fe4adc/wrapt-2.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07be671fa8875971222b0ba9059ed8b4dc738631122feba17c93aa36b4213e9a", size = 149002, upload-time = "2026-05-22T14:47:34.021Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/918884d9dfa84d0d135b42a51c00910f5c5447fe7a5e211a8e16ac324dd4/wrapt-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93fc2bf40cd7f4a0256010dce073d44eeb4a351b9bca94d0477ce2b6e62532b3", size = 153185, upload-time = "2026-05-22T14:47:35.722Z" }, + { url = "https://files.pythonhosted.org/packages/4c/00/382299d8ced610b29b59b099a89eda821e8c489aa152b7183748ac83f32a/wrapt-2.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba519b2d765df9871a25879e6f7fa78948ea59a2a31f9c1a257e34b651994afc", size = 148040, upload-time = "2026-05-22T14:47:37.052Z" }, + { url = "https://files.pythonhosted.org/packages/6c/46/62a79b79e35bbebb1207ca5d15b81192f37f20cc5659cf4e3ce955b7fcc8/wrapt-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9011395be8db1827d106c6449b4bb6dd17e331ff6ec521f227e4588f1c78e46f", size = 151773, upload-time = "2026-05-22T14:47:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/db/95c152151d206d4b430516c89725306e92484072f38e65492afde63f6d19/wrapt-2.2.1-cp310-cp310-win32.whl", hash = "sha256:a8f7176b83664af44567e9cc06e0d3827823fcc1a5e52307ebb8ac3aa95860b9", size = 77393, upload-time = "2026-05-22T14:47:40.061Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/882d50452c6fbd13f24fe5d2644b97cdad2565a7e1522cbb6312de8a52cf/wrapt-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:d7f513d3185e6fec82d0c3518f2e6365d8b4e49f5f45f29640d5162d56a23b54", size = 80350, upload-time = "2026-05-22T14:47:41.194Z" }, + { url = "https://files.pythonhosted.org/packages/58/0f/148376523b4e370692286a9ba14d5715cf3c5b86da3bd3630926367b6b73/wrapt-2.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:44255c84bc57554fed822e83e70036b51afa9edb56fc7ca56c54410ece7898c9", size = 79149, upload-time = "2026-05-22T14:47:42.835Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ac/4370bde262c0e633e6c4f0e56d55095710024cf9a5cecc20c59a10de483c/wrapt-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd57607acc85678925940bd5df0385ff8332083a32fa8d7a43f8767f4997263c", size = 80321, upload-time = "2026-05-22T14:47:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/b8ff3a61e71babf58a8cf4c0d63358e8bad383e15bf7f35e62d2f6b6e4a4/wrapt-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ae574d65c9fa8e86f64f6a7c2668f9fcd507b183e0e577619f504b883cb0a6c", size = 81216, upload-time = "2026-05-22T14:47:45.243Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/c0cac1f77c9c4f6fe58a920ca632ce379bb8be928720e11e8d73de28a5e9/wrapt-2.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a04c28c10ba7fd12842b109d2edb0678872a2fe65277ca4ff06a0d61edee245", size = 159208, upload-time = "2026-05-22T14:47:47.176Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4f/744132a7b2fbefa6b81118ec5942eca5fc2e9a129f9055a0c5e46885a549/wrapt-2.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2f02472a1cbbf3884b365714a810b5947134a95ad6952b554cb8cce9d492b0", size = 160322, upload-time = "2026-05-22T14:47:49.04Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/b7cd9a22a06cf93e6482904ee6afc956248983553593fd1009296d1b3b31/wrapt-2.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac2745950b2bff80219c15ebf2fa9d8427eba7e249739f97e55c9d169e47e9e1", size = 153243, upload-time = "2026-05-22T14:47:50.386Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4a/eb79423192015f46f0db2872e7e04a3dde8d359b83411e8959e7c9287eaa/wrapt-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a97e5b6c457f0cd3cfc19ebb2d84463e60c3ece754cc831e4281a3ca29bb18", size = 159231, upload-time = "2026-05-22T14:47:51.753Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dc/435015b58ce33c6fc4104158fa91ddb0e809ab03a5751fb7465d1d461456/wrapt-2.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c803a3d331796255af51ba2c79ed0ac8275865b516c09e61f248d1e7aff31ce9", size = 152351, upload-time = "2026-05-22T14:47:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/77/ac/5d203f98df8fd136b95c5227139aea02d34505e18baf812d0c005df61963/wrapt-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b984d1eb252145d6302c1dbd5e87fc6d404d45531447c84eadec04bf1fcb027", size = 158347, upload-time = "2026-05-22T14:47:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/a92427dbdc74e54c1674abbed27e61b2cb5e7a94441b8c1270c70671d928/wrapt-2.2.1-cp311-cp311-win32.whl", hash = "sha256:8a983a603a18c8708f024f7f6991b2e66159219abbf894634c5056243c55f3cd", size = 77562, upload-time = "2026-05-22T14:47:56.275Z" }, + { url = "https://files.pythonhosted.org/packages/c8/56/987b9c13b3e1c1a3c6de71284076f996b79caec90e75a87c044a40c23db9/wrapt-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:9c210a6994b21aa9b29e81c8d11560e8fdab54c117e9cff37870d0a27bde1343", size = 80616, upload-time = "2026-05-22T14:47:57.854Z" }, + { url = "https://files.pythonhosted.org/packages/7e/25/d01f560888d99d94a959c85533de349ce68d71ace3f2591d6ea8f632cfed/wrapt-2.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:401229e9d63ca09f9b8891ecf83798d26c11bbb445d11ed9f1836b6d4585b38a", size = 79025, upload-time = "2026-05-22T14:47:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" }, + { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" }, + { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" }, + { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" }, + { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" }, + { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, + { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, + { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" }, + { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" }, + { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" }, + { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, +] diff --git a/packages/sample-app/pyproject.toml b/packages/sample-app/pyproject.toml index fe7b26742d..04d2e30326 100644 --- a/packages/sample-app/pyproject.toml +++ b/packages/sample-app/pyproject.toml @@ -58,6 +58,7 @@ dependencies = [ "opentelemetry-instrumentation-vertexai", "opentelemetry-instrumentation-google-generativeai", "opentelemetry-instrumentation-groq", + "opentelemetry-instrumentation-deepseek", "opentelemetry-instrumentation-crewai", "opentelemetry-instrumentation-openai-agents", "opentelemetry-instrumentation-agno", @@ -78,6 +79,7 @@ opentelemetry-instrumentation-replicate = { path = "../opentelemetry-instrumenta opentelemetry-instrumentation-vertexai = { path = "../opentelemetry-instrumentation-vertexai", editable = true } opentelemetry-instrumentation-google-generativeai = { path = "../opentelemetry-instrumentation-google-generativeai", editable = true } opentelemetry-instrumentation-groq = { path = "../opentelemetry-instrumentation-groq", editable = true } +opentelemetry-instrumentation-deepseek = { path = "../opentelemetry-instrumentation-deepseek", editable = true } opentelemetry-instrumentation-crewai = { path = "../opentelemetry-instrumentation-crewai", editable = true } opentelemetry-instrumentation-openai-agents = { path = "../opentelemetry-instrumentation-openai-agents", editable = true } opentelemetry-instrumentation-agno = { path = "../opentelemetry-instrumentation-agno", editable = true } diff --git a/packages/sample-app/uv.lock b/packages/sample-app/uv.lock index f4ca95e07b..6e4f0d75b4 100644 --- a/packages/sample-app/uv.lock +++ b/packages/sample-app/uv.lock @@ -3708,7 +3708,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-agno" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-agno" } dependencies = [ { name = "opentelemetry-api" }, @@ -3748,7 +3748,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-alephalpha" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-alephalpha" } dependencies = [ { name = "opentelemetry-api" }, @@ -3786,7 +3786,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-anthropic" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-anthropic" } dependencies = [ { name = "opentelemetry-api" }, @@ -3824,7 +3824,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-bedrock" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-bedrock" } dependencies = [ { name = "opentelemetry-api" }, @@ -3860,7 +3860,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-chromadb" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-chromadb" } dependencies = [ { name = "opentelemetry-api" }, @@ -3895,7 +3895,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-cohere" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-cohere" } dependencies = [ { name = "opentelemetry-api" }, @@ -3933,7 +3933,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-crewai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-crewai" } dependencies = [ { name = "opentelemetry-api" }, @@ -3967,9 +3967,47 @@ test = [ { name = "pytest-sugar", specifier = "==1.0.0" }, ] +[[package]] +name = "opentelemetry-instrumentation-deepseek" +version = "0.61.0" +source = { editable = "../opentelemetry-instrumentation-deepseek" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-semantic-conventions-ai" }, +] + +[package.metadata] +requires-dist = [ + { name = "openai", marker = "extra == 'instruments'", specifier = ">=1.0.0" }, + { name = "opentelemetry-api", specifier = ">=1.38.0,<2" }, + { name = "opentelemetry-instrumentation", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.5.1,<0.6.0" }, +] +provides-extras = ["instruments"] + +[package.metadata.requires-dev] +dev = [ + { name = "autopep8", specifier = ">=2.2.0,<3" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "ruff", specifier = ">=0.4.0" }, +] +test = [ + { name = "openai", specifier = ">=1.0.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.38.0,<2" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-asyncio", specifier = ">=0.23.7,<0.24.0" }, + { name = "pytest-recording", specifier = ">=0.13.1,<0.14.0" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "vcrpy", specifier = ">=8.0.0,<9" }, +] + [[package]] name = "opentelemetry-instrumentation-google-generativeai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-google-generativeai" } dependencies = [ { name = "opentelemetry-api" }, @@ -4006,7 +4044,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-groq" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-groq" } dependencies = [ { name = "opentelemetry-api" }, @@ -4044,7 +4082,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-haystack" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-haystack" } dependencies = [ { name = "opentelemetry-api" }, @@ -4081,7 +4119,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-lancedb" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-lancedb" } dependencies = [ { name = "opentelemetry-api" }, @@ -4121,7 +4159,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-langchain" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-langchain" } dependencies = [ { name = "opentelemetry-api" }, @@ -4175,7 +4213,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-llamaindex" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-llamaindex" } dependencies = [ { name = "inflection" }, @@ -4238,7 +4276,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-marqo" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-marqo" } dependencies = [ { name = "opentelemetry-api" }, @@ -4276,7 +4314,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-mcp" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-mcp" } dependencies = [ { name = "opentelemetry-api" }, @@ -4316,7 +4354,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-milvus" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-milvus" } dependencies = [ { name = "opentelemetry-api" }, @@ -4330,7 +4368,7 @@ requires-dist = [ { name = "opentelemetry-api", specifier = ">=1.38.0,<2" }, { name = "opentelemetry-instrumentation", specifier = ">=0.59b0" }, { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, - { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.5.1,<0.6.0" }, + { name = "opentelemetry-semantic-conventions-ai", editable = "../opentelemetry-semantic-conventions-ai" }, { name = "pymilvus", marker = "extra == 'instruments'" }, ] provides-extras = ["instruments"] @@ -4352,7 +4390,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-mistralai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-mistralai" } dependencies = [ { name = "opentelemetry-api" }, @@ -4389,7 +4427,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-ollama" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-ollama" } dependencies = [ { name = "opentelemetry-api" }, @@ -4427,7 +4465,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-openai" } dependencies = [ { name = "opentelemetry-api" }, @@ -4464,7 +4502,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-openai-agents" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-openai-agents" } dependencies = [ { name = "opentelemetry-api" }, @@ -4503,7 +4541,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-pinecone" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-pinecone" } dependencies = [ { name = "opentelemetry-api" }, @@ -4517,7 +4555,7 @@ requires-dist = [ { name = "opentelemetry-api", specifier = ">=1.38.0,<2" }, { name = "opentelemetry-instrumentation", specifier = ">=0.59b0" }, { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, - { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.5.1,<0.6.0" }, + { name = "opentelemetry-semantic-conventions-ai", editable = "../opentelemetry-semantic-conventions-ai" }, { name = "pinecone", marker = "extra == 'instruments'", specifier = ">=5.1.0,<9" }, ] provides-extras = ["instruments"] @@ -4542,7 +4580,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-qdrant" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-qdrant" } dependencies = [ { name = "opentelemetry-api" }, @@ -4590,7 +4628,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-replicate" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-replicate" } dependencies = [ { name = "opentelemetry-api" }, @@ -4642,7 +4680,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-sagemaker" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-sagemaker" } dependencies = [ { name = "opentelemetry-api" }, @@ -4707,7 +4745,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-together" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-together" } dependencies = [ { name = "opentelemetry-api" }, @@ -4745,7 +4783,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-transformers" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-transformers" } dependencies = [ { name = "opentelemetry-api" }, @@ -4792,7 +4830,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-vertexai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-vertexai" } dependencies = [ { name = "opentelemetry-api" }, @@ -4830,7 +4868,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-voyageai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-voyageai" } dependencies = [ { name = "opentelemetry-api" }, @@ -4868,7 +4906,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-watsonx" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-watsonx" } dependencies = [ { name = "opentelemetry-api" }, @@ -4906,7 +4944,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-weaviate" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-weaviate" } dependencies = [ { name = "opentelemetry-api" }, @@ -4945,7 +4983,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-writer" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-writer" } dependencies = [ { name = "opentelemetry-api" }, @@ -5023,15 +5061,25 @@ wheels = [ [[package]] name = "opentelemetry-semantic-conventions-ai" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } +version = "0.5.2" +source = { editable = "../opentelemetry-semantic-conventions-ai" } dependencies = [ { name = "opentelemetry-sdk" }, { name = "opentelemetry-semantic-conventions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/02/10aeacc37a38a3a8fa16ff67bec1ae3bf882539f6f9efb0f70acf802ca2d/opentelemetry_semantic_conventions_ai-0.5.1.tar.gz", hash = "sha256:153906200d8c1d2f8e09bd78dbef526916023de85ac3dab35912bfafb69ff04c", size = 26533, upload-time = "2026-03-26T14:20:38.73Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/22/41fb05f1dc5fda2c468e05a41814c20859016c85117b66c8a257cae814f6/opentelemetry_semantic_conventions_ai-0.5.1-py3-none-any.whl", hash = "sha256:25aeb22bd261543b4898a73824026d96770e5351209c7d07a0b1314762b1f6e4", size = 11250, upload-time = "2026-03-26T14:20:37.108Z" }, + +[package.metadata] +requires-dist = [ + { name = "opentelemetry-sdk", specifier = ">=1.38.0,<2" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "autopep8", specifier = ">=2.2.0,<3" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "ruff", specifier = ">=0.4.0" }, ] [[package]] @@ -6462,6 +6510,7 @@ dependencies = [ { name = "opentelemetry-instrumentation-anthropic" }, { name = "opentelemetry-instrumentation-bedrock" }, { name = "opentelemetry-instrumentation-crewai" }, + { name = "opentelemetry-instrumentation-deepseek" }, { name = "opentelemetry-instrumentation-google-generativeai" }, { name = "opentelemetry-instrumentation-groq" }, { name = "opentelemetry-instrumentation-haystack" }, @@ -6530,6 +6579,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-anthropic", editable = "../opentelemetry-instrumentation-anthropic" }, { name = "opentelemetry-instrumentation-bedrock", editable = "../opentelemetry-instrumentation-bedrock" }, { name = "opentelemetry-instrumentation-crewai", editable = "../opentelemetry-instrumentation-crewai" }, + { name = "opentelemetry-instrumentation-deepseek", editable = "../opentelemetry-instrumentation-deepseek" }, { name = "opentelemetry-instrumentation-google-generativeai", editable = "../opentelemetry-instrumentation-google-generativeai" }, { name = "opentelemetry-instrumentation-groq", editable = "../opentelemetry-instrumentation-groq" }, { name = "opentelemetry-instrumentation-haystack", editable = "../opentelemetry-instrumentation-haystack" }, @@ -7090,7 +7140,7 @@ wheels = [ [[package]] name = "traceloop-sdk" -version = "0.60.0" +version = "0.61.0" source = { editable = "../traceloop-sdk" } dependencies = [ { name = "aiohttp" }, @@ -7108,6 +7158,7 @@ dependencies = [ { name = "opentelemetry-instrumentation-chromadb" }, { name = "opentelemetry-instrumentation-cohere" }, { name = "opentelemetry-instrumentation-crewai" }, + { name = "opentelemetry-instrumentation-deepseek" }, { name = "opentelemetry-instrumentation-google-generativeai" }, { name = "opentelemetry-instrumentation-groq" }, { name = "opentelemetry-instrumentation-haystack" }, @@ -7161,6 +7212,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-chromadb", editable = "../opentelemetry-instrumentation-chromadb" }, { name = "opentelemetry-instrumentation-cohere", editable = "../opentelemetry-instrumentation-cohere" }, { name = "opentelemetry-instrumentation-crewai", editable = "../opentelemetry-instrumentation-crewai" }, + { name = "opentelemetry-instrumentation-deepseek", editable = "../opentelemetry-instrumentation-deepseek" }, { name = "opentelemetry-instrumentation-google-generativeai", editable = "../opentelemetry-instrumentation-google-generativeai" }, { name = "opentelemetry-instrumentation-groq", editable = "../opentelemetry-instrumentation-groq" }, { name = "opentelemetry-instrumentation-haystack", editable = "../opentelemetry-instrumentation-haystack" }, diff --git a/packages/traceloop-sdk/pyproject.toml b/packages/traceloop-sdk/pyproject.toml index 8c913da49c..35adb7bcbb 100644 --- a/packages/traceloop-sdk/pyproject.toml +++ b/packages/traceloop-sdk/pyproject.toml @@ -52,6 +52,7 @@ dependencies = [ "opentelemetry-instrumentation-alephalpha", "opentelemetry-instrumentation-marqo", "opentelemetry-instrumentation-groq", + "opentelemetry-instrumentation-deepseek", "opentelemetry-instrumentation-mcp", "colorama>=0.4.6,<0.5.0", "tenacity>=8.2.3,<10.0", @@ -162,6 +163,7 @@ opentelemetry-instrumentation-bedrock = { path = "../opentelemetry-instrumentati opentelemetry-instrumentation-chromadb = { path = "../opentelemetry-instrumentation-chromadb", editable = true } opentelemetry-instrumentation-cohere = { path = "../opentelemetry-instrumentation-cohere", editable = true } opentelemetry-instrumentation-crewai = { path = "../opentelemetry-instrumentation-crewai", editable = true } +opentelemetry-instrumentation-deepseek = { path = "../opentelemetry-instrumentation-deepseek", editable = true } opentelemetry-instrumentation-google-generativeai = { path = "../opentelemetry-instrumentation-google-generativeai", editable = true } opentelemetry-instrumentation-groq = { path = "../opentelemetry-instrumentation-groq", editable = true } opentelemetry-instrumentation-haystack = { path = "../opentelemetry-instrumentation-haystack", editable = true } diff --git a/packages/traceloop-sdk/uv.lock b/packages/traceloop-sdk/uv.lock index 66a99bad13..8b2ca97c88 100644 --- a/packages/traceloop-sdk/uv.lock +++ b/packages/traceloop-sdk/uv.lock @@ -1517,7 +1517,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-agno" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-agno" } dependencies = [ { name = "opentelemetry-api" }, @@ -1557,7 +1557,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-alephalpha" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-alephalpha" } dependencies = [ { name = "opentelemetry-api" }, @@ -1595,7 +1595,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-anthropic" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-anthropic" } dependencies = [ { name = "opentelemetry-api" }, @@ -1633,7 +1633,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-bedrock" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-bedrock" } dependencies = [ { name = "opentelemetry-api" }, @@ -1669,7 +1669,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-chromadb" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-chromadb" } dependencies = [ { name = "opentelemetry-api" }, @@ -1704,7 +1704,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-cohere" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-cohere" } dependencies = [ { name = "opentelemetry-api" }, @@ -1742,7 +1742,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-crewai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-crewai" } dependencies = [ { name = "opentelemetry-api" }, @@ -1776,9 +1776,47 @@ test = [ { name = "pytest-sugar", specifier = "==1.0.0" }, ] +[[package]] +name = "opentelemetry-instrumentation-deepseek" +version = "0.61.0" +source = { editable = "../opentelemetry-instrumentation-deepseek" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-semantic-conventions-ai" }, +] + +[package.metadata] +requires-dist = [ + { name = "openai", marker = "extra == 'instruments'", specifier = ">=1.0.0" }, + { name = "opentelemetry-api", specifier = ">=1.38.0,<2" }, + { name = "opentelemetry-instrumentation", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions", specifier = ">=0.59b0" }, + { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.5.1,<0.6.0" }, +] +provides-extras = ["instruments"] + +[package.metadata.requires-dev] +dev = [ + { name = "autopep8", specifier = ">=2.2.0,<3" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "ruff", specifier = ">=0.4.0" }, +] +test = [ + { name = "openai", specifier = ">=1.0.0" }, + { name = "opentelemetry-sdk", specifier = ">=1.38.0,<2" }, + { name = "pytest", specifier = ">=8.2.2,<9" }, + { name = "pytest-asyncio", specifier = ">=0.23.7,<0.24.0" }, + { name = "pytest-recording", specifier = ">=0.13.1,<0.14.0" }, + { name = "pytest-sugar", specifier = "==1.0.0" }, + { name = "vcrpy", specifier = ">=8.0.0,<9" }, +] + [[package]] name = "opentelemetry-instrumentation-google-generativeai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-google-generativeai" } dependencies = [ { name = "opentelemetry-api" }, @@ -1815,7 +1853,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-groq" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-groq" } dependencies = [ { name = "opentelemetry-api" }, @@ -1853,7 +1891,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-haystack" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-haystack" } dependencies = [ { name = "opentelemetry-api" }, @@ -1890,7 +1928,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-lancedb" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-lancedb" } dependencies = [ { name = "opentelemetry-api" }, @@ -1930,7 +1968,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-langchain" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-langchain" } dependencies = [ { name = "opentelemetry-api" }, @@ -1984,7 +2022,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-llamaindex" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-llamaindex" } dependencies = [ { name = "inflection" }, @@ -2047,7 +2085,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-marqo" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-marqo" } dependencies = [ { name = "opentelemetry-api" }, @@ -2085,7 +2123,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-mcp" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-mcp" } dependencies = [ { name = "opentelemetry-api" }, @@ -2125,7 +2163,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-milvus" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-milvus" } dependencies = [ { name = "opentelemetry-api" }, @@ -2161,7 +2199,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-mistralai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-mistralai" } dependencies = [ { name = "opentelemetry-api" }, @@ -2198,7 +2236,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-ollama" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-ollama" } dependencies = [ { name = "opentelemetry-api" }, @@ -2236,7 +2274,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-openai" } dependencies = [ { name = "opentelemetry-api" }, @@ -2273,7 +2311,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-openai-agents" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-openai-agents" } dependencies = [ { name = "opentelemetry-api" }, @@ -2312,7 +2350,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-pinecone" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-pinecone" } dependencies = [ { name = "opentelemetry-api" }, @@ -2351,7 +2389,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-qdrant" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-qdrant" } dependencies = [ { name = "opentelemetry-api" }, @@ -2399,7 +2437,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-replicate" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-replicate" } dependencies = [ { name = "opentelemetry-api" }, @@ -2451,7 +2489,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-sagemaker" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-sagemaker" } dependencies = [ { name = "opentelemetry-api" }, @@ -2516,7 +2554,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-together" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-together" } dependencies = [ { name = "opentelemetry-api" }, @@ -2554,7 +2592,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-transformers" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-transformers" } dependencies = [ { name = "opentelemetry-api" }, @@ -2601,7 +2639,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-vertexai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-vertexai" } dependencies = [ { name = "opentelemetry-api" }, @@ -2639,7 +2677,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-voyageai" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-voyageai" } dependencies = [ { name = "opentelemetry-api" }, @@ -2677,7 +2715,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-watsonx" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-watsonx" } dependencies = [ { name = "opentelemetry-api" }, @@ -2715,7 +2753,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-weaviate" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-weaviate" } dependencies = [ { name = "opentelemetry-api" }, @@ -2754,7 +2792,7 @@ test = [ [[package]] name = "opentelemetry-instrumentation-writer" -version = "0.60.0" +version = "0.61.0" source = { editable = "../opentelemetry-instrumentation-writer" } dependencies = [ { name = "opentelemetry-api" }, @@ -3878,7 +3916,7 @@ wheels = [ [[package]] name = "traceloop-sdk" -version = "0.60.0" +version = "0.61.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -3896,6 +3934,7 @@ dependencies = [ { name = "opentelemetry-instrumentation-chromadb" }, { name = "opentelemetry-instrumentation-cohere" }, { name = "opentelemetry-instrumentation-crewai" }, + { name = "opentelemetry-instrumentation-deepseek" }, { name = "opentelemetry-instrumentation-google-generativeai" }, { name = "opentelemetry-instrumentation-groq" }, { name = "opentelemetry-instrumentation-haystack" }, @@ -3978,6 +4017,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-chromadb", editable = "../opentelemetry-instrumentation-chromadb" }, { name = "opentelemetry-instrumentation-cohere", editable = "../opentelemetry-instrumentation-cohere" }, { name = "opentelemetry-instrumentation-crewai", editable = "../opentelemetry-instrumentation-crewai" }, + { name = "opentelemetry-instrumentation-deepseek", editable = "../opentelemetry-instrumentation-deepseek" }, { name = "opentelemetry-instrumentation-google-generativeai", editable = "../opentelemetry-instrumentation-google-generativeai" }, { name = "opentelemetry-instrumentation-groq", editable = "../opentelemetry-instrumentation-groq" }, { name = "opentelemetry-instrumentation-haystack", editable = "../opentelemetry-instrumentation-haystack" }, From 190367e29f3c38cf17179428ee8c1791b4cd2565 Mon Sep 17 00:00:00 2001 From: CaringNihilistic Date: Thu, 18 Jun 2026 02:15:30 +0530 Subject: [PATCH 2/2] minor changes --- .../opentelemetry/instrumentation/deepseek/span_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py index e33ccc4f2c..ee3a9a0cbb 100644 --- a/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py +++ b/packages/opentelemetry-instrumentation-deepseek/opentelemetry/instrumentation/deepseek/span_utils.py @@ -172,6 +172,7 @@ def set_model_input_attributes(span, kwargs): pass +@dont_throw def set_streaming_response_attributes( span, accumulated_content, @@ -194,6 +195,7 @@ def set_streaming_response_attributes( set_span_attribute(span, GEN_AI_DEEPSEEK_REASONING_CONTENT, accumulated_reasoning) +@dont_throw def set_model_streaming_response_attributes(span, usage, finish_reasons=None): if not span.is_recording(): return @@ -256,6 +258,7 @@ def set_model_response_attributes(span, response, token_histogram): ) +@dont_throw def set_response_attributes(span, response): if not span.is_recording() or not should_send_prompts(): return