diff --git a/dspy/adapters/base.py b/dspy/adapters/base.py index 7520856182..f8d497e179 100644 --- a/dspy/adapters/base.py +++ b/dspy/adapters/base.py @@ -1,9 +1,7 @@ import logging from typing import Any, get_origin -import json_repair - -from dspy.adapters.types import History, Type +from dspy.adapters.types import ActionEvent, History, InputEvent, LegacyEvent, OutputEvent, Type from dspy.adapters.types.base_type import split_message_content_for_custom_types from dspy.adapters.types.reasoning import Reasoning from dspy.adapters.types.tool import Tool, ToolCalls @@ -93,6 +91,7 @@ def _call_preprocess( signature_for_native_function_calling = signature_for_native_function_calling.delete( tool_call_input_field_name ) + signature_for_native_function_calling.__dspy_native_fc__ = True return signature_for_native_function_calling @@ -148,14 +147,7 @@ def _call_postprocess( ) if tool_calls and tool_call_output_field_name: - tool_calls = [ - { - "name": v["function"]["name"], - "args": json_repair.loads(v["function"]["arguments"]), - } - for v in tool_calls - ] - value[tool_call_output_field_name] = ToolCalls.from_dict_list(tool_calls) + value[tool_call_output_field_name] = ToolCalls.model_validate(tool_calls) # Parse custom types that does not rely on the `Adapter.parse()` method for name, field in original_signature.output_fields.items(): @@ -269,24 +261,39 @@ def format( # If the signature and inputs have conversation history, we need to format the conversation history and # remove the history field from the signature. history_field_name = self._get_history_field_name(signature) + has_open_episode = False if history_field_name: + # Check if history has an open episode BEFORE format_conversation_history deletes it from inputs_copy. + history_obj = inputs_copy.get(history_field_name) + has_open_episode = hasattr(history_obj, "has_open_episode") and history_obj.has_open_episode() + # In order to format the conversation history, we need to remove the history field from the signature. signature_without_history = signature.delete(history_field_name) + if getattr(signature, "__dspy_native_fc__", False): + signature_without_history.__dspy_native_fc__ = True conversation_history = self.format_conversation_history( signature_without_history, history_field_name, inputs_copy, ) + inputs_copy.pop(history_field_name, None) messages = [] system_message = self.format_system_message(signature) messages.append({"role": "system", "content": system_message}) messages.extend(self.format_demos(signature, demos)) if history_field_name: - # Conversation history and current input - content = self.format_user_message_content(signature_without_history, inputs_copy, main_request=True) messages.extend(conversation_history) - messages.append({"role": "user", "content": content}) + if has_open_episode and hasattr(self, "user_message_output_requirements"): + # The InputEvent in the conversation history already contains the current query inputs. + # Only append the output requirements suffix to avoid sending the query twice. + output_req = self.user_message_output_requirements(signature_without_history) + if output_req: + messages.append({"role": "user", "content": output_req}) + else: + # No open episode — include the full input + output requirements as before. + content = self.format_user_message_content(signature_without_history, inputs_copy, main_request=True) + messages.append({"role": "user", "content": content}) else: # Only current input content = self.format_user_message_content(signature, inputs_copy, main_request=True) @@ -501,22 +508,70 @@ def format_conversation_history( return [] messages = [] - for message in conversation_history: - messages.append( - { - "role": "user", - "content": self.format_user_message_content(signature, message), - } - ) - messages.append( - { - "role": "assistant", - "content": self.format_assistant_message_content(signature, message), - } - ) - - # Remove the history field from the inputs - del inputs[history_field_name] + for event_idx, message in enumerate(conversation_history): + if isinstance(message, InputEvent): + content = self.format_user_message_content(signature, message.inputs) + if content.strip(): + messages.append({"role": "user", "content": content}) + elif isinstance(message, ActionEvent): + is_native_fc = getattr(signature, "__dspy_native_fc__", False) + tc_obj = message.tool_calls + obs = message.observations + + if is_native_fc and tc_obj and hasattr(tc_obj, "tool_calls"): + import json as _json + tc_list = [] + # Pre-generate stable IDs for tool calls that lack them so the + # assistant message and the corresponding tool-response messages + # reference the same ID (required by the OpenAI API). + resolved_ids = [] + for call_idx, tc in enumerate(tc_obj.tool_calls): + resolved_ids.append(tc.id or f"call_{event_idx}_{call_idx}") + for tc, tid in zip(tc_obj.tool_calls, resolved_ids, strict=True): + fmt = tc.format() + # Ensure the id is always present + fmt["id"] = tid + if isinstance(fmt.get("function", {}).get("arguments"), dict): + fmt["function"]["arguments"] = _json.dumps(fmt["function"]["arguments"]) + tc_list.append(fmt) + asst_msg = {"role": "assistant", "tool_calls": tc_list} + thought = message.thought + if thought: + asst_msg["content"] = str(thought) + messages.append(asst_msg) + for tid, obs_item in zip(resolved_ids, obs, strict=False): + content = str(obs_item.value) if not isinstance(obs_item.value, list) else "\n".join(str(r) for r in obs_item.value) + messages.append({"role": "tool", "content": content, "tool_call_id": tid}) + else: + asst_content = self.format_assistant_message_content( + signature, + {"next_thought": message.thought, "tool_calls": tc_obj}, + missing_field_message="Not supplied for this conversation history message. ", + ) + messages.append({"role": "assistant", "content": asst_content}) + if obs: + obs_parts = [] + for obs_item in obs: + label = "Error" if obs_item.is_error else "Observation" + if isinstance(obs_item.value, list): + obs_parts.append(f"{label}:\n" + "\n".join(str(item) for item in obs_item.value)) + else: + obs_parts.append(f"{label}: {obs_item.value}") + messages.append({"role": "user", "content": "\n\n".join(obs_parts)}) + elif isinstance(message, OutputEvent): + pass + elif isinstance(message, LegacyEvent): + # Backward compat fallback for plain dicts wrapped as LegacyEvent + messages.append({"role": "user", "content": self.format_user_message_content(signature, message.data)}) + messages.append( + { + "role": "assistant", + "content": self.format_assistant_message_content( + signature, message.data, + missing_field_message="Not supplied for this conversation history message. ", + ), + } + ) return messages diff --git a/dspy/adapters/chat_adapter.py b/dspy/adapters/chat_adapter.py index e94199fee1..ea0ca3d4e6 100644 --- a/dspy/adapters/chat_adapter.py +++ b/dspy/adapters/chat_adapter.py @@ -78,9 +78,10 @@ def __call__( isinstance(e, ContextWindowExceededError) or isinstance(self, JSONAdapter) or not self.use_json_adapter_fallback + or "tools" in lm_kwargs # Don't fall back to JSON mode when native FC is active ): - # On context window exceeded error, already using JSONAdapter, or use_json_adapter_fallback is False - # we don't want to retry with a different adapter. Raise the original error instead of the fallback error. + # On context window exceeded error, already using JSONAdapter, use_json_adapter_fallback is False, + # or native function calling is active — don't retry with a different adapter. raise e return JSONAdapter()(lm, lm_kwargs, signature, demos, inputs) @@ -102,9 +103,10 @@ async def acall( isinstance(e, ContextWindowExceededError) or isinstance(self, JSONAdapter) or not self.use_json_adapter_fallback + or "tools" in lm_kwargs # Don't fall back to JSON mode when native FC is active ): - # On context window exceeded error, already using JSONAdapter, or use_json_adapter_fallback is False - # we don't want to retry with a different adapter. Raise the original error instead of the fallback error. + # On context window exceeded error, already using JSONAdapter, use_json_adapter_fallback is False, + # or native function calling is active — don't retry with a different adapter. raise e return await JSONAdapter().acall(lm, lm_kwargs, signature, demos, inputs) @@ -120,6 +122,9 @@ def format_field_structure(self, signature: type[Signature]) -> str: `[[ ## field_name ## ]]`. An arbitrary field `completed` ([[ ## completed ## ]]) is added to the end of the output fields section to indicate the end of the output fields. """ + if getattr(signature, "__dspy_native_fc__", False): + return self._format_native_fc_structure(signature) + parts = [] parts.append("All interactions will be structured in the following way, with the appropriate values filled in.") @@ -136,6 +141,14 @@ def format_signature_fields_for_instructions(fields: dict[str, FieldInfo]): parts.append("[[ ## completed ## ]]\n") return "\n\n".join(parts).strip() + def _format_native_fc_structure(self, signature: type[Signature]) -> str: + parts = ["You will receive inputs and must respond with your reasoning in plain text, then call the appropriate tool."] + for name, field in signature.output_fields.items(): + desc = get_field_description_string({name: field}) + parts.append(f"Your response text should contain: {desc.strip()}") + parts.append("Do NOT use any special markers or delimiters. Think step-by-step, then call the appropriate tool via the API.") + return "\n".join(parts) + def format_task_description(self, signature: type[Signature]) -> str: instructions = textwrap.dedent(signature.instructions) objective = ("\n" + " " * 8).join([""] + instructions.splitlines()) @@ -164,23 +177,9 @@ def format_user_message_content( messages.append(suffix) return "\n\n".join(messages).strip() - def user_message_output_requirements(self, signature: type[Signature]) -> str: - """Returns a simplified format reminder for the language model. - - In chat-based interactions, language models may lose track of the required output format - as the conversation context grows longer. This method generates a concise reminder of - the expected output structure that can be included in user messages. - - Args: - signature (Type[Signature]): The DSPy signature defining the expected input/output fields. - - Returns: - str: A simplified description of the required output format. - - Note: - This is a more lightweight version of `format_field_structure` specifically designed - for inline reminders within chat messages. - """ + def user_message_output_requirements(self, signature: type[Signature]) -> str | None: + if getattr(signature, "__dspy_native_fc__", False): + return "Think step-by-step about what to do next, then call the appropriate tool." def type_info(v): if v.annotation is not str: @@ -209,6 +208,9 @@ def format_assistant_message_content( return assistant_message_content def parse(self, signature: type[Signature], completion: str) -> dict[str, Any]: + if getattr(signature, "__dspy_native_fc__", False): + return self._parse_native_fc(signature, completion) + sections = [(None, [])] for line in completion.splitlines(): @@ -245,11 +247,23 @@ def parse(self, signature: type[Signature], completion: str) -> dict[str, Any]: return fields + def _parse_native_fc(self, signature: type[Signature], completion: str) -> dict[str, Any]: + """Parse native FC response: assign free-form text to the single str output field.""" + str_fields = [k for k, v in signature.output_fields.items() if v.annotation is str] + if len(str_fields) == 1: + return {str_fields[0]: completion.strip()} + raise AdapterParseError( + adapter_name="ChatAdapter", + signature=signature, + lm_response=completion, + message="Native FC response with multiple output fields cannot be parsed without markers.", + ) + def format_field_with_value(self, fields_with_values: dict[FieldInfoWithName, Any]) -> str: """ Formats the values of the specified fields according to the field's DSPy type (input or output), annotation (e.g. str, int, etc.), and the type of the value itself. Joins the formatted values - into a single string, which is is a multiline string if there are multiple fields. + into a single string, which is a multiline string if there are multiple fields. Args: fields_with_values: A dictionary mapping information about a field to its corresponding diff --git a/dspy/adapters/types/__init__.py b/dspy/adapters/types/__init__.py index 5ec8043021..9709af6e3b 100644 --- a/dspy/adapters/types/__init__.py +++ b/dspy/adapters/types/__init__.py @@ -2,9 +2,20 @@ from dspy.adapters.types.base_type import Type from dspy.adapters.types.code import Code from dspy.adapters.types.file import File -from dspy.adapters.types.history import History +from dspy.adapters.types.history import ( + ActionEvent, + History, + HistoryEvent, + InputEvent, + LegacyEvent, + Observation, + OutputEvent, +) from dspy.adapters.types.image import Image from dspy.adapters.types.reasoning import Reasoning from dspy.adapters.types.tool import Tool, ToolCalls -__all__ = ["History", "Image", "Audio", "File", "Type", "Tool", "ToolCalls", "Code", "Reasoning"] +__all__ = [ + "ActionEvent", "History", "HistoryEvent", "InputEvent", "LegacyEvent", "Observation", "OutputEvent", + "Image", "Audio", "File", "Type", "Tool", "ToolCalls", "Code", "Reasoning", +] diff --git a/dspy/adapters/types/history.py b/dspy/adapters/types/history.py index 6dda4f9b7c..f75d37efa3 100644 --- a/dspy/adapters/types/history.py +++ b/dspy/adapters/types/history.py @@ -1,68 +1,123 @@ -from typing import Any +from typing import Annotated, Any, Callable, Literal import pydantic +from dspy.adapters.types.tool import ToolCalls -class History(pydantic.BaseModel): - """Class representing the conversation history. - The conversation history is a list of messages, each message entity should have keys from the associated signature. - For example, if you have the following signature: +class InputEvent(pydantic.BaseModel): + event: Literal["input"] = "input" + inputs: dict[str, Any] + - ``` - class MySignature(dspy.Signature): - question: str = dspy.InputField() - history: dspy.History = dspy.InputField() - answer: str = dspy.OutputField() - ``` +class Observation(pydantic.BaseModel): + """A single tool observation with an optional error flag.""" + value: Any + is_error: bool = False - Then the history should be a list of dictionaries with keys "question" and "answer". - Examples: - ``` - import dspy +class ActionEvent(pydantic.BaseModel): + event: Literal["action"] = "action" + thought: str | None = None + tool_calls: ToolCalls | None = None + observations: list[Observation] = [] - dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) - class MySignature(dspy.Signature): - question: str = dspy.InputField() - history: dspy.History = dspy.InputField() - answer: str = dspy.OutputField() +class OutputEvent(pydantic.BaseModel): + event: Literal["output"] = "output" + outputs: dict[str, Any] - history = dspy.History( - messages=[ - {"question": "What is the capital of France?", "answer": "Paris"}, - {"question": "What is the capital of Germany?", "answer": "Berlin"}, - ] - ) - predict = dspy.Predict(MySignature) - outputs = predict(question="What is the capital of France?", history=history) - ``` +class LegacyEvent(pydantic.BaseModel): + """Backward-compat wrapper for plain dict messages from old History format.""" + event: Literal["legacy"] = "legacy" + data: dict[str, Any] - Example of capturing the conversation history: - ``` - import dspy - dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) +HistoryEvent = Annotated[InputEvent | ActionEvent | OutputEvent | LegacyEvent, pydantic.Field(discriminator="event")] - class MySignature(dspy.Signature): - question: str = dspy.InputField() - history: dspy.History = dspy.InputField() - answer: str = dspy.OutputField() - predict = dspy.Predict(MySignature) - outputs = predict(question="What is the capital of France?") - history = dspy.History(messages=[{"question": "What is the capital of France?", **outputs}]) - outputs_with_history = predict(question="Are you sure?", history=history) - ``` - """ +class History(pydantic.BaseModel): + """Conversation history with typed semantic events and pluggable compaction.""" - messages: list[dict[str, Any]] + messages: list[HistoryEvent] model_config = pydantic.ConfigDict( - frozen=True, str_strip_whitespace=True, - validate_assignment=True, extra="forbid", ) + + @pydantic.model_validator(mode="before") + @classmethod + def _coerce_legacy_messages(cls, data: Any) -> Any: + if not isinstance(data, dict) or "messages" not in data: + return data + raw = data["messages"] + if not isinstance(raw, list): + return data + coerced = [] + for msg in raw: + if not isinstance(msg, dict) or "event" in msg: + coerced.append(msg) + continue + # Legacy plain dict — wrap as LegacyEvent so it passes validation + coerced.append({"event": "legacy", "data": msg}) + return {**data, "messages": coerced} + + @pydantic.model_serializer(mode="wrap") + def _serialize_legacy_messages(self, handler: Any) -> dict[str, Any]: + data = handler(self) + if "messages" in data: + serialized = [] + for msg in data["messages"]: + if isinstance(msg, dict) and msg.get("event") == "legacy": + serialized.append(msg["data"]) + else: + serialized.append(msg) + data["messages"] = serialized + return data + + def __init__(self, *args: Any, compact_fn: Callable[["History"], None] | None = None, **kwargs: Any): + super().__init__(*args, **kwargs) + object.__setattr__(self, "_compact_fn", compact_fn) + + def compact_if_needed(self) -> None: + fn = getattr(self, "_compact_fn", None) + if fn is not None: + fn(self) + + def append_input(self, inputs: dict[str, Any]) -> None: + self.messages.append(InputEvent(inputs=inputs)) + + def append_action(self, *, thought: str, tool_calls: ToolCalls | None, observations: list[Observation]) -> None: + self.messages.append(ActionEvent(thought=thought, tool_calls=tool_calls, observations=observations)) + + def append_output(self, outputs: dict[str, Any]) -> None: + self.messages.append(OutputEvent(outputs=outputs)) + + def has_open_episode(self) -> bool: + last_boundary = None + for m in self.messages: + if isinstance(m, InputEvent): + last_boundary = "input" + elif isinstance(m, OutputEvent): + last_boundary = "output" + return last_boundary == "input" + + +def truncate_oldest_actions(history: History, *, max_tokens: int = 200_000, keep_n: int = 3) -> None: + est = len(str(history.messages)) // 4 + if est <= max_tokens: + return + actions = [(i, m) for i, m in enumerate(history.messages) if isinstance(m, ActionEvent)] + to_drop = len(actions) - keep_n + if to_drop <= 0: + return + drop_indices = {i for i, _ in actions[:to_drop]} + history.messages[:] = [m for i, m in enumerate(history.messages) if i not in drop_indices] + + +def make_truncate_oldest_actions(max_tokens: int = 200_000, keep_n: int = 3) -> Callable[[History], None]: + def _compact(history: History) -> None: + truncate_oldest_actions(history, max_tokens=max_tokens, keep_n=keep_n) + return _compact diff --git a/dspy/adapters/types/tool.py b/dspy/adapters/types/tool.py index e6deb9b7c2..59fea3574f 100644 --- a/dspy/adapters/types/tool.py +++ b/dspy/adapters/types/tool.py @@ -1,13 +1,16 @@ import asyncio import inspect +import re from typing import TYPE_CHECKING, Any, Callable, get_origin, get_type_hints +import json_repair import pydantic from jsonschema import ValidationError, validate from pydantic import BaseModel, TypeAdapter, create_model from dspy.adapters.types.base_type import Type from dspy.dsp.utils.settings import settings +from dspy.predict.parameter import Parameter from dspy.utils.callback import with_callbacks if TYPE_CHECKING: @@ -15,9 +18,15 @@ from langchain.tools import BaseTool _TYPE_MAPPING = {"string": str, "integer": int, "number": float, "boolean": bool, "array": list, "object": dict} +_TOOL_NAME_RE = re.compile(r"[^a-zA-Z0-9_-]") -class Tool(Type): +def _sanitize_tool_name(name: str) -> str: + """Sanitize tool name to match OpenAI's ^[a-zA-Z0-9_-]+$ pattern.""" + return _TOOL_NAME_RE.sub("_", name) + + +class Tool(Type, Parameter): """Tool class. This class is used to simplify the creation of tools for tool calling (function calling) in LLMs. Only supports @@ -110,12 +119,23 @@ def _parse_function(self, func: Callable, arg_desc: dict[str, str] | None = None if arg_desc and k in arg_desc: args[k]["description"] = arg_desc[k] - self.name = self.name or name + self.name = _sanitize_tool_name(self.name or name) self.desc = self.desc or desc self.args = self.args if self.args is not None else args self.arg_types = self.arg_types if self.arg_types is not None else arg_types self.has_kwargs = any(param.kind == param.VAR_KEYWORD for param in sig.parameters.values()) + def dump_state(self, json_mode=True): + return {"name": self.name, "desc": self.desc, "args": self.args} + + def load_state(self, state): + if "desc" in state: + self.desc = state["desc"] + if "name" in state: + self.name = state["name"] + if "args" in state: + self.args = state["args"] + def _validate_and_parse_args(self, **kwargs): # Validate the args value comply to the json schema. for k, v in kwargs.items(): @@ -263,15 +283,19 @@ class ToolCalls(Type): class ToolCall(Type): name: str args: dict[str, Any] + id: str | None = None def format(self): - return { + d = { "type": "function", "function": { "name": self.name, "arguments": self.args, }, } + if self.id is not None: + d["id"] = self.id + return d def execute(self, functions: dict[str, Any] | list[Tool] | None = None) -> Any: """Execute this individual tool call and return its result. @@ -340,8 +364,7 @@ def from_dict_list(cls, tool_calls_dicts: list[dict[str, Any]]) -> "ToolCalls": tool_calls = ToolCalls.from_dict_list(tool_calls_dict) ``` """ - tool_calls = [cls.ToolCall(**item) for item in tool_calls_dicts] - return cls(tool_calls=tool_calls) + return cls.model_validate(tool_calls_dicts) @classmethod def description(cls) -> str: @@ -356,30 +379,59 @@ def format(self) -> list[dict[str, Any]]: "tool_calls": [tool_call.format() for tool_call in self.tool_calls], } + @staticmethod + def _normalize_openai_tool_call(item: dict) -> dict: + """Normalize {type:'function', function:{name, arguments}} → {name, args}.""" + if hasattr(item, "model_dump"): + item = item.model_dump() + if not isinstance(item, dict): + return item + if "type" in item and item["type"] == "function" and "function" in item: + fn = item["function"] + args = fn.get("arguments", {}) + if isinstance(args, str): + args = json_repair.loads(args) + normalized = {"name": fn["name"], "args": args} + if "id" in item: + normalized["id"] = item["id"] + return normalized + if item.get("type") == "function_call" and "name" in item: + args = item.get("arguments", {}) + if isinstance(args, str): + args = json_repair.loads(args) + normalized = {"name": item["name"], "args": args} + call_id = item.get("call_id") or item.get("id") + if call_id: + normalized["id"] = call_id + return normalized + return item + @pydantic.model_validator(mode="before") @classmethod def validate_input(cls, data: Any): if isinstance(data, cls): return data - # Handle case where data is a list of dicts with "name" and "args" keys - if isinstance(data, list) and all( - isinstance(item, dict) and "name" in item and "args" in item for item in data - ): - return {"tool_calls": [cls.ToolCall(**item) for item in data]} + # Handle case where data is a list of dicts + if isinstance(data, list): + normalized = [cls._normalize_openai_tool_call(item) for item in data] + if all("name" in item and "args" in item for item in normalized): + return {"tool_calls": [cls.ToolCall(**item) for item in normalized]} # Handle case where data is a dict elif isinstance(data, dict): if "tool_calls" in data: - # Handle case where data is a dict with "tool_calls" key tool_calls_data = data["tool_calls"] if isinstance(tool_calls_data, list): + normalized = [ + cls._normalize_openai_tool_call(item) if isinstance(item, dict) else item + for item in tool_calls_data + ] return { "tool_calls": [ - cls.ToolCall(**item) if isinstance(item, dict) else item for item in tool_calls_data + cls.ToolCall(**item) if isinstance(item, dict) else item for item in normalized ] } elif "name" in data and "args" in data: - # Handle case where data is a dict with "name" and "args" keys return {"tool_calls": [cls.ToolCall(**data)]} raise ValueError(f"Received invalid value for `dspy.ToolCalls`: {data}") @@ -393,7 +445,7 @@ def _resolve_json_schema_reference(schema: dict) -> dict: return schema def resolve_refs(obj: Any) -> Any: - if not isinstance(obj, (dict, list)): + if not isinstance(obj, dict | list): return obj if isinstance(obj, dict): if "$ref" in obj: diff --git a/dspy/clients/base_lm.py b/dspy/clients/base_lm.py index 3770c4c2bd..26f0436824 100644 --- a/dspy/clients/base_lm.py +++ b/dspy/clients/base_lm.py @@ -261,6 +261,17 @@ def _process_completion(self, response, merged_kwargs): output = {} output["text"] = c.message.content if hasattr(c, "message") else c["text"] + # Extract reasoning from model_extra when content is None. + # Reasoning models (e.g. gpt-oss-120b via Groq) return content=None + # with reasoning in model_extra; surface it so adapters can use it + # (e.g. as next_thought in ReActV2 native FC). + if output["text"] is None and hasattr(c, "message"): + model_extra = getattr(c.message, "model_extra", None) + if model_extra and isinstance(model_extra, dict): + reasoning = model_extra.get("reasoning") + if reasoning: + output["text"] = reasoning + if hasattr(c, "message") and hasattr(c.message, "reasoning_content") and c.message.reasoning_content: output["reasoning_content"] = c.message.reasoning_content diff --git a/dspy/clients/lm.py b/dspy/clients/lm.py index 3921cc889c..45501c47fd 100644 --- a/dspy/clients/lm.py +++ b/dspy/clients/lm.py @@ -516,8 +516,9 @@ def _convert_chat_request_to_responses_request(request: dict[str, Any]): """ request = dict(request) if "messages" in request: - content_blocks = [] + input_items = [] for msg in request.pop("messages"): + content_blocks = [] c = msg.get("content") if isinstance(c, str): content_blocks.append({"type": "input_text", "text": c}) @@ -525,7 +526,8 @@ def _convert_chat_request_to_responses_request(request: dict[str, Any]): # Convert each content item from Chat API format to Responses API format for item in c: content_blocks.append(_convert_content_item_to_responses_format(item)) - request["input"] = [{"role": msg.get("role", "user"), "content": content_blocks}] + input_items.append({"role": msg.get("role", "user"), "content": content_blocks}) + request["input"] = input_items # Convert `reasoning_effort` to reasoning format supported by the Responses API if "reasoning_effort" in request: effort = request.pop("reasoning_effort") diff --git a/dspy/predict/__init__.py b/dspy/predict/__init__.py index 906ef90ae9..e3ad562a6a 100644 --- a/dspy/predict/__init__.py +++ b/dspy/predict/__init__.py @@ -8,6 +8,7 @@ from dspy.predict.predict import Predict from dspy.predict.program_of_thought import ProgramOfThought from dspy.predict.react import ReAct, Tool +from dspy.predict.reactv2 import ReActV2 from dspy.predict.refine import Refine from dspy.predict.rlm import RLM @@ -21,6 +22,7 @@ "Predict", "ProgramOfThought", "ReAct", + "ReActV2", "Refine", "RLM", "Tool", diff --git a/dspy/predict/reactv2.py b/dspy/predict/reactv2.py new file mode 100644 index 0000000000..0545291ba4 --- /dev/null +++ b/dspy/predict/reactv2.py @@ -0,0 +1,226 @@ +import logging +import traceback +from typing import TYPE_CHECKING, Callable + +import dspy +from dspy.adapters.types.history import ActionEvent, InputEvent, Observation +from dspy.adapters.types.tool import Tool, ToolCalls +from dspy.primitives.module import Module +from dspy.signatures.signature import ensure_signature +from dspy.utils.exceptions import AdapterParseError, ContextWindowExceededError + +logger = logging.getLogger(__name__) + +if TYPE_CHECKING: + from dspy.signatures.signature import Signature + +_ANNOTATION_TO_JSON_TYPE = { + str: "string", + int: "integer", + float: "number", + bool: "boolean", + list: "array", +} + + +def _build_submit_tool(signature: type["Signature"]) -> Tool: + outputs = ", ".join([f"`{k}`" for k in signature.output_fields.keys()]) + output_args = {} + output_arg_types = {} + for k, v in signature.output_fields.items(): + annotation = v.annotation if hasattr(v, "annotation") else str + json_type = _ANNOTATION_TO_JSON_TYPE.get(annotation, "string") + output_args[k] = {"type": json_type} + output_arg_types[k] = annotation + + return Tool( + func=lambda **kwargs: kwargs, + name="submit", + desc=f"Call this tool to end the task and return your final answer. Takes: {outputs}.", + args=output_args, + arg_types=output_arg_types, + ) + + +class ReActV2(Module): + def __init__(self, signature: type["Signature"] | str, tools: list[Callable], max_iters: int = 20): + super().__init__() + self.signature = signature = ensure_signature(signature) + self.max_iters = max_iters + + tools = [t if isinstance(t, Tool) else Tool(t) for t in tools] + tools = {tool.name: tool for tool in tools} + tools["submit"] = _build_submit_tool(signature) + self.tools = tools + + react_signature = ( + dspy.Signature({**signature.input_fields}, self._build_instructions()) + .append("history", dspy.InputField(), type_=dspy.History) + .append("tools", dspy.InputField(), type_=list[dspy.Tool]) + .append("next_thought", dspy.OutputField(), type_=str) + .append("tool_calls", dspy.OutputField(), type_=dspy.ToolCalls) + ) + + # Extract fallback: dedicated LM call to extract answer from history + # (like v1's self.extract, fires only when submit fails in _forced_submit) + extract_signature = dspy.Signature( + {**signature.input_fields, **signature.output_fields}, + signature.instructions, + ).append("trajectory", dspy.InputField(desc="The agent's history of thoughts, actions, and observations"), type_=str) + + self.react = dspy.Predict(react_signature) + self.extract = dspy.ChainOfThought(extract_signature) + + def _build_instructions(self): + """Build the instruction string from current signature and tools.""" + inputs = ", ".join([f"`{k}`" for k in self.signature.input_fields.keys()]) + outputs = ", ".join([f"`{k}`" for k in self.signature.output_fields.keys()]) + instr = [f"{self.signature.instructions}\n"] if self.signature.instructions else [] + + instr.extend([ + f"You are an Agent. Given {inputs}, use tools to produce {outputs}.", + "Each turn: think, then call one or more tools. After each tool call you receive an observation.", + "When you have enough information to answer, call `submit` to finish. Do not keep using tools after you have the answer.\n", + "Available tools:\n", + ]) + + for idx, tool in enumerate(self.tools.values()): + instr.append(f"({idx + 1}) {tool}") + + return "\n".join(instr) + + def _rebuild_instructions(self): + """Regenerate the instruction string from current tool descs. + + Called after GEPA updates tool.desc so that both text-mode prompts + and native FC schemas reflect the optimized descriptions. + """ + base_instr = self._build_instructions() + self.react.signature = self.react.signature.with_instructions(base_instr) + self.extract.predict.signature = self.extract.predict.signature.with_instructions(self.signature.instructions) + + def forward(self, **input_args): + # Callers can pass a History with a compact_fn; compaction triggers on ContextWindowExceededError. + history = input_args.pop("history", dspy.History(messages=[])) + max_iters = input_args.pop("max_iters", self.max_iters) + tool_list = list(self.tools.values()) + + if not history.has_open_episode(): + history.append_input(input_args) + + break_reason = None + for idx in range(max_iters): + try: + pred: dspy.Prediction = self.react(history=history, tools=tool_list, **input_args) + except ContextWindowExceededError: + history.compact_if_needed() + try: + pred = self.react(history=history, tools=tool_list, **input_args) + except ContextWindowExceededError: + logger.warning("Context window exceeded after compaction, ending loop.") + break_reason = "context_overflow" + break + except (AdapterParseError, ValueError) as err: + logger.warning(f"Agent iteration {idx} failed: {_fmt_exc(err)}") + break_reason = "parse_error" + break + + if pred.tool_calls is None or not pred.tool_calls.tool_calls: + logger.warning("Agent returned no tool calls, ending loop.") + break_reason = "no_tool_calls" + break + + observations: list[Observation] = [] + for tool_call in pred.tool_calls.tool_calls: + tool = self.tools.get(tool_call.name) + if tool is None: + observations.append(Observation(value=f"Unknown tool: {tool_call.name}", is_error=True)) + continue + try: + result = tool(**tool_call.args) + observations.append(Observation(value=result, is_error=False)) + except Exception as err: + observations.append(Observation(value=f"Execution error in {tool_call.name}: {_fmt_exc(err)}", is_error=True)) + + history.append_action( + thought=pred.next_thought, + tool_calls=pred.tool_calls, + observations=observations, + ) + + for tool_call, obs in zip(pred.tool_calls.tool_calls, observations, strict=True): + if tool_call.name == "submit" and not obs.is_error: + history.append_output(obs.value) + return dspy.Prediction(history=history, termination_reason="submit", **obs.value) + + # Forced submit: ask the model to submit one more time + return self._forced_submit(history, input_args, break_reason=break_reason) + + def _forced_submit(self, history, input_args, break_reason=None): + tool_list = list(self.tools.values()) + + # Tier 1: Re-use self.react with tool_choice forced to submit. + adapter = dspy.settings.adapter + native_fc = getattr(adapter, "use_native_function_calling", False) if adapter else False + + call_config = {} + if native_fc: + call_config["tool_choice"] = {"type": "function", "function": {"name": "submit"}} + + try: + pred = self.react(history=history, tools=tool_list, config=call_config, **input_args) + except Exception as err: + logger.debug(f"Forced submit tier 1 (react) failed: {_fmt_exc(err)}") + pred = None + + if pred and pred.tool_calls and pred.tool_calls.tool_calls: + for tool_call in pred.tool_calls.tool_calls: + if tool_call.name == "submit": + try: + result = self.tools["submit"](**tool_call.args) + history.append_action( + thought=pred.next_thought, + tool_calls=ToolCalls(tool_calls=[tool_call]), + observations=[Observation(value=result, is_error=False)], + ) + history.append_output(result) + return dspy.Prediction(history=history, termination_reason="forced_submit", **result) + except Exception as err: + logger.debug(f"Forced submit tool execution failed: {_fmt_exc(err)}") + + # Tier 2: Extract fallback via ChainOfThought. + try: + trajectory_text = self._render_history_as_text(history) + extract = self.extract(trajectory=trajectory_text, **input_args) + result = {k: getattr(extract, k) for k in self.signature.output_fields if hasattr(extract, k)} + if any(v is not None for v in result.values()): + history.append_output(result) + return dspy.Prediction(history=history, termination_reason="extract", **result) + except Exception as err: + logger.debug(f"Forced submit tier 2 (extract) failed: {_fmt_exc(err)}") + + return dspy.Prediction(history=history, termination_reason=break_reason or "failed") + + @staticmethod + def _render_history_as_text(history) -> str: + lines = [] + for event in history.messages: + if isinstance(event, InputEvent): + for k, v in event.inputs.items(): + lines.append(f"[Input] {k}: {v}") + elif isinstance(event, ActionEvent): + if event.thought: + lines.append(f"[Thought] {event.thought}") + if event.tool_calls and hasattr(event.tool_calls, "tool_calls"): + for i, tc in enumerate(event.tool_calls.tool_calls): + args_str = ", ".join(f"{k}={v!r}" for k, v in (tc.args or {}).items()) + lines.append(f"[Action] {tc.name}({args_str})") + if i < len(event.observations): + obs_val, was_err = event.observations[i].value, event.observations[i].is_error + prefix = "[Error]" if was_err else "[Observation]" + lines.append(f"{prefix} {obs_val}") + return "\n".join(lines) + + +def _fmt_exc(err: BaseException, *, limit: int = 5) -> str: + return "\n" + "".join(traceback.format_exception(type(err), err, err.__traceback__, limit=limit)).strip() diff --git a/dspy/primitives/base_module.py b/dspy/primitives/base_module.py index 2a44e1416e..e01ed7a489 100644 --- a/dspy/primitives/base_module.py +++ b/dspy/primitives/base_module.py @@ -160,6 +160,9 @@ def load_state(self, state, *, allow_unsafe_lm_state=False): from dspy.predict.predict import Predict for name, param in self.named_parameters(): + if name not in state: + logger.debug(f"Skipping parameter '{name}' not found in saved state.") + continue if isinstance(param, Predict): param.load_state(state[name], allow_unsafe_lm_state=allow_unsafe_lm_state) else: diff --git a/dspy/teleprompt/gepa/gepa.py b/dspy/teleprompt/gepa/gepa.py index 2fdc0494de..b8acbf048d 100644 --- a/dspy/teleprompt/gepa/gepa.py +++ b/dspy/teleprompt/gepa/gepa.py @@ -107,27 +107,27 @@ def best_candidate(self) -> dict[str, str]: @property def highest_score_achieved_per_val_task(self) -> list[float]: return [ - self.val_subscores[list(self.per_val_instance_best_candidates[val_idx])[0]][val_idx] + self.val_subscores[next(iter(self.per_val_instance_best_candidates[val_idx]))][val_idx] for val_idx in range(len(self.val_subscores[0])) ] def to_dict(self) -> dict[str, Any]: - cands = [{k: v for k, v in cand.items()} for cand in self.candidates] - - return dict( - candidates=cands, - parents=self.parents, - val_aggregate_scores=self.val_aggregate_scores, - best_outputs_valset=self.best_outputs_valset, - val_subscores=self.val_subscores, - per_val_instance_best_candidates=[list(s) for s in self.per_val_instance_best_candidates], - discovery_eval_counts=self.discovery_eval_counts, - total_metric_calls=self.total_metric_calls, - num_full_val_evals=self.num_full_val_evals, - log_dir=self.log_dir, - seed=self.seed, - best_idx=self.best_idx, - ) + cands = [dict(cand) for cand in self.candidates] + + return { + "candidates": cands, + "parents": self.parents, + "val_aggregate_scores": self.val_aggregate_scores, + "best_outputs_valset": self.best_outputs_valset, + "val_subscores": self.val_subscores, + "per_val_instance_best_candidates": [list(s) for s in self.per_val_instance_best_candidates], + "discovery_eval_counts": self.discovery_eval_counts, + "total_metric_calls": self.total_metric_calls, + "num_full_val_evals": self.num_full_val_evals, + "log_dir": self.log_dir, + "seed": self.seed, + "best_idx": self.best_idx, + } @staticmethod def from_gepa_result(gepa_result: "GEPAResult", adapter: "DspyAdapter") -> "DspyGEPAResult": @@ -439,27 +439,27 @@ def auto_budget( if full_eval_steps < 1: raise ValueError("full_eval_steps must be >= 1.") - V = valset_size - N = num_trials - M = minibatch_size + val_count = valset_size + trial_count = num_trials + minibatch_count = minibatch_size m = full_eval_steps # Initial full evaluation on the default program - total = V + total = val_count # Assume upto 5 trials for bootstrapping each candidate total += num_candidates * 5 - # N minibatch evaluations - total += N * M - if N == 0: + # Minibatch evaluations + total += trial_count * minibatch_count + if trial_count == 0: return total # no periodic/full evals inside the loop - # Periodic full evals occur when trial_num % (m+1) == 0, where trial_num runs 2..N+1 - periodic_fulls = (N + 1) // (m) + 1 - # If 1 <= N < m, the code triggers one final full eval at the end - extra_final = 1 if N < m else 0 + # Periodic full evals occur when trial_num % (m+1) == 0. + periodic_fulls = (trial_count + 1) // m + 1 + # If 1 <= trial_count < m, the code triggers one final full eval at the end + extra_final = 1 if trial_count < m else 0 - total += (periodic_fulls + extra_final) * V + total += (periodic_fulls + extra_final) * val_count return total def compile( @@ -539,7 +539,7 @@ def feedback_fn( o["feedback"] = f"This trajectory got a score of {o['score']}." return o else: - return dict(score=o, feedback=f"This trajectory got a score of {o}.") + return {"score": o, "feedback": f"This trajectory got a score of {o}."} return feedback_fn @@ -561,7 +561,25 @@ def feedback_fn( ) # Build the seed candidate: map each predictor name to its current instruction - seed_candidate = {name: pred.signature.instructions for name, pred in student.named_predictors()} + named_predictors = list(student.named_predictors()) + seed_candidate = {name: pred.signature.instructions for name, pred in named_predictors} + + # Also discover tools and add their descs as optimizable components + from dspy.adapters.types.tool import Tool as DspyTool + + tool_params = [ + (name, param) + for name, param in student.named_parameters() + if isinstance(param, DspyTool) and param.name != "submit" + ] + for name, param in tool_params: + seed_candidate[name] = param.desc or "" + + # Add feedback entries for tool components, reusing the parent predictor's feedback + parent_pred_name = named_predictors[0][0] if named_predictors else None + if parent_pred_name and parent_pred_name in feedback_map: + for name, _ in tool_params: + feedback_map[name] = feedback_map[parent_pred_name] gepa_result: GEPAResult = optimize( seed_candidate=seed_candidate, diff --git a/dspy/teleprompt/gepa/gepa_utils.py b/dspy/teleprompt/gepa/gepa_utils.py index dae7157feb..d5aeb4e837 100644 --- a/dspy/teleprompt/gepa/gepa_utils.py +++ b/dspy/teleprompt/gepa/gepa_utils.py @@ -134,12 +134,25 @@ def propose_new_texts( return results def build_program(self, candidate: dict[str, str]): + from dspy.adapters.types.tool import Tool as DspyTool + new_prog = self.student.deepcopy() for name, pred in new_prog.named_predictors(): if name in candidate: pred.signature = pred.signature.with_instructions(candidate[name]) + # Apply optimized tool descriptions + for name, param in new_prog.named_parameters(): + if isinstance(param, DspyTool) and name in candidate: + param.desc = candidate[name] + + # Rebuild instruction strings for any module that has tools + # This ensures the text-mode prompt reflects the optimized tool descs + for _mod_name, mod in new_prog.named_sub_modules(): + if hasattr(mod, "tools") and hasattr(mod, "_rebuild_instructions"): + mod._rebuild_instructions() + return new_prog def evaluate(self, batch, candidate, capture_traces=False): @@ -200,16 +213,22 @@ def make_reflective_dataset( ) -> dict[str, list[ReflectiveExample]]: program = self.build_program(candidate) + # Build predictor lookup once + predictors = dict(program.named_predictors()) + ret_d: dict[str, list[ReflectiveExample]] = {} for pred_name in components_to_update: - # Find the predictor object - module = None - for name, m in program.named_predictors(): - if name == pred_name: - module = m - break - assert module is not None, f"Predictor not found: {pred_name}" + if pred_name in predictors: + module = predictors[pred_name] + else: + # This is a tool component (e.g. tools['add']). + # Use the first predictor's traces — tool descriptions affect how + # the parent predictor behaves, so its traces carry the relevant signal. + module = next(iter(predictors.values()), None) + if module is None: + logger.warning(f" No predictor found to use as parent for tool component {pred_name}") + continue # Create reflective examples from traces items: list[ReflectiveExample] = [] diff --git a/dspy/utils/inspect_history.py b/dspy/utils/inspect_history.py index 46aebad1cc..c783dd241b 100644 --- a/dspy/utils/inspect_history.py +++ b/dspy/utils/inspect_history.py @@ -44,10 +44,13 @@ def pretty_print_history(history: list[dict[str, Any]], n: int = 1, file: TextIO print(_blue(f"[{timestamp}]", use_colors=use_colors), file=out) for msg in messages: - print(_red(f"{msg['role'].capitalize()} message:", use_colors=use_colors), file=out) - if isinstance(msg["content"], str): + role_label = f"{msg['role'].capitalize()} message:" + if msg["role"] == "tool" and msg.get("tool_call_id"): + role_label = f"Tool message: (tool_call_id={msg['tool_call_id']})" + print(_red(role_label, use_colors=use_colors), file=out) + if isinstance(msg.get("content"), str): print(msg["content"].strip(), file=out) - else: + elif msg.get("content") is not None: if isinstance(msg["content"], list): for c in msg["content"]: if c["type"] == "text": @@ -75,6 +78,10 @@ def pretty_print_history(history: list[dict[str, Any]], n: int = 1, file: TextIO file_data = file_info.get("file_data", "") file_str = f"" print(_blue(file_str.strip(), use_colors=use_colors), file=out) + if msg.get("tool_calls"): + print(_red("Tool calls:", use_colors=use_colors), file=out) + for tool_call in msg["tool_calls"]: + print(_green(f"{tool_call['function']['name']}: {tool_call['function']['arguments']}", use_colors=use_colors), file=out) print("\n", file=out) if isinstance(outputs[0], dict): diff --git a/tests/adapters/test_chat_adapter.py b/tests/adapters/test_chat_adapter.py index 86c662b3ba..ed78a9a6d3 100644 --- a/tests/adapters/test_chat_adapter.py +++ b/tests/adapters/test_chat_adapter.py @@ -562,7 +562,13 @@ def get_weather(city: str) -> str: ) assert result[0]["tool_calls"] == dspy.ToolCalls( - tool_calls=[dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Paris"})] + tool_calls=[ + dspy.ToolCalls.ToolCall( + name="get_weather", + args={"city": "Paris"}, + id="call_pQm8ajtSMxgA0nrzK2ivFmxG", + ) + ] ) # `answer` is not present, so we set it to None assert result[0]["answer"] is None @@ -726,7 +732,7 @@ class MySignature(dspy.Signature): expected_system_message = """Your input fields are: 1. `question` (str): Your output fields are: -1. `answers` (list[str]): +1. `answers` (list[str]):\x20 2. `scores` (list[float]): All interactions will be structured in the following way, with the appropriate values filled in. @@ -740,7 +746,7 @@ class MySignature(dspy.Signature): {scores} # note: the value you produce must adhere to the JSON schema: {"type": "array", "items": {"type": "number"}} [[ ## completed ## ]] -In adhering to this structure, your objective is: +In adhering to this structure, your objective is:\x20 Answer the question with multiple answers and scores""" assert system_message == expected_system_message diff --git a/tests/adapters/test_json_adapter.py b/tests/adapters/test_json_adapter.py index f376831d98..c96b397fff 100644 --- a/tests/adapters/test_json_adapter.py +++ b/tests/adapters/test_json_adapter.py @@ -833,7 +833,13 @@ def get_weather(city: str) -> str: ) assert result[0]["tool_calls"] == dspy.ToolCalls( - tool_calls=[dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Paris"})] + tool_calls=[ + dspy.ToolCalls.ToolCall( + name="get_weather", + args={"city": "Paris"}, + id="call_pQm8ajtSMxgA0nrzK2ivFmxG", + ) + ] ) # `answer` is not present, so we set it to None assert result[0]["answer"] is None @@ -998,7 +1004,7 @@ class MySignature(dspy.Signature): expected_system_message = """Your input fields are: 1. `question` (str): Your output fields are: -1. `answers` (list[str]): +1. `answers` (list[str]):\x20 2. `scores` (list[float]): All interactions will be structured in the following way, with the appropriate values filled in. @@ -1013,6 +1019,6 @@ class MySignature(dspy.Signature): "answers": "{answers} # note: the value you produce must adhere to the JSON schema: {\\"type\\": \\"array\\", \\"items\\": {\\"type\\": \\"string\\"}}", "scores": "{scores} # note: the value you produce must adhere to the JSON schema: {\\"type\\": \\"array\\", \\"items\\": {\\"type\\": \\"number\\"}}" } -In adhering to this structure, your objective is: +In adhering to this structure, your objective is:\x20 Answer the question with multiple answers and scores""" assert system_message == expected_system_message diff --git a/tests/adapters/test_tool.py b/tests/adapters/test_tool.py index cfcffe0947..af20bbf33e 100644 --- a/tests/adapters/test_tool.py +++ b/tests/adapters/test_tool.py @@ -577,7 +577,7 @@ def get_pi(): tool_call4 = dspy.ToolCalls.ToolCall(name="nonexistent", args={}) try: tool_call4.execute(functions=tools) - assert False, "Should have raised ValueError" + raise AssertionError("Should have raised ValueError") except ValueError as e: assert "not found" in str(e) diff --git a/tests/predict/test_reactv2.py b/tests/predict/test_reactv2.py new file mode 100644 index 0000000000..fd3faa910a --- /dev/null +++ b/tests/predict/test_reactv2.py @@ -0,0 +1,367 @@ +import dspy +from dspy.adapters.types.history import ( + ActionEvent, + History, + InputEvent, + Observation, + OutputEvent, + truncate_oldest_actions, +) +from dspy.adapters.types.tool import Tool, ToolCalls, _sanitize_tool_name +from dspy.predict.reactv2 import ReActV2, _build_submit_tool +from dspy.utils.dummies import DummyLM + + +def _make_add_tool(): + def add(a: int, b: int) -> int: + """Add two numbers.""" + return a + b + return add + + +def test_submit_tool_returns_dict(): + """VAL-CORE-002: submit(answer='42') returns {'answer': '42'}.""" + sig = dspy.Signature("question -> answer") + submit = _build_submit_tool(sig) + result = submit(answer="42") + assert result == {"answer": "42"} + + +def test_submit_tool_args_match_output_fields(): + """Submit tool args match signature output fields.""" + sig = dspy.Signature("question -> answer, confidence") + submit = _build_submit_tool(sig) + assert "answer" in submit.args + assert "confidence" in submit.args + result = submit(answer="42", confidence="high") + assert result == {"answer": "42", "confidence": "high"} + + +def test_basic_forward_with_submit(): + """VAL-CORE-003: forward() terminates on submit, returns Prediction with history.""" + lm = DummyLM([ + {"next_thought": "I should add.", "tool_calls": [{"name": "add", "args": {"a": 1, "b": 2}}]}, + {"next_thought": "I have the answer.", "tool_calls": [{"name": "submit", "args": {"answer": "3"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + result = react(question="What is 1+2?") + assert result.answer == "3" + assert hasattr(result, "history") + # input + 2 actions + final = 4 events + assert len(result.history.messages) == 4 + assert isinstance(result.history.messages[0], InputEvent) + assert result.history.messages[0].event == "input" + assert isinstance(result.history.messages[-1], OutputEvent) + assert result.history.messages[-1].event == "output" + + +def test_max_iters_forced_submit(): + """VAL-CORE-004: model submits on final iteration within the loop.""" + lm = DummyLM([ + {"next_thought": "Adding.", "tool_calls": [{"name": "add", "args": {"a": 1, "b": 2}}]}, + {"next_thought": "Adding again.", "tool_calls": [{"name": "add", "args": {"a": 3, "b": 4}}]}, + # Submit on the 3rd (final) iteration within the loop: + {"next_thought": "Submitting.", "tool_calls": [{"name": "submit", "args": {"answer": "10"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + result = react(question="Add stuff", max_iters=3) + assert result.answer == "10" + + +def test_per_call_max_iters(): + """VAL-CORE-007: agent(question=..., max_iters=2) overrides instance default.""" + lm = DummyLM([ + {"next_thought": "Adding.", "tool_calls": [{"name": "add", "args": {"a": 1, "b": 2}}]}, + # Submit on the 2nd (final) iteration within the loop: + {"next_thought": "Submitting.", "tool_calls": [{"name": "submit", "args": {"answer": "3"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()], max_iters=20) + result = react(question="1+2", max_iters=2) + assert result.answer == "3" + + +def test_none_tool_calls_handled(): + """VAL-CORE-005: None tool_calls break loop gracefully.""" + lm = DummyLM([ + {"next_thought": "I dunno.", "tool_calls": []}, + # Forced submit - also returns empty so we get Prediction with just history + {"next_thought": "Still nothing.", "tool_calls": []}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + result = react(question="What?") + assert hasattr(result, "history") + + +def test_unknown_tool_returns_error_observation(): + """VAL-CORE-005: Unknown tool names return error observation, loop continues.""" + lm = DummyLM([ + {"next_thought": "Call fake.", "tool_calls": [{"name": "nonexistent", "args": {}}]}, + {"next_thought": "Now submit.", "tool_calls": [{"name": "submit", "args": {"answer": "ok"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + result = react(question="test") + assert result.answer == "ok" + actions = [m for m in result.history.messages if isinstance(m, ActionEvent)] + assert any("Unknown tool" in str(m.observations) for m in actions) + + +def test_tool_execution_error_caught(): + """VAL-CORE-006: Tool exceptions caught as error observations, loop continues.""" + def failing_tool(x: str) -> str: + """Always fails.""" + raise RuntimeError("boom") + + lm = DummyLM([ + {"next_thought": "Call it.", "tool_calls": [{"name": "failing_tool", "args": {"x": "hi"}}]}, + {"next_thought": "Submit anyway.", "tool_calls": [{"name": "submit", "args": {"answer": "recovered"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[failing_tool]) + result = react(question="test") + assert result.answer == "recovered" + actions = [m for m in result.history.messages if isinstance(m, ActionEvent)] + assert any("Execution error" in str(m.observations) for m in actions) + + +def test_reactv2_exported_from_dspy(): + """ReActV2 exported from dspy.""" + assert hasattr(dspy, "ReActV2") + assert dspy.ReActV2 is ReActV2 + + +# --- History semantic events tests (VAL-HIST-*) --- + +def test_history_events_input_action_final(): + """VAL-HIST-001: append methods create input/action/final events.""" + h = History(messages=[]) + h.append_input({"question": "hi"}) + h.append_action(thought="thinking", tool_calls=None, observations=[Observation(value="ok", is_error=False)]) + h.append_output({"answer": "bye"}) + assert [m.event for m in h.messages] == ["input", "action", "output"] + assert isinstance(h.messages[0], InputEvent) + assert h.messages[0].inputs["question"] == "hi" + assert isinstance(h.messages[1], ActionEvent) + assert h.messages[1].thought == "thinking" + assert h.messages[1].observations == [Observation(value="ok", is_error=False)] + assert isinstance(h.messages[2], OutputEvent) + assert h.messages[2].outputs["answer"] == "bye" + + +def test_has_open_episode(): + """VAL-HIST-002: has_open_episode tracks state correctly.""" + h = History(messages=[]) + assert not h.has_open_episode() + h.append_input({"q": "1"}) + assert h.has_open_episode() + h.append_action(thought="t", tool_calls=None, observations=[]) + assert h.has_open_episode() + h.append_output({"a": "1"}) + assert not h.has_open_episode() + + +def test_multi_turn_history_reuse(): + """VAL-HIST-003: History from forward #1 passed to forward #2.""" + lm = DummyLM([ + {"next_thought": "Add.", "tool_calls": [{"name": "add", "args": {"a": 1, "b": 2}}]}, + {"next_thought": "Submit.", "tool_calls": [{"name": "submit", "args": {"answer": "3"}}]}, + {"next_thought": "Add again.", "tool_calls": [{"name": "add", "args": {"a": 3, "b": 4}}]}, + {"next_thought": "Submit.", "tool_calls": [{"name": "submit", "args": {"answer": "7"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + r1 = react(question="1+2") + r2 = react(question="3+4", history=r1.history) + assert r2.answer == "7" + requests = [m for m in r2.history.messages if isinstance(m, InputEvent)] + assert len(requests) == 2 + + +# --- Compaction tests (VAL-COMPACT-*) --- + +def test_truncate_oldest_actions(): + """VAL-COMPACT-001: truncation preserves input event + most recent N actions.""" + h = History(messages=[ + InputEvent(inputs={"q": "x"}), + *[ActionEvent(thought=str(i)) for i in range(10)], + ]) + truncate_oldest_actions(h, max_tokens=0, keep_n=3) + actions = [m for m in h.messages if isinstance(m, ActionEvent)] + assert len(actions) == 3 + assert [a.thought for a in actions] == ["7", "8", "9"] + assert isinstance(h.messages[0], InputEvent) + + +def test_compaction_not_called_without_context_overflow(): + """VAL-COMPACT-002: compact_if_needed() is only called after context overflow.""" + calls = [] + + def track_compact(history): + calls.append(len(history.messages)) + + lm = DummyLM([ + {"next_thought": "Go.", "tool_calls": [{"name": "add", "args": {"a": 1, "b": 2}}]}, + {"next_thought": "Done.", "tool_calls": [{"name": "submit", "args": {"answer": "3"}}]}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + history = dspy.History(messages=[], compact_fn=track_compact) + react(question="1+2", history=history) + # No ContextWindowExceededError occurred, so there is nothing to compact. + assert len(calls) == 0 + + +# --- Native FC + format tests (VAL-FMT-*) --- + +def test_native_fc_prompt_format(): + """VAL-FMT-002: Native path has reasoning guidance, no [[ ## completed ## ]].""" + adapter = dspy.ChatAdapter(use_native_function_calling=True) + sig = ( + dspy.Signature({}, "Do the task.") + .append("question", dspy.InputField(), type_=str) + .append("tools", dspy.InputField(), type_=list[dspy.Tool]) + .append("next_thought", dspy.OutputField(), type_=str) + .append("tool_calls", dspy.OutputField(), type_=dspy.ToolCalls) + ) + # Simulate _call_preprocess: remove tool_calls and tools, tag native FC + processed = sig.delete("tool_calls").delete("tools") + processed.__dspy_native_fc__ = True + messages = adapter.format(processed, [], {"question": "hi"}) + system_msg = messages[0]["content"] + assert "[[ ## completed ## ]]" not in system_msg + assert "step-by-step" in system_msg.lower() or "reasoning" in system_msg.lower() or "tool" in system_msg.lower() + + +def test_non_native_prompt_format_unchanged(): + """VAL-FMT-001: Non-native path still has structured markers.""" + adapter = dspy.ChatAdapter() + sig = ( + dspy.Signature({}, "Do the task.") + .append("question", dspy.InputField(), type_=str) + .append("next_thought", dspy.OutputField(), type_=str) + .append("tool_calls", dspy.OutputField(), type_=dspy.ToolCalls) + ) + messages = adapter.format(sig, [], {"question": "hi"}) + system_msg = messages[0]["content"] + assert "[[ ## completed ## ]]" in system_msg + assert "[[ ## next_thought ## ]]" in system_msg + + +def test_non_native_history_action_uses_dspy_format(): + """Non-native ReActV2 history provides ICL examples using DSPy field markers.""" + adapter = dspy.ChatAdapter() + sig = ( + dspy.Signature({}, "Do the task.") + .append("question", dspy.InputField(), type_=str) + .append("history", dspy.InputField(), type_=dspy.History) + .append("tools", dspy.InputField(), type_=list[dspy.Tool]) + .append("next_thought", dspy.OutputField(), type_=str) + .append("tool_calls", dspy.OutputField(), type_=dspy.ToolCalls) + ) + history = History(messages=[]) + history.append_input({"question": "What is 1+2?"}) + history.append_action( + thought="I should add the two numbers.", + tool_calls=ToolCalls.from_dict_list([{"name": "add", "args": {"a": 1, "b": 2}}]), + observations=[Observation(value=3)], + ) + + messages = adapter.format(sig, [], {"question": "What is 1+2?", "history": history, "tools": [Tool(_make_add_tool())]}) + assistant_content = next(message["content"] for message in messages if message["role"] == "assistant") + + assert "[[ ## next_thought ## ]]" in assistant_content + assert "I should add the two numbers." in assistant_content + assert "[[ ## tool_calls ## ]]" in assistant_content + assert '"name": "add"' in assistant_content + assert "[[ ## completed ## ]]" in assistant_content + assert "Thought:" not in assistant_content + assert "Action:" not in assistant_content + + +def test_toolcalls_normalizes_openai_format(): + """VAL-FMT-004: ToolCalls normalizes OpenAI {type:'function', function:{name, arguments}} format.""" + tc = ToolCalls(tool_calls=[ + {"type": "function", "function": {"name": "search", "arguments": '{"query": "hello"}'}, "id": "call_1"}, + {"type": "function", "function": {"name": "submit", "arguments": {"answer": "42"}}}, + ]) + assert len(tc.tool_calls) == 2 + assert tc.tool_calls[0].name == "search" + assert tc.tool_calls[0].args == {"query": "hello"} + assert tc.tool_calls[0].id == "call_1" + assert tc.tool_calls[1].name == "submit" + + +def test_tool_name_sanitization(): + """Tool names sanitized to match OpenAI ^[a-zA-Z0-9_-]+$ pattern.""" + assert _sanitize_tool_name("my.tool") == "my_tool" + assert _sanitize_tool_name("tool name!") == "tool_name_" + assert _sanitize_tool_name("valid-name_123") == "valid-name_123" + # Test via Tool constructor + def my_weird_fn(x: str) -> str: + """A tool.""" + return x + tool = Tool(my_weird_fn, name="weird.tool.name") + assert tool.name == "weird_tool_name" + + +def test_gepa_compile_with_reactv2(): + """VAL-OPTIM-001: GEPA.compile() on a ReActV2 module completes without error.""" + from dspy.teleprompt.gepa.gepa import GEPA + lm = DummyLM([ + {"next_thought": "Do it.", "tool_calls": [{"name": "submit", "args": {"answer": "3"}}]}, + ] * 20) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + + def metric(ex, pred, *a, **kw): + return float(getattr(pred, "answer", None) == ex.answer) if hasattr(pred, "answer") else 0.0 + + trainset = [dspy.Example(question="1+2", answer="3").with_inputs("question")] + gepa = GEPA(metric=metric, max_metric_calls=2, reflection_lm=lm) + result = gepa.compile(react, trainset=trainset) + assert isinstance(result, ReActV2) + assert "add" in result.react.signature.instructions + + +def test_forced_submit_extract_fallback(): + """Tier 2 extract fallback produces output when model never calls submit.""" + lm = DummyLM([ + # Main loop: model keeps calling add, never submits + {"next_thought": "Adding.", "tool_calls": [{"name": "add", "args": {"a": 1, "b": 2}}]}, + # Forced submit tier 1: model STILL calls add instead of submit + {"next_thought": "Adding more.", "tool_calls": [{"name": "add", "args": {"a": 3, "b": 4}}]}, + # Tier 2 extract (ChainOfThought): model finally produces the answer + {"reasoning": "Based on the trajectory, the answer is 3.", "answer": "3"}, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + result = react(question="What is 1+2?", max_iters=1) + assert result.answer == "3" + assert result.termination_reason == "extract" + + +def test_forced_submit_records_only_selected_submit_call(): + """Forced submit records the submit call and its observation with aligned history.""" + lm = DummyLM([ + { + "next_thought": "I can submit now.", + "tool_calls": [ + {"name": "add", "args": {"a": 1, "b": 2}}, + {"name": "submit", "args": {"answer": "3"}}, + ], + }, + ]) + dspy.configure(lm=lm) + react = ReActV2("question -> answer", tools=[_make_add_tool()]) + + result = react(question="What is 1+2?", max_iters=0) + + assert result.answer == "3" + action = next(m for m in result.history.messages if isinstance(m, ActionEvent)) + assert len(action.tool_calls.tool_calls) == 1 + assert action.tool_calls.tool_calls[0].name == "submit" + assert len(action.observations) == 1 diff --git a/uv.lock b/uv.lock index 88667a3f74..08534b80e2 100644 --- a/uv.lock +++ b/uv.lock @@ -180,20 +180,21 @@ wheels = [ [[package]] name = "anthropic" -version = "0.54.0" +version = "0.89.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "distro" }, + { name = "docstring-parser" }, { name = "httpx" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/28/80cb9bb6e7ce77d404145b51da4257455805c17f0a6be528ff3286e3882f/anthropic-0.54.0.tar.gz", hash = "sha256:5e6f997d97ce8e70eac603c3ec2e7f23addeff953fbbb76b19430562bb6ba815", size = 312376, upload-time = "2025-06-11T02:46:27.642Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/af/862e216dd6c5e9bc02fb374eeaaa19017c51b90ddfa5692668a3811947bd/anthropic-0.89.0.tar.gz", hash = "sha256:f3d75b8ccef4b35f3702639519e461eba437d4bcdfabb69378c65a02ab7bda66", size = 596758, upload-time = "2026-04-03T18:57:01.348Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b9/6ffb48e82c5e97b03cecee872d134a6b6666c2767b2d32ed709f3a60a8fe/anthropic-0.54.0-py3-none-any.whl", hash = "sha256:c1062a0a905daeec17ca9c06c401e4b3f24cb0495841d29d752568a1d4018d56", size = 288774, upload-time = "2025-06-11T02:46:25.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/9f973f22abb512d5d17428a76e4ecbc8d49b9dd1b5a1152576d48c24dc1d/anthropic-0.89.0-py3-none-any.whl", hash = "sha256:c6d23854af798f2471ca3bc653cca394d392cc272fe803d3da9d63575b8445f0", size = 478847, upload-time = "2026-04-03T18:56:59.54Z" }, ] [[package]] @@ -521,11 +522,11 @@ wheels = [ [[package]] name = "cloudpickle" -version = "3.1.1" +version = "3.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113, upload-time = "2025-01-14T17:02:05.085Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992, upload-time = "2025-01-14T17:02:02.417Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, ] [[package]] @@ -727,6 +728,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + [[package]] name = "dspy" version = "3.1.3" @@ -800,7 +810,7 @@ requires-dist = [ { name = "asyncer", specifier = "==0.0.8" }, { name = "build", marker = "extra == 'dev'", specifier = ">=1.0.3" }, { name = "cachetools", specifier = ">=5.5.0" }, - { name = "cloudpickle", specifier = ">=3.0.0" }, + { name = "cloudpickle", specifier = ">=3.1.2" }, { name = "datamodel-code-generator", marker = "extra == 'dev'", specifier = ">=0.26.3" }, { name = "datasets", marker = "extra == 'test-extras'", specifier = ">=2.14.6" }, { name = "diskcache", specifier = ">=5.6.0" }, @@ -1034,7 +1044,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/db/b4c12cff13ebac2786f4f217f06588bccd8b53d260453404ef22b121fc3a/greenlet-3.2.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:1afd685acd5597349ee6d7a88a8bec83ce13c106ac78c196ee9dde7c04fe87be", size = 268977, upload-time = "2025-06-05T16:10:24.001Z" }, { url = "https://files.pythonhosted.org/packages/52/61/75b4abd8147f13f70986df2801bf93735c1bd87ea780d70e3b3ecda8c165/greenlet-3.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:761917cac215c61e9dc7324b2606107b3b292a8349bdebb31503ab4de3f559ac", size = 627351, upload-time = "2025-06-05T16:38:50.685Z" }, { url = "https://files.pythonhosted.org/packages/35/aa/6894ae299d059d26254779a5088632874b80ee8cf89a88bca00b0709d22f/greenlet-3.2.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a433dbc54e4a37e4fff90ef34f25a8c00aed99b06856f0119dcf09fbafa16392", size = 638599, upload-time = "2025-06-05T16:41:34.057Z" }, - { url = "https://files.pythonhosted.org/packages/30/64/e01a8261d13c47f3c082519a5e9dbf9e143cc0498ed20c911d04e54d526c/greenlet-3.2.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:72e77ed69312bab0434d7292316d5afd6896192ac4327d44f3d613ecb85b037c", size = 634482, upload-time = "2025-06-05T16:48:16.26Z" }, { url = "https://files.pythonhosted.org/packages/47/48/ff9ca8ba9772d083a4f5221f7b4f0ebe8978131a9ae0909cf202f94cd879/greenlet-3.2.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68671180e3849b963649254a882cd544a3c75bfcd2c527346ad8bb53494444db", size = 633284, upload-time = "2025-06-05T16:13:01.599Z" }, { url = "https://files.pythonhosted.org/packages/e9/45/626e974948713bc15775b696adb3eb0bd708bec267d6d2d5c47bb47a6119/greenlet-3.2.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49c8cfb18fb419b3d08e011228ef8a25882397f3a859b9fe1436946140b6756b", size = 582206, upload-time = "2025-06-05T16:12:48.51Z" }, { url = "https://files.pythonhosted.org/packages/b1/8e/8b6f42c67d5df7db35b8c55c9a850ea045219741bb14416255616808c690/greenlet-3.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:efc6dc8a792243c31f2f5674b670b3a95d46fa1c6a912b8e310d6f542e7b0712", size = 1111412, upload-time = "2025-06-05T16:36:45.479Z" }, @@ -1043,7 +1052,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, @@ -1052,7 +1060,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, @@ -1061,7 +1068,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, - { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, @@ -1070,7 +1076,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/ca/accd7aa5280eb92b70ed9e8f7fd79dc50a2c21d8c73b9a0856f5b564e222/greenlet-3.2.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3d04332dddb10b4a211b68111dabaee2e1a073663d117dc10247b5b1642bac86", size = 271479, upload-time = "2025-06-05T16:10:47.525Z" }, { url = "https://files.pythonhosted.org/packages/55/71/01ed9895d9eb49223280ecc98a557585edfa56b3d0e965b9fa9f7f06b6d9/greenlet-3.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8186162dffde068a465deab08fc72c767196895c39db26ab1c17c0b77a6d8b97", size = 683952, upload-time = "2025-06-05T16:38:55.125Z" }, { url = "https://files.pythonhosted.org/packages/ea/61/638c4bdf460c3c678a0a1ef4c200f347dff80719597e53b5edb2fb27ab54/greenlet-3.2.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f4bfbaa6096b1b7a200024784217defedf46a07c2eee1a498e94a1b5f8ec5728", size = 696917, upload-time = "2025-06-05T16:41:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/22/cc/0bd1a7eb759d1f3e3cc2d1bc0f0b487ad3cc9f34d74da4b80f226fde4ec3/greenlet-3.2.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:ed6cfa9200484d234d8394c70f5492f144b20d4533f69262d530a1a082f6ee9a", size = 692443, upload-time = "2025-06-05T16:48:23.113Z" }, { url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995, upload-time = "2025-06-05T16:13:07.972Z" }, { url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320, upload-time = "2025-06-05T16:12:53.453Z" }, { url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236, upload-time = "2025-06-05T16:15:20.111Z" }, @@ -2120,79 +2125,83 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/1d/5e0ae38788bdf0721326695e65fdf41405ed535f633eb0df0f06f57552fa/orjson-3.11.2.tar.gz", hash = "sha256:91bdcf5e69a8fd8e8bdb3de32b31ff01d2bd60c1e8d5fe7d5afabdcf19920309", size = 5470739, upload-time = "2025-08-12T15:12:28.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/7b/7aebe925c6b1c46c8606a960fe1d6b681fccd4aaf3f37cd647c3309d6582/orjson-3.11.2-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6b8a78c33496230a60dc9487118c284c15ebdf6724386057239641e1eb69761", size = 226896, upload-time = "2025-08-12T15:10:22.02Z" }, - { url = "https://files.pythonhosted.org/packages/7d/39/c952c9b0d51063e808117dd1e53668a2e4325cc63cfe7df453d853ee8680/orjson-3.11.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc04036eeae11ad4180d1f7b5faddb5dab1dee49ecd147cd431523869514873b", size = 111845, upload-time = "2025-08-12T15:10:24.963Z" }, - { url = "https://files.pythonhosted.org/packages/f5/dc/90b7f29be38745eeacc30903b693f29fcc1097db0c2a19a71ffb3e9f2a5f/orjson-3.11.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c04325839c5754c253ff301cee8aaed7442d974860a44447bb3be785c411c27", size = 116395, upload-time = "2025-08-12T15:10:26.314Z" }, - { url = "https://files.pythonhosted.org/packages/10/c2/fe84ba63164c22932b8d59b8810e2e58590105293a259e6dd1bfaf3422c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32769e04cd7fdc4a59854376211145a1bbbc0aea5e9d6c9755d3d3c301d7c0df", size = 118768, upload-time = "2025-08-12T15:10:27.605Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ce/d9748ec69b1a4c29b8e2bab8233e8c41c583c69f515b373f1fb00247d8c9/orjson-3.11.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff285d14917ea1408a821786e3677c5261fa6095277410409c694b8e7720ae0", size = 120887, upload-time = "2025-08-12T15:10:29.153Z" }, - { url = "https://files.pythonhosted.org/packages/c1/66/b90fac8e4a76e83f981912d7f9524d402b31f6c1b8bff3e498aa321c326c/orjson-3.11.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2662f908114864b63ff75ffe6ffacf996418dd6cc25e02a72ad4bda81b1ec45a", size = 123650, upload-time = "2025-08-12T15:10:30.602Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/56143898d1689c7f915ac67703efb97e8f2f8d5805ce8c2c3fd0f2bb6e3d/orjson-3.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab463cf5d08ad6623a4dac1badd20e88a5eb4b840050c4812c782e3149fe2334", size = 121287, upload-time = "2025-08-12T15:10:31.868Z" }, - { url = "https://files.pythonhosted.org/packages/80/de/f9c6d00c127be766a3739d0d85b52a7c941e437d8dd4d573e03e98d0f89c/orjson-3.11.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:64414241bde943cbf3c00d45fcb5223dca6d9210148ba984aae6b5d63294502b", size = 119637, upload-time = "2025-08-12T15:10:33.078Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/ab70c7627022d395c1b4eb5badf6196b7144e82b46a3a17ed2354f9e592d/orjson-3.11.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7773e71c0ae8c9660192ff144a3d69df89725325e3d0b6a6bb2c50e5ebaf9b84", size = 392478, upload-time = "2025-08-12T15:10:34.669Z" }, - { url = "https://files.pythonhosted.org/packages/77/91/d890b873b69311db4fae2624c5603c437df9c857fb061e97706dac550a77/orjson-3.11.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:652ca14e283b13ece35bf3a86503c25592f294dbcfc5bb91b20a9c9a62a3d4be", size = 134343, upload-time = "2025-08-12T15:10:35.978Z" }, - { url = "https://files.pythonhosted.org/packages/47/16/1aa248541b4830274a079c4aeb2aa5d1ff17c3f013b1d0d8d16d0848f3de/orjson-3.11.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:26e99e98df8990ecfe3772bbdd7361f602149715c2cbc82e61af89bfad9528a4", size = 123887, upload-time = "2025-08-12T15:10:37.601Z" }, - { url = "https://files.pythonhosted.org/packages/95/e4/7419833c55ac8b5f385d00c02685a260da1f391e900fc5c3e0b797e0d506/orjson-3.11.2-cp310-cp310-win32.whl", hash = "sha256:5814313b3e75a2be7fe6c7958201c16c4560e21a813dbad25920752cecd6ad66", size = 124560, upload-time = "2025-08-12T15:10:38.966Z" }, - { url = "https://files.pythonhosted.org/packages/74/f8/27ca7ef3e194c462af32ce1883187f5ec483650c559166f0de59c4c2c5f0/orjson-3.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc471ce2225ab4c42ca672f70600d46a8b8e28e8d4e536088c1ccdb1d22b35ce", size = 119700, upload-time = "2025-08-12T15:10:40.911Z" }, - { url = "https://files.pythonhosted.org/packages/78/7d/e295df1ac9920cbb19fb4c1afa800e86f175cb657143aa422337270a4782/orjson-3.11.2-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:888b64ef7eaeeff63f773881929434a5834a6a140a63ad45183d59287f07fc6a", size = 226502, upload-time = "2025-08-12T15:10:42.284Z" }, - { url = "https://files.pythonhosted.org/packages/65/21/ffb0f10ea04caf418fb4e7ad1fda4b9ab3179df9d7a33b69420f191aadd5/orjson-3.11.2-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:83387cc8b26c9fa0ae34d1ea8861a7ae6cff8fb3e346ab53e987d085315a728e", size = 115999, upload-time = "2025-08-12T15:10:43.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/d5/8da1e252ac3353d92e6f754ee0c85027c8a2cda90b6899da2be0df3ef83d/orjson-3.11.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7e35f003692c216d7ee901b6b916b5734d6fc4180fcaa44c52081f974c08e17", size = 111563, upload-time = "2025-08-12T15:10:45.301Z" }, - { url = "https://files.pythonhosted.org/packages/4f/81/baabc32e52c570b0e4e1044b1bd2ccbec965e0de3ba2c13082255efa2006/orjson-3.11.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4a0a4c29ae90b11d0c00bcc31533854d89f77bde2649ec602f512a7e16e00640", size = 116222, upload-time = "2025-08-12T15:10:46.92Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/da2ad55ad80b49b560dce894c961477d0e76811ee6e614b301de9f2f8728/orjson-3.11.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:585d712b1880f68370108bc5534a257b561672d1592fae54938738fe7f6f1e33", size = 118594, upload-time = "2025-08-12T15:10:48.488Z" }, - { url = "https://files.pythonhosted.org/packages/61/be/014f7eab51449f3c894aa9bbda2707b5340c85650cb7d0db4ec9ae280501/orjson-3.11.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d08e342a7143f8a7c11f1c4033efe81acbd3c98c68ba1b26b96080396019701f", size = 120700, upload-time = "2025-08-12T15:10:49.811Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ae/c217903a30c51341868e2d8c318c59a8413baa35af54d7845071c8ccd6fe/orjson-3.11.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c0f84fc50398773a702732c87cd622737bf11c0721e6db3041ac7802a686fb", size = 123433, upload-time = "2025-08-12T15:10:51.06Z" }, - { url = "https://files.pythonhosted.org/packages/57/c2/b3c346f78b1ff2da310dd300cb0f5d32167f872b4d3bb1ad122c889d97b0/orjson-3.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:140f84e3c8d4c142575898c91e3981000afebf0333df753a90b3435d349a5fe5", size = 121061, upload-time = "2025-08-12T15:10:52.381Z" }, - { url = "https://files.pythonhosted.org/packages/00/c8/c97798f6010327ffc75ad21dd6bca11ea2067d1910777e798c2849f1c68f/orjson-3.11.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96304a2b7235e0f3f2d9363ddccdbfb027d27338722fe469fe656832a017602e", size = 119410, upload-time = "2025-08-12T15:10:53.692Z" }, - { url = "https://files.pythonhosted.org/packages/37/fd/df720f7c0e35694617b7f95598b11a2cb0374661d8389703bea17217da53/orjson-3.11.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d7612bb227d5d9582f1f50a60bd55c64618fc22c4a32825d233a4f2771a428a", size = 392294, upload-time = "2025-08-12T15:10:55.079Z" }, - { url = "https://files.pythonhosted.org/packages/ba/52/0120d18f60ab0fe47531d520372b528a45c9a25dcab500f450374421881c/orjson-3.11.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a134587d18fe493befc2defffef2a8d27cfcada5696cb7234de54a21903ae89a", size = 134134, upload-time = "2025-08-12T15:10:56.568Z" }, - { url = "https://files.pythonhosted.org/packages/ec/10/1f967671966598366de42f07e92b0fc694ffc66eafa4b74131aeca84915f/orjson-3.11.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0b84455e60c4bc12c1e4cbaa5cfc1acdc7775a9da9cec040e17232f4b05458bd", size = 123745, upload-time = "2025-08-12T15:10:57.907Z" }, - { url = "https://files.pythonhosted.org/packages/43/eb/76081238671461cfd0f47e0c24f408ffa66184237d56ef18c33e86abb612/orjson-3.11.2-cp311-cp311-win32.whl", hash = "sha256:f0660efeac223f0731a70884e6914a5f04d613b5ae500744c43f7bf7b78f00f9", size = 124393, upload-time = "2025-08-12T15:10:59.267Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/cc598c1811ba9ba935171267b02e377fc9177489efce525d478a2999d9cc/orjson-3.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:955811c8405251d9e09cbe8606ad8fdef49a451bcf5520095a5ed38c669223d8", size = 119561, upload-time = "2025-08-12T15:11:00.559Z" }, - { url = "https://files.pythonhosted.org/packages/d8/17/c48011750f0489006f7617b0a3cebc8230f36d11a34e7e9aca2085f07792/orjson-3.11.2-cp311-cp311-win_arm64.whl", hash = "sha256:2e4d423a6f838552e3a6d9ec734b729f61f88b1124fd697eab82805ea1a2a97d", size = 114186, upload-time = "2025-08-12T15:11:01.931Z" }, - { url = "https://files.pythonhosted.org/packages/40/02/46054ebe7996a8adee9640dcad7d39d76c2000dc0377efa38e55dc5cbf78/orjson-3.11.2-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:901d80d349d8452162b3aa1afb82cec5bee79a10550660bc21311cc61a4c5486", size = 226528, upload-time = "2025-08-12T15:11:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/6b6f0b4d8aea1137436546b990f71be2cd8bd870aa2f5aa14dba0fcc95dc/orjson-3.11.2-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:cf3bd3967a360e87ee14ed82cb258b7f18c710dacf3822fb0042a14313a673a1", size = 115931, upload-time = "2025-08-12T15:11:04.759Z" }, - { url = "https://files.pythonhosted.org/packages/ae/05/4205cc97c30e82a293dd0d149b1a89b138ebe76afeca66fc129fa2aa4e6a/orjson-3.11.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26693dde66910078229a943e80eeb99fdce6cd2c26277dc80ead9f3ab97d2131", size = 111382, upload-time = "2025-08-12T15:11:06.468Z" }, - { url = "https://files.pythonhosted.org/packages/50/c7/b8a951a93caa821f9272a7c917115d825ae2e4e8768f5ddf37968ec9de01/orjson-3.11.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad4c8acb50a28211c33fc7ef85ddf5cb18d4636a5205fd3fa2dce0411a0e30c", size = 116271, upload-time = "2025-08-12T15:11:07.845Z" }, - { url = "https://files.pythonhosted.org/packages/17/03/1006c7f8782d5327439e26d9b0ec66500ea7b679d4bbb6b891d2834ab3ee/orjson-3.11.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994181e7f1725bb5f2d481d7d228738e0743b16bf319ca85c29369c65913df14", size = 119086, upload-time = "2025-08-12T15:11:09.329Z" }, - { url = "https://files.pythonhosted.org/packages/44/61/57d22bc31f36a93878a6f772aea76b2184102c6993dea897656a66d18c74/orjson-3.11.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbb79a0476393c07656b69c8e763c3cc925fa8e1d9e9b7d1f626901bb5025448", size = 120724, upload-time = "2025-08-12T15:11:10.674Z" }, - { url = "https://files.pythonhosted.org/packages/78/a9/4550e96b4c490c83aea697d5347b8f7eb188152cd7b5a38001055ca5b379/orjson-3.11.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191ed27a1dddb305083d8716af413d7219f40ec1d4c9b0e977453b4db0d6fb6c", size = 123577, upload-time = "2025-08-12T15:11:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/3a/86/09b8cb3ebd513d708ef0c92d36ac3eebda814c65c72137b0a82d6d688fc4/orjson-3.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0afb89f16f07220183fd00f5f297328ed0a68d8722ad1b0c8dcd95b12bc82804", size = 121195, upload-time = "2025-08-12T15:11:13.399Z" }, - { url = "https://files.pythonhosted.org/packages/37/68/7b40b39ac2c1c644d4644e706d0de6c9999764341cd85f2a9393cb387661/orjson-3.11.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ab6e6b4e93b1573a026b6ec16fca9541354dd58e514b62c558b58554ae04307", size = 119234, upload-time = "2025-08-12T15:11:15.134Z" }, - { url = "https://files.pythonhosted.org/packages/40/7c/bb6e7267cd80c19023d44d8cbc4ea4ed5429fcd4a7eb9950f50305697a28/orjson-3.11.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9cb23527efb61fb75527df55d20ee47989c4ee34e01a9c98ee9ede232abf6219", size = 392250, upload-time = "2025-08-12T15:11:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/64/f2/6730ace05583dbca7c1b406d59f4266e48cd0d360566e71482420fb849fc/orjson-3.11.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a4dd1268e4035af21b8a09e4adf2e61f87ee7bf63b86d7bb0a237ac03fad5b45", size = 134572, upload-time = "2025-08-12T15:11:18.205Z" }, - { url = "https://files.pythonhosted.org/packages/96/0f/7d3e03a30d5aac0432882b539a65b8c02cb6dd4221ddb893babf09c424cc/orjson-3.11.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff8b155b145eaf5a9d94d2c476fbe18d6021de93cf36c2ae2c8c5b775763f14e", size = 123869, upload-time = "2025-08-12T15:11:19.554Z" }, - { url = "https://files.pythonhosted.org/packages/45/80/1513265eba6d4a960f078f4b1d2bff94a571ab2d28c6f9835e03dfc65cc6/orjson-3.11.2-cp312-cp312-win32.whl", hash = "sha256:ae3bb10279d57872f9aba68c9931aa71ed3b295fa880f25e68da79e79453f46e", size = 124430, upload-time = "2025-08-12T15:11:20.914Z" }, - { url = "https://files.pythonhosted.org/packages/fb/61/eadf057b68a332351eeb3d89a4cc538d14f31cd8b5ec1b31a280426ccca2/orjson-3.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:d026e1967239ec11a2559b4146a61d13914504b396f74510a1c4d6b19dfd8732", size = 119598, upload-time = "2025-08-12T15:11:22.372Z" }, - { url = "https://files.pythonhosted.org/packages/6b/3f/7f4b783402143d965ab7e9a2fc116fdb887fe53bdce7d3523271cd106098/orjson-3.11.2-cp312-cp312-win_arm64.whl", hash = "sha256:59f8d5ad08602711af9589375be98477d70e1d102645430b5a7985fdbf613b36", size = 114052, upload-time = "2025-08-12T15:11:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/0dd6b4750eb556ae4e2c6a9cb3e219ec642e9c6d95f8ebe5dc9020c67204/orjson-3.11.2-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a079fdba7062ab396380eeedb589afb81dc6683f07f528a03b6f7aae420a0219", size = 226419, upload-time = "2025-08-12T15:11:25.517Z" }, - { url = "https://files.pythonhosted.org/packages/44/d5/e67f36277f78f2af8a4690e0c54da6b34169812f807fd1b4bfc4dbcf9558/orjson-3.11.2-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:6a5f62ebbc530bb8bb4b1ead103647b395ba523559149b91a6c545f7cd4110ad", size = 115803, upload-time = "2025-08-12T15:11:27.357Z" }, - { url = "https://files.pythonhosted.org/packages/24/37/ff8bc86e0dacc48f07c2b6e20852f230bf4435611bab65e3feae2b61f0ae/orjson-3.11.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7df6c7b8b0931feb3420b72838c3e2ba98c228f7aa60d461bc050cf4ca5f7b2", size = 111337, upload-time = "2025-08-12T15:11:28.805Z" }, - { url = "https://files.pythonhosted.org/packages/b9/25/37d4d3e8079ea9784ea1625029988e7f4594ce50d4738b0c1e2bf4a9e201/orjson-3.11.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f59dfea7da1fced6e782bb3699718088b1036cb361f36c6e4dd843c5111aefe", size = 116222, upload-time = "2025-08-12T15:11:30.18Z" }, - { url = "https://files.pythonhosted.org/packages/b7/32/a63fd9c07fce3b4193dcc1afced5dd4b0f3a24e27556604e9482b32189c9/orjson-3.11.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edf49146520fef308c31aa4c45b9925fd9c7584645caca7c0c4217d7900214ae", size = 119020, upload-time = "2025-08-12T15:11:31.59Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b6/400792b8adc3079a6b5d649264a3224d6342436d9fac9a0ed4abc9dc4596/orjson-3.11.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50995bbeb5d41a32ad15e023305807f561ac5dcd9bd41a12c8d8d1d2c83e44e6", size = 120721, upload-time = "2025-08-12T15:11:33.035Z" }, - { url = "https://files.pythonhosted.org/packages/40/f3/31ab8f8c699eb9e65af8907889a0b7fef74c1d2b23832719a35da7bb0c58/orjson-3.11.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cc42960515076eb639b705f105712b658c525863d89a1704d984b929b0577d1", size = 123574, upload-time = "2025-08-12T15:11:34.433Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a6/ce4287c412dff81878f38d06d2c80845709c60012ca8daf861cb064b4574/orjson-3.11.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56777cab2a7b2a8ea687fedafb84b3d7fdafae382165c31a2adf88634c432fa", size = 121225, upload-time = "2025-08-12T15:11:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/69/b0/7a881b2aef4fed0287d2a4fbb029d01ed84fa52b4a68da82bdee5e50598e/orjson-3.11.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07349e88025b9b5c783077bf7a9f401ffbfb07fd20e86ec6fc5b7432c28c2c5e", size = 119201, upload-time = "2025-08-12T15:11:37.642Z" }, - { url = "https://files.pythonhosted.org/packages/cf/98/a325726b37f7512ed6338e5e65035c3c6505f4e628b09a5daf0419f054ea/orjson-3.11.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:45841fbb79c96441a8c58aa29ffef570c5df9af91f0f7a9572e5505e12412f15", size = 392193, upload-time = "2025-08-12T15:11:39.153Z" }, - { url = "https://files.pythonhosted.org/packages/cb/4f/a7194f98b0ce1d28190e0c4caa6d091a3fc8d0107ad2209f75c8ba398984/orjson-3.11.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13d8d8db6cd8d89d4d4e0f4161acbbb373a4d2a4929e862d1d2119de4aa324ac", size = 134548, upload-time = "2025-08-12T15:11:40.768Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/b84caa2986c3f472dc56343ddb0167797a708a8d5c3be043e1e2677b55df/orjson-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51da1ee2178ed09c00d09c1b953e45846bbc16b6420965eb7a913ba209f606d8", size = 123798, upload-time = "2025-08-12T15:11:42.164Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5b/e398449080ce6b4c8fcadad57e51fa16f65768e1b142ba90b23ac5d10801/orjson-3.11.2-cp313-cp313-win32.whl", hash = "sha256:51dc033df2e4a4c91c0ba4f43247de99b3cbf42ee7a42ee2b2b2f76c8b2f2cb5", size = 124402, upload-time = "2025-08-12T15:11:44.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/429e4608e124debfc4790bfc37131f6958e59510ba3b542d5fc163be8e5f/orjson-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:29d91d74942b7436f29b5d1ed9bcfc3f6ef2d4f7c4997616509004679936650d", size = 119498, upload-time = "2025-08-12T15:11:45.864Z" }, - { url = "https://files.pythonhosted.org/packages/7b/04/f8b5f317cce7ad3580a9ad12d7e2df0714dfa8a83328ecddd367af802f5b/orjson-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:4ca4fb5ac21cd1e48028d4f708b1bb13e39c42d45614befd2ead004a8bba8535", size = 114051, upload-time = "2025-08-12T15:11:47.555Z" }, - { url = "https://files.pythonhosted.org/packages/74/83/2c363022b26c3c25b3708051a19d12f3374739bb81323f05b284392080c0/orjson-3.11.2-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3dcba7101ea6a8d4ef060746c0f2e7aa8e2453a1012083e1ecce9726d7554cb7", size = 226406, upload-time = "2025-08-12T15:11:49.445Z" }, - { url = "https://files.pythonhosted.org/packages/b0/a7/aa3c973de0b33fc93b4bd71691665ffdfeae589ea9d0625584ab10a7d0f5/orjson-3.11.2-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:15d17bdb76a142e1f55d91913e012e6e6769659daa6bfef3ef93f11083137e81", size = 115788, upload-time = "2025-08-12T15:11:50.992Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/e45f233dfd09fdbb052ec46352363dca3906618e1a2b264959c18f809d0b/orjson-3.11.2-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:53c9e81768c69d4b66b8876ec3c8e431c6e13477186d0db1089d82622bccd19f", size = 111318, upload-time = "2025-08-12T15:11:52.495Z" }, - { url = "https://files.pythonhosted.org/packages/3e/23/cf5a73c4da6987204cbbf93167f353ff0c5013f7c5e5ef845d4663a366da/orjson-3.11.2-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d4f13af59a7b84c1ca6b8a7ab70d608f61f7c44f9740cd42409e6ae7b6c8d8b7", size = 121231, upload-time = "2025-08-12T15:11:53.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/1d/47468a398ae68a60cc21e599144e786e035bb12829cb587299ecebc088f1/orjson-3.11.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bde64aa469b5ee46cc960ed241fae3721d6a8801dacb2ca3466547a2535951e4", size = 119204, upload-time = "2025-08-12T15:11:55.409Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d9/f99433d89b288b5bc8836bffb32a643f805e673cf840ef8bab6e73ced0d1/orjson-3.11.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b5ca86300aeb383c8fa759566aca065878d3d98c3389d769b43f0a2e84d52c5f", size = 392237, upload-time = "2025-08-12T15:11:57.18Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dc/1b9d80d40cebef603325623405136a29fb7d08c877a728c0943dd066c29a/orjson-3.11.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:24e32a558ebed73a6a71c8f1cbc163a7dd5132da5270ff3d8eeb727f4b6d1bc7", size = 134578, upload-time = "2025-08-12T15:11:58.844Z" }, - { url = "https://files.pythonhosted.org/packages/45/b3/72e7a4c5b6485ef4e83ef6aba7f1dd041002bad3eb5d1d106ca5b0fc02c6/orjson-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e36319a5d15b97e4344110517450396845cc6789aed712b1fbf83c1bd95792f6", size = 123799, upload-time = "2025-08-12T15:12:00.352Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/a3d76b392e7acf9b34dc277171aad85efd6accc75089bb35b4c614990ea9/orjson-3.11.2-cp314-cp314-win32.whl", hash = "sha256:40193ada63fab25e35703454d65b6afc71dbc65f20041cb46c6d91709141ef7f", size = 124461, upload-time = "2025-08-12T15:12:01.854Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e3/75c6a596ff8df9e4a5894813ff56695f0a218e6ea99420b4a645c4f7795d/orjson-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:7c8ac5f6b682d3494217085cf04dadae66efee45349ad4ee2a1da3c97e2305a8", size = 119494, upload-time = "2025-08-12T15:12:03.337Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3d/9e74742fc261c5ca473c96bb3344d03995869e1dc6402772c60afb97736a/orjson-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:21cf261e8e79284242e4cb1e5924df16ae28255184aafeff19be1405f6d33f67", size = 114046, upload-time = "2025-08-12T15:12:04.87Z" }, +version = "3.11.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/90/5d81f61fe3e4270da80c71442864c091cee3003cc8984c75f413fe742a07/orjson-3.11.8-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e6693ff90018600c72fd18d3d22fa438be26076cd3c823da5f63f7bab28c11cb", size = 229663, upload-time = "2026-03-31T16:14:30.708Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/85e06b0eb11de6fb424120fd5788a07035bd4c5e6bb7841ae9972a0526d1/orjson-3.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93de06bc920854552493c81f1f729fab7213b7db4b8195355db5fda02c7d1363", size = 132321, upload-time = "2026-03-31T16:14:32.317Z" }, + { url = "https://files.pythonhosted.org/packages/86/71/089338ee51b3132f050db0864a7df9bdd5e94c2a03820ab8a91e8f655618/orjson-3.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe0b8c83e0f36247fc9431ce5425a5d95f9b3a689133d494831bdbd6f0bceb13", size = 130658, upload-time = "2026-03-31T16:14:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/10/0d/f39d8802345d0ad65f7fd4374b29b9b59f98656dc30f21ca5c773265b2f0/orjson-3.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97d823831105c01f6c8029faf297633dbeb30271892bd430e9c24ceae3734744", size = 135708, upload-time = "2026-03-31T16:14:35.224Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b5/40aae576b3473511696dcffea84fde638b2b64774eb4dcb8b2c262729f8a/orjson-3.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60c0423f15abb6cf78f56dff00168a1b582f7a1c23f114036e2bfc697814d5f", size = 147047, upload-time = "2026-03-31T16:14:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f0/778a84458d1fdaa634b2e572e51ce0b354232f580b2327e1f00a8d88c38c/orjson-3.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01928d0476b216ad2201823b0a74000440360cef4fed1912d297b8d84718f277", size = 133072, upload-time = "2026-03-31T16:14:37.715Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/1bbf2fc3ffcc4b829ade554b574af68cec898c9b5ad6420a923c75a073d3/orjson-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4a639049c44d36a6d1ae0f4a94b271605c745aee5647fa8ffaabcdc01b69a6", size = 133867, upload-time = "2026-03-31T16:14:39.356Z" }, + { url = "https://files.pythonhosted.org/packages/08/94/6413da22edc99a69a8d0c2e83bf42973b8aa94d83ef52a6d39ac85da00bc/orjson-3.11.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3222adff1e1ff0dce93c16146b93063a7793de6c43d52309ae321234cdaf0f4d", size = 142268, upload-time = "2026-03-31T16:14:40.972Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/aa5dbaa6136d7ba55f5461ac2e885efc6e6349424a428927fd46d68f4396/orjson-3.11.8-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3223665349bbfb68da234acd9846955b1a0808cbe5520ff634bf253a4407009b", size = 424008, upload-time = "2026-03-31T16:14:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/fa/aa/2c1962d108c7fe5e27aa03a354b378caf56d8eafdef15fd83dec081ce45a/orjson-3.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:61c9d357a59465736022d5d9ba06687afb7611dfb581a9d2129b77a6fcf78e59", size = 147942, upload-time = "2026-03-31T16:14:44.256Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/65f404f4c47eb1b0b4476f03ec838cac0c4aa933920ff81e5dda4dee14e7/orjson-3.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58fb9b17b4472c7b1dcf1a54583629e62e23779b2331052f09a9249edf81675b", size = 136640, upload-time = "2026-03-31T16:14:45.884Z" }, + { url = "https://files.pythonhosted.org/packages/90/5f/7b784aea98bdb125a2f2da7c27d6c2d2f6d943d96ef0278bae596d563f85/orjson-3.11.8-cp310-cp310-win32.whl", hash = "sha256:b43dc2a391981d36c42fa57747a49dae793ef1d2e43898b197925b5534abd10a", size = 132066, upload-time = "2026-03-31T16:14:47.397Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/2e284af8d6c9478df5ef938917743f61d68f4c70d17f1b6e82f7e3b8dba1/orjson-3.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:c98121237fea2f679480765abd566f7713185897f35c9e6c2add7e3a9900eb61", size = 127609, upload-time = "2026-03-31T16:14:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/67/41/5aa7fa3b0f4dc6b47dcafc3cea909299c37e40e9972feabc8b6a74e2730d/orjson-3.11.8-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:003646067cc48b7fcab2ae0c562491c9b5d2cbd43f1e5f16d98fd118c5522d34", size = 229229, upload-time = "2026-03-31T16:14:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d7/57e7f2458e0a2c41694f39fc830030a13053a84f837a5b73423dca1f0938/orjson-3.11.8-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:ed193ce51d77a3830cad399a529cd4ef029968761f43ddc549e1bc62b40d88f8", size = 128871, upload-time = "2026-03-31T16:14:51.888Z" }, + { url = "https://files.pythonhosted.org/packages/53/4a/e0fdb9430983e6c46e0299559275025075568aad5d21dd606faee3703924/orjson-3.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30491bc4f862aa15744b9738517454f1e46e56c972a2be87d70d727d5b2a8f8", size = 132104, upload-time = "2026-03-31T16:14:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/08/4a/2025a60ff3f5c8522060cda46612d9b1efa653de66ed2908591d8d82f22d/orjson-3.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eda5b8b6be91d3f26efb7dc6e5e68ee805bc5617f65a328587b35255f138bf4", size = 130483, upload-time = "2026-03-31T16:14:54.605Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/b9cde05bdc7b2385c66014e0620627da638d3d04e4954416ab48c31196c5/orjson-3.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee8db7bfb6fe03581bbab54d7c4124a6dd6a7f4273a38f7267197890f094675f", size = 135481, upload-time = "2026-03-31T16:14:55.901Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f2/a8238e7734de7cb589fed319857a8025d509c89dc52fdcc88f39c6d03d5a/orjson-3.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d8b5231de76c528a46b57010bbd83fb51e056aa0220a372fd5065e978406f1c", size = 146819, upload-time = "2026-03-31T16:14:57.548Z" }, + { url = "https://files.pythonhosted.org/packages/db/10/dbf1e2a3cafea673b1b4350e371877b759060d6018a998643b7040e5de48/orjson-3.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58a4a208a6fbfdb7a7327b8f201c6014f189f721fd55d047cafc4157af1bc62a", size = 132846, upload-time = "2026-03-31T16:14:58.91Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fc/55e667ec9c85694038fcff00573d221b085d50777368ee3d77f38668bf3c/orjson-3.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f8952d6d2505c003e8f0224ff7858d341fa4e33fef82b91c4ff0ef070f2393c", size = 133580, upload-time = "2026-03-31T16:15:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a6/c08c589a9aad0cb46c4831d17de212a2b6901f9d976814321ff8e69e8785/orjson-3.11.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0022bb50f90da04b009ce32c512dc1885910daa7cb10b7b0cba4505b16db82a8", size = 142042, upload-time = "2026-03-31T16:15:01.906Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cc/2f78ea241d52b717d2efc38878615fe80425bf2beb6e68c984dde257a766/orjson-3.11.8-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ff51f9d657d1afb6f410cb435792ce4e1fe427aab23d2fcd727a2876e21d4cb6", size = 423845, upload-time = "2026-03-31T16:15:03.703Z" }, + { url = "https://files.pythonhosted.org/packages/70/07/c17dcf05dd8045457538428a983bf1f1127928df5bf328cb24d2b7cddacb/orjson-3.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6dbe9a97bdb4d8d9d5367b52a7c32549bba70b2739c58ef74a6964a6d05ae054", size = 147729, upload-time = "2026-03-31T16:15:05.203Z" }, + { url = "https://files.pythonhosted.org/packages/90/6c/0fb6e8a24e682e0958d71711ae6f39110e4b9cd8cab1357e2a89cb8e1951/orjson-3.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5c370674ebabe16c6ccac33ff80c62bf8a6e59439f5e9d40c1f5ab8fd2215b7", size = 136425, upload-time = "2026-03-31T16:15:07.052Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/4d3cc3a3d616035beb51b24a09bb872942dc452cf2df0c1d11ab35046d9f/orjson-3.11.8-cp311-cp311-win32.whl", hash = "sha256:0e32f7154299f42ae66f13488963269e5eccb8d588a65bc839ed986919fc9fac", size = 131870, upload-time = "2026-03-31T16:15:08.678Z" }, + { url = "https://files.pythonhosted.org/packages/13/26/9fe70f81d16b702f8c3a775e8731b50ad91d22dacd14c7599b60a0941cd1/orjson-3.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:25e0c672a2e32348d2eb33057b41e754091f2835f87222e4675b796b92264f06", size = 127440, upload-time = "2026-03-31T16:15:09.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/c6/b038339f4145efd2859c1ca53097a52c0bb9cbdd24f947ebe146da1ad067/orjson-3.11.8-cp311-cp311-win_arm64.whl", hash = "sha256:9185589c1f2a944c17e26c9925dcdbc2df061cc4a145395c57f0c51f9b5dbfcd", size = 127399, upload-time = "2026-03-31T16:15:11.412Z" }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/95fba509bb2305fab0073558f1e8c3a2ec4b2afe58ed9fcb7d3b8beafe94/orjson-3.11.8-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3f23426851d98478c8970da5991f84784a76682213cd50eb73a1da56b95239dc", size = 229180, upload-time = "2026-03-31T16:15:36.426Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9d/b237215c743ca073697d759b5503abd2cb8a0d7b9c9e21f524bcf176ab66/orjson-3.11.8-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ebaed4cef74a045b83e23537b52ef19a367c7e3f536751e355a2a394f8648559", size = 128754, upload-time = "2026-03-31T16:15:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/27d65b6d11e63f133781425f132807aef793ed25075fec686fc8e46dd528/orjson-3.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97c8f5d3b62380b70c36ffacb2a356b7c6becec86099b177f73851ba095ef623", size = 131877, upload-time = "2026-03-31T16:15:39.484Z" }, + { url = "https://files.pythonhosted.org/packages/dd/cc/faee30cd8f00421999e40ef0eba7332e3a625ce91a58200a2f52c7fef235/orjson-3.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:436c4922968a619fb7fef1ccd4b8b3a76c13b67d607073914d675026e911a65c", size = 130361, upload-time = "2026-03-31T16:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bb/a6c55896197f97b6d4b4e7c7fd77e7235517c34f5d6ad5aadd43c54c6d7c/orjson-3.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ab359aff0436d80bfe8a23b46b5fea69f1e18aaf1760a709b4787f1318b317f", size = 135521, upload-time = "2026-03-31T16:15:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7c/ca3a3525aa32ff636ebb1778e77e3587b016ab2edb1b618b36ba96f8f2c0/orjson-3.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f89b6d0b3a8d81e1929d3ab3d92bbc225688bd80a770c49432543928fe09ac55", size = 146862, upload-time = "2026-03-31T16:15:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0c/18a9d7f18b5edd37344d1fd5be17e94dc652c67826ab749c6e5948a78112/orjson-3.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c009e7a2ca9ad0ed1376ce20dd692146a5d9fe4310848904b6b4fee5c5c137", size = 132847, upload-time = "2026-03-31T16:15:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/23/91/7e722f352ad67ca573cee44de2a58fb810d0f4eb4e33276c6a557979fd8a/orjson-3.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b895b781b3e395c067129d8551655642dfe9437273211d5404e87ac752b53", size = 133637, upload-time = "2026-03-31T16:15:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/af/04/32845ce13ac5bd1046ddb02ac9432ba856cc35f6d74dde95864fe0ad5523/orjson-3.11.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:88006eda83858a9fdf73985ce3804e885c2befb2f506c9a3723cdeb5a2880e3e", size = 141906, upload-time = "2026-03-31T16:15:49.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/5e/c551387ddf2d7106d9039369862245c85738b828844d13b99ccb8d61fd06/orjson-3.11.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:55120759e61309af7fcf9e961c6f6af3dde5921cdb3ee863ef63fd9db126cae6", size = 423722, upload-time = "2026-03-31T16:15:51.176Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/ecfe62434096f8a794d4976728cb59bcfc4a643977f21c2040545d37eb4c/orjson-3.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:98bdc6cb889d19bed01de46e67574a2eab61f5cc6b768ed50e8ac68e9d6ffab6", size = 147801, upload-time = "2026-03-31T16:15:52.939Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/0dce10b9f6643fdc59d99333871a38fa5a769d8e2fc34a18e5d2bfdee900/orjson-3.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:708c95f925a43ab9f34625e45dcdadf09ec8a6e7b664a938f2f8d5650f6c090b", size = 136460, upload-time = "2026-03-31T16:15:54.431Z" }, + { url = "https://files.pythonhosted.org/packages/01/d6/6dde4f31842d87099238f1f07b459d24edc1a774d20687187443ab044191/orjson-3.11.8-cp313-cp313-win32.whl", hash = "sha256:01c4e5a6695dc09098f2e6468a251bc4671c50922d4d745aff1a0a33a0cf5b8d", size = 131956, upload-time = "2026-03-31T16:15:56.081Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f9/4e494a56e013db957fb77186b818b916d4695b8fa2aa612364974160e91b/orjson-3.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:c154a35dd1330707450bb4d4e7dd1f17fa6f42267a40c1e8a1daa5e13719b4b8", size = 127410, upload-time = "2026-03-31T16:15:57.54Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/803203d00d6edb6e9e7eef421d4e1adbb5ea973e40b3533f3cfd9aeb374e/orjson-3.11.8-cp313-cp313-win_arm64.whl", hash = "sha256:4861bde57f4d253ab041e374f44023460e60e71efaa121f3c5f0ed457c3a701e", size = 127338, upload-time = "2026-03-31T16:15:59.106Z" }, + { url = "https://files.pythonhosted.org/packages/6d/35/b01910c3d6b85dc882442afe5060cbf719c7d1fc85749294beda23d17873/orjson-3.11.8-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ec795530a73c269a55130498842aaa762e4a939f6ce481a7e986eeaa790e9da4", size = 229171, upload-time = "2026-03-31T16:16:00.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/56/c9ec97bd11240abef39b9e5d99a15462809c45f677420fd148a6c5e6295e/orjson-3.11.8-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c492a0e011c0f9066e9ceaa896fbc5b068c54d365fea5f3444b697ee01bc8625", size = 128746, upload-time = "2026-03-31T16:16:02.673Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e4/66d4f30a90de45e2f0cbd9623588e8ae71eef7679dbe2ae954ed6d66a41f/orjson-3.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:883206d55b1bd5f5679ad5e6ddd3d1a5e3cac5190482927fdb8c78fb699193b5", size = 131867, upload-time = "2026-03-31T16:16:04.342Z" }, + { url = "https://files.pythonhosted.org/packages/19/30/2a645fc9286b928675e43fa2a3a16fb7b6764aa78cc719dc82141e00f30b/orjson-3.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5774c1fdcc98b2259800b683b19599c133baeb11d60033e2095fd9d4667b82db", size = 124664, upload-time = "2026-03-31T16:16:05.837Z" }, + { url = "https://files.pythonhosted.org/packages/db/44/77b9a86d84a28d52ba3316d77737f6514e17118119ade3f91b639e859029/orjson-3.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7381c83dd3d4a6347e6635950aa448f54e7b8406a27c7ecb4a37e9f1ae08b", size = 129701, upload-time = "2026-03-31T16:16:07.407Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/eff3d9bfe47e9bc6969c9181c58d9f71237f923f9c86a2d2f490cd898c82/orjson-3.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14439063aebcb92401c11afc68ee4e407258d2752e62d748b6942dad20d2a70d", size = 141202, upload-time = "2026-03-31T16:16:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/90d4b4c60c84d62068d0cf9e4d8f0a4e05e76971d133ac0c60d818d4db20/orjson-3.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa72e71977bff96567b0f500fc5bfd2fdf915f34052c782a4c6ebbdaa97aa858", size = 127194, upload-time = "2026-03-31T16:16:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c7/ea9e08d1f0ba981adffb629811148b44774d935171e7b3d780ae43c4c254/orjson-3.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7679bc2f01bb0d219758f1a5f87bb7c8a81c0a186824a393b366876b4948e14f", size = 133639, upload-time = "2026-03-31T16:16:13.434Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8c/ddbbfd6ba59453c8fc7fe1d0e5983895864e264c37481b2a791db635f046/orjson-3.11.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14f7b8fcb35ef403b42fa5ecfa4ed032332a91f3dc7368fbce4184d59e1eae0d", size = 141914, upload-time = "2026-03-31T16:16:14.955Z" }, + { url = "https://files.pythonhosted.org/packages/4e/31/dbfbefec9df060d34ef4962cd0afcb6fa7a9ec65884cb78f04a7859526c3/orjson-3.11.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c2bdf7b2facc80b5e34f48a2d557727d5c5c57a8a450de122ae81fa26a81c1bc", size = 423800, upload-time = "2026-03-31T16:16:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/87/cf/f74e9ae9803d4ab46b163494adba636c6d7ea955af5cc23b8aaa94cfd528/orjson-3.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ccd7ba1b0605813a0715171d39ec4c314cb97a9c85893c2c5c0c3a3729df38bf", size = 147837, upload-time = "2026-03-31T16:16:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/9214f017b5db85e84e68602792f742e5dc5249e963503d1b356bee611e01/orjson-3.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbc8c9c02463fef4d3c53a9ba3336d05496ec8e1f1c53326a1e4acc11f5c600", size = 136441, upload-time = "2026-03-31T16:16:20.151Z" }, + { url = "https://files.pythonhosted.org/packages/24/dd/3590348818f58f837a75fb969b04cdf187ae197e14d60b5e5a794a38b79d/orjson-3.11.8-cp314-cp314-win32.whl", hash = "sha256:0b57f67710a8cd459e4e54eb96d5f77f3624eba0c661ba19a525807e42eccade", size = 131983, upload-time = "2026-03-31T16:16:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0f/b6cb692116e05d058f31ceee819c70f097fa9167c82f67fabe7516289abc/orjson-3.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:735e2262363dcbe05c35e3a8869898022af78f89dde9e256924dc02e99fe69ca", size = 127396, upload-time = "2026-03-31T16:16:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d1/facb5b5051fabb0ef9d26c6544d87ef19a939a9a001198655d0d891062dd/orjson-3.11.8-cp314-cp314-win_arm64.whl", hash = "sha256:6ccdea2c213cf9f3d9490cbd5d427693c870753df41e6cb375bd79bcbafc8817", size = 127330, upload-time = "2026-03-31T16:16:25.496Z" }, ] [[package]] @@ -2393,7 +2402,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.2.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -2402,9 +2411,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] @@ -3054,7 +3063,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -3062,9 +3071,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -3484,14 +3493,14 @@ wheels = [ [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]]