From 94c5d5998401f473f1d0368f8872d863d65b9d68 Mon Sep 17 00:00:00 2001 From: Jacob Alber Date: Thu, 3 Jul 2025 13:51:49 -0400 Subject: [PATCH] feat: Model Client and associated Content Types (#53) * feat: ModelClient and content types * refactor: Pythonify ChatResponseFormat and ChatRole * feat: Add guardrail interfaces * refactor: Remove CancellationToken * feat: Solidify the Usage APIs * Adds well-known keys for additional_counts, and guidance for how to avoid collisions between providers * Implement sum-aggregation for usage * refactor: Move AITool out of model_client * refactor: Copy editing * fix: CI checks (pyupgrade, ruff, etc.) * ci: Fix pre-commit to use pyright in uv venv The existing pyright precommit hook inside of python-pyright is no longer being maintained by the owner (see https://github.com/RobertCraigie/pyright-python/issues/265) The fix is to define the hook ourselves, relying on `uv run` to drive it. In order for that to work right we need to use the "system" language to break out of the sandbox. * fix: Pyright error fixes * docs: Update models and types design docs * Python: Refinement of content types and model client (#112) * refinement of structure and buildup with ports from semantigen * refined the data and uri contents * refined chat response and updates * moved things and added tests * moved out of src folder * fixed imports and tests * small tweaks * missing build system * upgrade * add mypy * fixed typing for types * fix tests * fixed tool * disable json checks on vscode * remove print --------- Co-authored-by: Eduard van Valkenburg Co-authored-by: eavanvalkenburg --- docs/design/models.md | 282 +++- docs/design/types.md | 175 +- python/.pre-commit-config.yaml | 1 + python/.vscode/launch.json | 15 + python/.vscode/tasks.json | 122 ++ python/agent_framework/__init__.py | 24 + python/agent_framework/__init__.pyi | 60 +- python/agent_framework/_cancellation_token.py | 50 + python/agent_framework/_pydantic.py | 10 + python/agent_framework/_tools.py | 122 ++ python/agent_framework/_types.py | 1445 +++++++++++++++++ python/agent_framework/guard_rails.py | 25 + python/extensions/azure/pyproject.toml | 3 + python/extensions/openai/pyproject.toml | 3 + python/pyproject.toml | 39 +- python/tests/unit/test_tool.py | 43 + python/tests/unit/test_types.py | 466 ++++++ python/tests/unit/test_version.py | 8 + python/uv.lock | 405 ++--- 19 files changed, 3009 insertions(+), 289 deletions(-) create mode 100644 python/.vscode/launch.json create mode 100644 python/.vscode/tasks.json create mode 100644 python/agent_framework/_cancellation_token.py create mode 100644 python/agent_framework/_pydantic.py create mode 100644 python/agent_framework/guard_rails.py create mode 100644 python/tests/unit/test_tool.py create mode 100644 python/tests/unit/test_types.py create mode 100644 python/tests/unit/test_version.py diff --git a/docs/design/models.md b/docs/design/models.md index d3da88f941..1861fbec1b 100644 --- a/docs/design/models.md +++ b/docs/design/models.md @@ -20,8 +20,8 @@ The framework provides a set of pre-built model clients: - `BedrockClient` - `NIMClient` -Prompt template is a component that is used by model clients to generate prompts with parameters set based on some injected context. -prompts with parameters set based on some injected context. +Prompt template is a component that is used by model clients to generate prompts +with parameters set based on some injected context. This gets into the actual interface and implementation detail of model clients, so we just mention it here. @@ -29,41 +29,55 @@ The design goal is to provide integration with a wide range of model providers, including both open-source and commercial models, while maintaining a consistent interface for developers to use. -## `ModelClient` base class (draft) +## `ModelClient` Protocol (draft) ```python -class ModelClient(ABC): - """The base class for all model clients in the framework.""" - - @abstractmethod - async def create( +class ModelClient(Protocol): + """A protocol for a model client that can generate chat responses.""" + async def generate_response( self, - thread: Thread, - context: Context, - stream: bool = False, - tools: Optional[list[Tool]] = None, - output_format: Optional[OutputFormat] = None, - ) -> Message: - """Generate a response from the model based on the provided messages. + messages: Sequence[ChatMessage], + **options, # kwargs? + ) -> ChatResponse: + """Sends chat messages and returns the response. Args: - thread: The conversation context to generate a response. - context: The context for the current invocation of the model client. - This is for accessing event channels for streaming tokens. - stream: Whether to stream the response tokens. - tools: Optional list of tools to use for tool calling. - output_format: Optional structured output format for the response. - If provided, the model will generate a response in this format - and returns a structured response message. + messages: The sequence of chat messages to send. + **options: Additional options for the chat request, such as model_id, temperature, etc. + See `ChatOptions` for more details. Returns: - The generated response message. + The response messages generated by the client. + + Raises: + ValueError: If the input message sequence is `None`. """ ... - + + async def generate_streaming_response( + self, + messages: Sequence[ChatMessage], + **options, # kwargs? + ) -> AsyncIterable[ChatResponseUpdate]: + """Sends chat messages and streams the response. + + Args: + messages: The sequence of chat messages to send. + **options: Additional options for the chat request, such as model_id, temperature, etc. + See `ChatOptions` for more details. + + Returns: + An async iterable of chat response updates containing the content of the response messages + generated by the client. + + Raises: + ValueError: If the input message sequence is `None`. + """ + ... + def add_input_guardrails( - self, - guardrails: list[InputGuardrail[Message]] + self, + guardrails: list[InputGuardrail[ChatMessage]] ) -> None: """Add input guardrails to the model client. @@ -71,10 +85,10 @@ class ModelClient(ABC): guardrails: The list of input guardrails to add. """ ... - + def add_output_guardrails( - self, - guardrails: list[OutputGuardrail[Message]] + self, + guardrails: list[OutputGuardrail[ChatResponse | Sequence[ChatResponseUpdate]]] ) -> None: """Add output guardrails to the model client. @@ -82,4 +96,210 @@ class ModelClient(ABC): guardrails: The list of output guardrails to add. """ ... -``` \ No newline at end of file +``` + +## `ChatMessage`/ and `ChatResponse`/`StructuredChatResponse`/`ChatResponseUpdate` Types + +For the data types used to represent message and response contents, see `docs/design/types.md`. + +```python + +class ChatMessage(BaseModel): + author_name: str | None # The name of the author of the message + contents: list[AIContent] # The contents of the message, which can include text, images, function calls, etc. + message_id: str | None # The ID of the message + raw_representation: Any | None = None # The raw representation of the chat message from an underlying implementation + role: ChatRole # The role of the author of the message + additional_properties: dict[str, Any] | None = None + + +class ChatRole(BaseModel): + """Describes the intended purpose of a message within a chat interaction.""" + value: str + + SYSTEM: ClassVar[Self] # The role that instructs or sets the behaviour of the AI system. + USER: ClassVar[Self] # The role that provides user input for chat interactions. + ASSISTANT: ClassVar[Self] # The role that provides responses to system-instructed, user-prompted input. + TOOL: ClassVar[Self] # The role that provides additional information and references in response to tool use requests. +``` + +`ChatRole` is an enum-like class that defines the roles of the author in a chat message. We use a class with class variables to reduce the coupling between versions of the framework and the capabilities available in the various providers. This allows us to work with providers that support extra roles without having to change the framework code. (See also `ChatFinishReason`.) + +The response types are designed to support both structured and unstructured responses, allowing for streaming in the unstructured case. + +```python +class ChatResponse(BaseModel): + """Represents the response to a chat request.""" + messages: list[ChatMessage] # The chat response messages. + + additional_properties: dict[str, Any] | None = None # Any additional properties associated with the chat response. + conversation_id: str | None = None # An identifier for the state of the conversation. + created_at: CreatedAtT | None = None # A timestamp for the chat response. (TODO: Use a datetime type?) + finish_reason: ChatFinishReason | None = None # The reason for the chat response. + model_id: str | None = None #The model ID used in the creation of the chat response. + raw_representation: Any | None = None # The raw representation of the chat response from an underlying implementation. + response_id: str | None = None # The ID of the chat response. + usage_details: UsageDetails | None = None # The usage details for the chat response. + + +TValue = TypeVar("TValue", bound=pydantic.BaseModel) # The value type needs to be deserializable (so we rely on Pydantic) + + +class StructuredResponse(GenericModel, Generic[TValue], ChatResponse): + """Represents a structured response to a chat request.""" + value: TValue + + +class ChatFinishReason(BaseModel): + """Represents the reason a chat response completed.""" + value: str + + CONTENT_FILTER: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model filtering content, whether for safety, prohibited content, + sensitive content, or other such issues.""" + LENGTH: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model reaching the maximum length allowed for the request and/or + response (typically in terms of tokens).""" + STOP: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model encountering a natural stop point or provided stop sequence.""" + TOOL_CALLS: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model requesting the use of a tool that was defined in the request.""" +``` + +For ease of use, all of `ChatMessage`, `ChatResponse`, `StructuredResponse` (and `ChatResponseUpdate`) provide a helper method to extract the text content. Note that all it does is concatenate any `TextContent` instances found in the contents. It will ignore any other content types, including `TextReasoningContent`. + +```python + @property + def text(self) -> str: + """Returns the concatenated text of all messages in the response.""" + return " ".join(content.text for content in self.contents if isinstance(content, TextContent)) +``` + +The streaming response type is designed to represent a "differential" of a full response object, in that the relationship between the two is akin to: `Join(ChatResponseUpdate) === ChatResponse`. + +```python +class ChatResponseUpdate(BaseModel): + """Represents a single streaming response chunk from a `ModelClient`.""" + contents: list[AIContent] + """The chat response update content items.""" + + additional_properties: dict[str, Any] | None = None + """Any additional properties associated with the chat response update.""" + author_name: str | None = None + """The name of the author of the response update.""" + conversation_id: str | None = None + """An identifier for the state of the conversation of which this update is a part.""" + created_at: CreatedAtT | None = None # use a datetimeoffset type? + """A timestamp for the chat response update.""" + finish_reason: ChatFinishReason | None = None + """The finish reason for the operation.""" + message_id: str | None = None + """The ID of the message of which this update is a part.""" + model_id: str | None = None + """The model ID associated with this response update.""" + raw_representation: Any | None = None + """The raw representation of the chat response update from an underlying implementation.""" + response_id: str | None = None + """The ID of the response of which this update is a part.""" + role: ChatRole | None = None + """The role of the author of the response update.""" +``` + +`StreamingResponseUpdate` provides helpers to join a set of updates into a single `ChatResponse` object, as well as to create the next update in a stream given the content of the new update. + +## `ModelOptions`: Configuring a Model Client request + +Although the `ModelClient` protocol uses named arguments to pass options to the `generate_response` and `generate_streaming_response` methods, we also provide a `ModelOptions` class representing the total set of configurable options that the user could reasonably expect on a `ModelClient` implementation. It also provides a convenient way to document them in one place. + +```python + +class ChatOptions(TypedDict, total=False): + """Represents the options for a chat request. + + Remarks: + This class is here for the purposes of documentation and ease of use. Options should still + be passed as keyword arguments to the `ModelClient.generate_response` and + `ModelClient.generate_streaming_response` methods. + """ + allow_multiple_tool_calls: bool | None = None + """Indicates whether a single response is allowed to include multiple tool calls. If `False`, + the `ModelClient` is asked to return a maximum of one tool call per request. If `True`, there is + no limit. If `None`, the provider may select its own default.""" + conversation_id: str | None = None + """An optional identifier used to associate a request with an existing conversation.""" + frequency_penalty: float | None = None + """A penalty for repeated tokens in chat responses proportional to how many times they've appeared.""" + max_output_tokens: int | None = None + """The maximum number of tokens in the generated chat response.""" + model_id: str | None = None + """The model ID for the chat request.""" + presence_penalty: float | None = None + """a value that influences the probability of generated tokens appearing based on their existing + presence in generated text.""" + response_format: ChatResponseFormat | None = None + """The response format for the chat request.""" + seed: int | None = None + """A seed value used by a service to control the reproducibility of results.""" + stop_sequences: list[str] | None = None + """The list of stop sequences.""" + temperature: float | None = None + """The temperature for generating chat responses.""" + tool_mode: ChatToolMode | None = None + """The tool mode for the chat request.""" + tools: list[AITool] | None = None + """The list of tools to include with a chat request.""" + top_k: int | None = None + """The number of most probable tokens that the model considers when generating the next part of + the text.""" + top_p: float | None = None + """The 'nucleus sampling' factor (or "top p") for generating chat responses.""" +``` + +`ChatResponseFormat` and `ChatToolMode` are used to configure the output format (structured or not) and tool use mode. + +```python + +class ChatResponseFormatJson(BaseModel): + """Represents a response format for structured JSON data.""" + type: Literal["json"] = "json" + + schema_name: str | None = None + """The name of the schema.""" + schema_description: str | None = None + """The description of the schema.""" + schema_: dict[str, Any] | None = Field(default=None, alias="schema") + """The JSON schema associated with the response, or `None` if there is none.""" + + +class ChatResponseFormatText(BaseModel): + """Represents a response format with no constraints around the format.""" + type: Literal["text"] = "text" + + +ChatResponseFormat = ChatResponseFormatJson | ChatResponseFormatText + + +class AutoChatToolMode(BaseModel): + """Indicates that a `ModelClient` is free to select any of the available tools, or none at all.""" + value: Literal["auto"] = "auto" + + +class NoneChatToolMode(BaseModel): + """Indicates that a `ModelClient` should not request the invocation of any tools.""" + value: Literal["none"] = "none" + + +class RequiredChatToolMode(BaseModel): + """Represents a mode where a chat tool must be called. + + This class can optionally nominate a specific function or indicate that any of the functions can be + selected. + """ + value: Literal["require_any", "require_specific"] = "require_any" + + required_function_name: str | None = None + """The name of a specific function that must be called.""" + + +ChatToolMode = AutoChatToolMode | NoneChatToolMode | RequiredChatToolMode +``` diff --git a/docs/design/types.md b/docs/design/types.md index 186e84bd53..10718e4ad3 100644 --- a/docs/design/types.md +++ b/docs/design/types.md @@ -18,100 +18,157 @@ To start, we should follow [MEAI](https://learn.microsoft.com/en-us/dotnet/api/m This document describes the data types from Python perspective, while for .NET, we should directly use the MEAI data types. +In Python, we will use `pydantic` to define and operate with these data types. + ## Content types +The core data type is `AIContent`, which represents content used by AI services. While the Agent Framework will primarily use the abstracted types, shared across implementations of the underlying Model Client, the `raw_representation` field allows for more tightly-coupled implementations where appropriate. + ```python -class AIContent(ABC): - """Base class for all AI content types.""" - additional_properties: Dict[str, Any] = field(default_factory=dict) - """Additional properties for extensibility, allowing custom fields.""" +class AIContent(BaseModel): + raw_representation: Any | None = None + additional_properties: dict[str, Any] | None = None +``` -class DataContent(AIContent): - """Data content type.""" - data: bytes # Raw binary data. - media_type: str # MIME type of the data, e.g., "image/png", "application/json" - uri: str # URI constructed from the data. - base64: str # Base64 encoded data for easy transport. - -class ErrorContent(AIContent): - """Error content type.""" - details: str # Detailed error message. - error_code: str # Error code for programmatic handling. - message: str # Human-readable error message. - -class FunctionCallContent(AIContent): - """Function call content type.""" - name: str # Name of the function to call. - arguments: Dict[str, Any] # Arguments for the function call, serialized as JSON. - call_id: str # Unique identifier for the function call. - exception: Optional[Exception] = None # Optional exception for error occurred while mapping the original function call data to this content type. - -class FunctionResultContent(AIContent): - """Function result content type.""" - call_id: str # Unique identifier for the function call. - result: Any # Result of the function call, or a generic error message. - exception: Optional[Exception] = None # Optional exception for error occurred while executing the function call. +The basic data-bearing types (`TextContent`, `TextReasoningContent`, `DataContent`, and `UriContent`) represent an interface for developers to input data into the system, as well as for agents to output resulting data. In AutoGen, messages containing these content types were considered `ChatMesssage` objects, contrasting with `AgentEvent` objects that were primarily used for internal events (such as tool calls, etc.) +```python class TextContent(AIContent): - """Text content type.""" text: str + class TextReasoningContent(AIContent): - """Text reasoning content type.""" text: str +AnyTextualContent = TextContent | TextReasoningContent | "UriContent" + + +class DataContent(AIContent): + data: bytes + media_type: str + + @property + def base64_data(self) -> str: + """Returns the data represented by this instance encoded as a Base64 string.""" + ... + + @property + def uri(self) -> str: + """Returns the data as a data URI.""" + ... + + class UriContent(AIContent): - """URI content type.""" - uri: str # URI of the content, e.g., a link to an image or document. - media_type: str # MIME type of the content, e.g., "image/png", "application/pdf". + uri: str + media_type: str -class UsageDetails: - input_token_count: Optional[int] = None - output_token_count: Optional[int] = None - additional_counts: Optional[Dict[str, int]] = None - total_token_count: Optional[int] = None +LocatableContent = DataContent | UriContent # TODO: Consider other names that are more descriptive. +``` + +Note that `TextContent` and `TextReasoningContent` do not have an inheritance relationship, and similarly `DataContent` and `UriContent` do not inherit from one-another. In this case, we opt to remain consistent with the .NET-side M.E.AI design. To abstract over both pairs, we provide the `AnyTextualContent` and `LocatableContent` type aliases. + +Function calls and results have dedicated content types to represent the ModelClient's request for tool use as well as the means to flow the results back to the agent. +```python +class FunctionCallContent(AIContent): + call_id: str + name: str + arguments: dict[str, Any | None] | None = None + exception: Exception | None = None + + +class FunctionResultContent(AIContent): + call_id: str + result: Any | None = None + exception: Exception | None = None +``` + +The types contain all necessary error reporting channels (`FunctionCallContext.exception` when the raw function call request cannot be successfully mapped to the input to the tool - likely due to argument mismatch - and `FunctionResultContent.exception` when the tool execution fails). `ErrorContent` is a more general-purpose error reporting type, but one that should be used for reporting non-fatal errors. The way to think about this is that this is akin to a "400" error or a transient "500" error in HTTP parlance. + + +```python +class ErrorContent(AIContent): + error_code: str | None = None + details: str | None = None + message: str | None +``` + + +Communicating the usage details when invoking a model client is important, but it has traditionally not been well supported in various frameworks, typically ending up relying on the implementor of the assistant consuming the underlying LLM to handle this. Ideally, the Model Client would provide an a-prori means to compute the usage given a message: + + +```python + +class TokenEstimator(Protocol): + """Estimates the count of tokens in the provided message""" + def estimate(message: ChatMessage) -> int: + ... + + def estimate_all(message: Sequence[ChatMessage]) -> Generator[int, None, int] + ... + + +@runtime_checkable +class ModelClientWithEstimator(Protocol, ModelClient): + @property + def token_estimator(self) -> TokenEstimator: + """Returns the token estimator for this model client.""" + ... +``` + +As this is not available in the current MEAI design, not generally a capability of native clients of existing hosted models, we will rely on the `UsageDetails` pattern to let the Model Client communicate this data into the Agent consuming it. Custom agents will be responsible for proper aggregation; built-in ones should provide the patterns and ideally implementation helper types. The `UsageDetails` class will provide a canonical additional operator. + +```python class UsageContent(AIContent): """Usage content type.""" details: UsageDetails + +class UsageDetails: + """Provides usage details about a request/response.""" + input_token_count: int | None = None + """The number of tokens in the input.""" + output_token_count: int | None = None + """The number of tokens in the output.""" + total_token_count: int | None = None + """The total number of tokens used to produce the response.""" + additional_counts: AdditionalCounts | None = None + """A dictionary of additional usage counts.""" ``` -## `ChatMessage` -A message in a thread that is sent to or received from a model client. - -> Should we use `Message` instead of `ChatMessage`? - -> We may need to extend this class to support more framework-level functionalities -> such as handoff, stopping, and so on? +The `AdditionalCounts` class is a `TypedDict` that allows for us to define well-known keys for additional counts (e.g. `thought_token_count` and `image_token_count`), while also allowing for arbitrary keys to be used by the underlying AI service prvoider. We recommend that services prefix their keys with a service identifier (e.g. `openai.`) to avoid collisions, unles using a well-known key. ```python -class ChatRole(Enum): - """The role of the author in a chat message.""" - USER = "user" - ASSISTANT = "assistant" - SYSTEM = "system" - TOOL = "tool" +class AdditionalCounts(TypedDict, total=False): + """Represents well-known additional counts for usage. This is not an exhaustive list. -class ChatMessage: - message_id: str # Unique identifier for the message. - author: str # Unique identifier for the author of the message. - role: ChatRole # Role of the author in the chat, e.g., user, assistant, system, tool. - contents: List[AIContent] # List of content types in the message, e.g., text, images, function calls. + Remarks: + To make it possible to avoid colisions between similarly-named, but unrelated, additional counts + between different AI services, any keys not explicitly defined here should be prefixed with the + name of the AI service, e.g., "openai." or "azure.". The separator "." was chosen because it cannot + be a legal character in a JSON key. + + Over time additional counts may be added to this class. + """ + thought_token_count: int + """The number of tokens used for thought processing.""" + image_token_count: int + """The number of token equivalents used for image processing.""" ``` + ## Tool types -Align with the [MEAI tool types](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.ai.aifunction?view=net-9.0-pp) +Align with the [MEAI tool types](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.ai.aifunction?view=net-9.0-pp) in terms of the core attributes and methods. See [Tools](./tools.md) for more details. -## Model client types +## Model client and `ChatMessage` types Align with the MEAI model client types in terms of the core attributes and methods. diff --git a/python/.pre-commit-config.yaml b/python/.pre-commit-config.yaml index fe1a034f3e..2b92478c10 100644 --- a/python/.pre-commit-config.yaml +++ b/python/.pre-commit-config.yaml @@ -9,6 +9,7 @@ repos: files: \.yaml$ - id: check-json files: \.json$ + exclude: ^python\/\.vscode\/.* - id: end-of-file-fixer files: \.py$ - id: mixed-line-ending diff --git a/python/.vscode/launch.json b/python/.vscode/launch.json new file mode 100644 index 0000000000..6b76b4fabc --- /dev/null +++ b/python/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/python/.vscode/tasks.json b/python/.vscode/tasks.json new file mode 100644 index 0000000000..c0707f5513 --- /dev/null +++ b/python/.vscode/tasks.json @@ -0,0 +1,122 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Run Checks", + "type": "shell", + "command": "uv", + "args": [ + "run", + "pre-commit", + "run", + "-a" + ], + "problemMatcher": { + "owner": "python", + "fileLocation": [ + "relative", + "${workspaceFolder}" + ], + "pattern": { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + }, + "presentation": { + "panel": "shared" + } + }, + { + "label": "Mypy", + "type": "shell", + "command": "uv", + "args": [ + "run", + "mypy", + "-p", + "agent_framework", + ], + "problemMatcher": { + "owner": "python", + "fileLocation": [ + "relative", + "${workspaceFolder}" + ], + "pattern": { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + }, + "presentation": { + "panel": "shared" + } + }, + { + "label": "Tests - Code Coverage", + "type": "shell", + "command": "uv", + "args": [ + "run", + "pytest", + "--cov=semantic_kernel", + "--cov-report=term-missing:skip-covered", + "tests/unit/" + ], + "group": "test", + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "problemMatcher": [] + }, + { + "label": "Create Venv", + "type": "shell", + "command": "uv venv PYTHON=${input:py_version}", + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Install all dependencies", + "type": "shell", + "command": "uv", + "args": [ + "sync", + "--all-extras", + "--dev", + "-U", + "--prerelease=if-necessary-or-explicit" + ], + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ], + "inputs": [ + { + "type": "pickString", + "options": [ + "3.10", + "3.11", + "3.12", + "3.13" + ], + "id": "py_version", + "description": "Python version", + "default": "3.10" + } + ] +} \ No newline at end of file diff --git a/python/agent_framework/__init__.py b/python/agent_framework/__init__.py index e0bf6acdf5..7135cf7618 100644 --- a/python/agent_framework/__init__.py +++ b/python/agent_framework/__init__.py @@ -10,6 +10,30 @@ except importlib.metadata.PackageNotFoundError: _IMPORTS = { "get_logger": "._logging", + "AITool": "._tools", + "ai_function": "._tools", + "AIContent": "._types", + "AIContents": "._types", + "TextContent": "._types", + "TextReasoningContent": "._types", + "DataContent": "._types", + "UriContent": "._types", + "UsageContent": "._types", + "UsageDetails": "._types", + "FunctionCallContent": "._types", + "FunctionResultContent": "._types", + "ChatFinishReason": "._types", + "ChatMessage": "._types", + "ChatResponse": "._types", + "StructuredResponse": "._types", + "ChatResponseUpdate": "._types", + "ChatRole": "._types", + "ErrorContent": "._types", + "ModelClient": "._types", + "ChatOptions": "._types", + "ChatToolMode": "._types", + "InputGuardrail": ".guard_rails", + "OutputGuardrail": ".guard_rails", } diff --git a/python/agent_framework/__init__.pyi b/python/agent_framework/__init__.pyi index 4422ccb2ce..dc995f7ee5 100644 --- a/python/agent_framework/__init__.pyi +++ b/python/agent_framework/__init__.pyi @@ -1,11 +1,57 @@ # Copyright (c) Microsoft. All rights reserved. -import importlib.metadata - -try: - __version__ = importlib.metadata.version(__name__) -except importlib.metadata.PackageNotFoundError: - __version__ = "0.0.0" # Fallback for development mode +from . import __version__ # type: ignore[attr-defined] from ._logging import get_logger +from ._tools import AITool, ai_function +from ._types import ( + AIContent, + AIContents, + ChatFinishReason, + ChatMessage, + ChatOptions, + ChatResponse, + ChatResponseUpdate, + ChatRole, + ChatToolMode, + DataContent, + ErrorContent, + FunctionCallContent, + FunctionResultContent, + ModelClient, + StructuredResponse, + TextContent, + TextReasoningContent, + UriContent, + UsageContent, + UsageDetails, +) +from .guard_rails import InputGuardrail, OutputGuardrail -__all__ = ["__version__", "get_logger"] +__all__ = [ + "AIContent", + "AIContents", + "AITool", + "ChatFinishReason", + "ChatMessage", + "ChatOptions", + "ChatResponse", + "ChatResponseUpdate", + "ChatRole", + "ChatToolMode", + "DataContent", + "ErrorContent", + "FunctionCallContent", + "FunctionResultContent", + "InputGuardrail", + "ModelClient", + "OutputGuardrail", + "StructuredResponse", + "TextContent", + "TextReasoningContent", + "UriContent", + "UsageContent", + "UsageDetails", + "__version__", + "ai_function", + "get_logger", +] diff --git a/python/agent_framework/_cancellation_token.py b/python/agent_framework/_cancellation_token.py new file mode 100644 index 0000000000..770200fbbd --- /dev/null +++ b/python/agent_framework/_cancellation_token.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft. All rights reserved. + +import threading +from asyncio import Future +from collections.abc import Callable +from typing import Any + + +# from https://github.com/microsoft/autogen/blob/main/python/packages/autogen-core/src/autogen_core/_cancellation_token.py +class CancellationToken: + """A token used to cancel pending async calls.""" + + def __init__(self) -> None: + self._cancelled: bool = False + self._lock: threading.Lock = threading.Lock() + self._callbacks: list[Callable[[], None]] = [] + + def cancel(self) -> None: + """Cancel pending async calls linked to this cancellation token.""" + with self._lock: + if not self._cancelled: + self._cancelled = True + for callback in self._callbacks: + callback() + + def is_cancelled(self) -> bool: + """Check if the CancellationToken has been used.""" + with self._lock: + return self._cancelled + + def add_callback(self, callback: Callable[[], None]) -> None: + """Attach a callback that will be called when cancel is invoked.""" + with self._lock: + if self._cancelled: + callback() + else: + self._callbacks.append(callback) + + def link_future(self, future: Future[Any]) -> Future[Any]: + """Link a pending async call to a token to allow its cancellation.""" + with self._lock: + if self._cancelled: + future.cancel() + else: + + def _cancel() -> None: + future.cancel() + + self._callbacks.append(_cancel) + return future diff --git a/python/agent_framework/_pydantic.py b/python/agent_framework/_pydantic.py new file mode 100644 index 0000000000..29fad5255e --- /dev/null +++ b/python/agent_framework/_pydantic.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from pydantic import BaseModel, ConfigDict + + +class AFBaseModel(BaseModel): + """Base class for all pydantic models in the Agent Framework.""" + + model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True, validate_assignment=True) diff --git a/python/agent_framework/_tools.py b/python/agent_framework/_tools.py index e69de29bb2..82f445b60a 100644 --- a/python/agent_framework/_tools.py +++ b/python/agent_framework/_tools.py @@ -0,0 +1,122 @@ +# Copyright (c) Microsoft. All rights reserved. + +import functools +import inspect +from collections.abc import Awaitable, Callable +from typing import Any, Generic, Protocol, TypeVar, runtime_checkable + +from pydantic import BaseModel, create_model + + +@runtime_checkable +class AITool(Protocol): + """Represents a tool that can be specified to an AI service.""" + + name: str + """The name of the tool.""" + description: str | None = None + """A description of the tool, suitable for use in describing the purpose to a model.""" + additional_properties: dict[str, Any] | None = None + """Additional properties associated with the tool.""" + + def __str__(self) -> str: + """Return a string representation of the tool.""" + ... + + +ArgsT = TypeVar("ArgsT", bound=BaseModel) +ReturnT = TypeVar("ReturnT") + + +class AIFunction(Generic[ArgsT, ReturnT]): + """A tool that represents a function that can be called by an AI service.""" + + def __init__( + self, + func: Callable[..., Awaitable[ReturnT] | ReturnT], + name: str, + description: str, + input_model: type[ArgsT], + **kwargs: Any, + ): + """Initialize a FunctionTool. + + Args: + func: The function to wrap. + name: The name of the tool. + description: A description of the tool. + input_model: A Pydantic model that defines the input parameters for the function. + **kwargs: Additional properties to set on the tool. + stored in additional_properties. + """ + self.name = name + self.description = description + self.input_model = input_model + self.additional_properties: dict[str, Any] | None = kwargs + self._func = func + + def model_json_schema(self) -> dict[str, Any]: + """Return the JSON schema of the input model.""" + return self.input_model.model_json_schema() + + def __call__(self, *args: Any, **kwargs: Any) -> ReturnT | Awaitable[ReturnT]: + """Call the wrapped function with the provided arguments.""" + return self._func(*args, **kwargs) + + async def invoke( + self, + *, + arguments: ArgsT | None = None, + **kwargs: Any, + ) -> ReturnT: + """Run the AI function with the provided arguments as a Pydantic model. + + Args: + arguments: A Pydantic model instance containing the arguments for the function. + kwargs: keyword arguments to pass to the function, will not be used if `args` is provided. + """ + if arguments is not None: + if not isinstance(arguments, self.input_model): + raise TypeError(f"Expected {self.input_model.__name__}, got {type(arguments).__name__}") + kwargs = arguments.model_dump(exclude_none=True) + res = self.__call__(**kwargs) + if inspect.isawaitable(res): + return await res + return res + + +def ai_function( + func: Callable[..., ReturnT | Awaitable[ReturnT]] | None = None, + *, + name: str | None = None, + description: str | None = None, + additional_properties: dict[str, Any] | None = None, +) -> AIFunction[Any, ReturnT] | Callable[[Callable[..., ReturnT | Awaitable[ReturnT]]], AIFunction[Any, ReturnT]]: + """Create a AIFunction from a function and return the callable tool object.""" + + def wrapper(f: Callable[..., ReturnT | Awaitable[ReturnT]]) -> AIFunction[Any, ReturnT]: + tool_name: str = name or getattr(f, "__name__", "unknown_function") # type: ignore[assignment] + tool_desc: str = description or (f.__doc__ or "") + sig = inspect.signature(f) + fields = { + pname: ( + param.annotation if param.annotation is not inspect.Parameter.empty else str, + param.default if param.default is not inspect.Parameter.empty else ..., + ) + for pname, param in sig.parameters.items() + if pname not in {"self", "cls"} + } + input_model = create_model(f"{tool_name}_input", **fields) # type: ignore[call-overload] + + return functools.update_wrapper( # type: ignore[return-value] + AIFunction[Any, ReturnT]( + func=f, + name=tool_name, + description=tool_desc, + input_model=input_model, + **(additional_properties if additional_properties is not None else {}), + ), + f, + ) + + return wrapper(func) if func else wrapper diff --git a/python/agent_framework/_types.py b/python/agent_framework/_types.py index e69de29bb2..09c12ebed5 100644 --- a/python/agent_framework/_types.py +++ b/python/agent_framework/_types.py @@ -0,0 +1,1445 @@ +# Copyright (c) Microsoft. All rights reserved. + +import base64 +import re +import sys +from collections.abc import AsyncIterable, MutableSequence, Sequence +from typing import Annotated, Any, ClassVar, Generic, Literal, Protocol, TypeVar, overload, runtime_checkable + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +from ._pydantic import AFBaseModel +from ._tools import AITool +from .guard_rails import InputGuardrail, OutputGuardrail + +if sys.version_info >= (3, 12): + pass # pragma: no cover +if sys.version_info >= (3, 11): + from typing import Self # pragma: no cover +else: + from typing_extensions import Self # pragma: no cover + +# region: Constants and types +_T = TypeVar("_T") +TValue = TypeVar("TValue") +TInput = TypeVar("TInput") +TResponse = TypeVar("TResponse") +TChatResponse = TypeVar("TChatResponse", bound="ChatResponse") +TChatToolMode = TypeVar("TChatToolMode", bound="ChatToolMode") + +CreatedAtT = str # Use a datetimeoffset type? Or a more specific type like datetime.datetime? + +URI_PATTERN = re.compile(r"^data:(?P[^;]+);base64,(?P[A-Za-z0-9+/=]+)$") + +KNOWN_MEDIA_TYPES = [ + "application/json", + "application/octet-stream", + "application/pdf", + "application/xml", + "audio/mpeg", + "audio/ogg", + "audio/wav", + "image/apng", + "image/avif", + "image/bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/webp", + "text/css", + "text/csv", + "text/html", + "text/javascript", + "text/plain", + "text/plain;charset=UTF-8", + "text/xml", +] + + +class UsageDetails(AFBaseModel): + """Provides usage details about a request/response. + + Attributes: + input_token_count: The number of tokens in the input. + output_token_count: The number of tokens in the output. + total_token_count: The total number of tokens used to produce the response. + additional_counts: A dictionary of additional token counts, can be set by passing kwargs. + """ + + model_config = ConfigDict( + populate_by_name=True, arbitrary_types_allowed=True, validate_assignment=True, extra="allow" + ) + __pydantic_extra__: dict[str, int] + """Overriding the default extras type, to make sure all extras are integers.""" + + input_token_count: int | None = None + """The number of tokens in the input.""" + output_token_count: int | None = None + """The number of tokens in the output.""" + total_token_count: int | None = None + """The total number of tokens used to produce the response.""" + + @property + def additional_counts(self) -> dict[str, int]: + """Represents well-known additional counts for usage. This is not an exhaustive list. + + Remarks: + To make it possible to avoid collisions between similarly-named, but unrelated, additional counts + between different AI services, any keys not explicitly defined here should be prefixed with the + name of the AI service, e.g., "openai." or "azure.". The separator "." was chosen because it cannot + be a legal character in a JSON key. + + Over time additional counts may be added to the base class. + """ + return self.model_extra or {} + + def __add__(self, other: "UsageDetails") -> "UsageDetails": + """Combines two `UsageDetails` instances.""" + if not isinstance(other, UsageDetails): + raise ValueError("Can only add two usage details objects together.") + + additional_counts = self.additional_counts or {} + if other.additional_counts: + for key, value in other.additional_counts.items(): + additional_counts[key] = additional_counts.get(key, 0) + (value or 0) + + return UsageDetails( + input_token_count=(self.input_token_count or 0) + (other.input_token_count or 0), + output_token_count=(self.output_token_count or 0) + (other.output_token_count or 0), + total_token_count=(self.total_token_count or 0) + (other.total_token_count or 0), + **additional_counts, + ) + + def __init__( + self, + input_token_count: int | None = None, + output_token_count: int | None = None, + total_token_count: int | None = None, + **kwargs: int, + ) -> None: + """Initializes the UsageDetails instance. + + Args: + input_token_count: The number of tokens in the input. + output_token_count: The number of tokens in the output. + total_token_count: The total number of tokens used to produce the response. + **kwargs: Additional token counts, can be set by passing keyword arguments. + They can be retrieved through the `additional_counts` property. + """ + super().__init__( + input_token_count=input_token_count, + output_token_count=output_token_count, + total_token_count=total_token_count, + **kwargs, + ) + + +def _process_update(response: "ChatResponse", update: "ChatResponseUpdate") -> None: + """Processes a single update and modifies the response in place.""" + is_new_message = False + if not response.messages or (update.message_id and response.messages[-1].message_id != update.message_id): + is_new_message = True + + if is_new_message: + message = ChatMessage(role=ChatRole.ASSISTANT, contents=[]) + response.messages.append(message) + else: + message = response.messages[-1] + # Incorporate the update's properties into the message. + if update.author_name is not None: + message.author_name = update.author_name + if update.role is not None: + message.role = update.role + if update.message_id: + message.message_id = update.message_id + for content in update.contents: + if isinstance(content, UsageContent): + if response.usage_details is None: + response.usage_details = UsageDetails() + response.usage_details += content.details + else: + message.contents.append(content) + # Incorporate the update's properties into the response. + if update.response_id: + response.response_id = update.response_id + if update.conversation_id is not None: + response.conversation_id = update.conversation_id + if update.created_at is not None: + response.created_at = update.created_at + if update.finish_reason is not None: + response.finish_reason = update.finish_reason + if update.ai_model_id is not None: + response.ai_model_id = update.ai_model_id + if update.additional_properties is not None: + if response.additional_properties is None: + response.additional_properties = {} + response.additional_properties.update(update.additional_properties) + + +def _coalesce_text_content( + contents: list["AIContents"], type_: type["TextContent"] | type["TextReasoningContent"] +) -> None: + """Take any subsequence Text or TextReasoningContent items and coalesce them into a single item.""" + if not contents: + return + coalesced_contents: list["AIContents"] = [] + current_texts = [] + first_new_content = None + for i, content in enumerate(contents): + if isinstance(content, type_): + current_texts.append(content.text) # type: ignore[union-attr] + if first_new_content is None: + first_new_content = i + else: + if first_new_content is not None: + new_content = type_(text="\n".join(current_texts)) + new_content.raw_representation = contents[first_new_content].raw_representation + new_content.additional_properties = contents[first_new_content].additional_properties + # Store the replacement node. We inherit the properties of the first text node. We don't + # currently propagate additional properties from the subsequent nodes. If we ever need to, + # we can add that here. + coalesced_contents.append(new_content) + current_texts = [] + first_new_content = None + coalesced_contents.append(content) + if current_texts: + coalesced_contents.append(type_(text="\n".join(current_texts))) + contents.clear() + contents.extend(coalesced_contents) + + +def _finalize_response(response: "ChatResponse") -> None: + """Finalizes the chat response by performing any necessary post-processing.""" + for msg in response.messages: + _coalesce_text_content(msg.contents, TextContent) + _coalesce_text_content(msg.contents, TextReasoningContent) + + +# region: AIContent + + +class AIContent(AFBaseModel): + """Represents content used by AI services. + + Attributes: + type: The type of content, which is always "ai" for this class. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content from an underlying implementation. + + """ + + type: Literal["ai"] = "ai" + additional_properties: dict[str, Any] | None = None + """Additional properties for the content.""" + raw_representation: Any | None = Field(default=None, repr=False) + """The raw representation of the content from an underlying implementation.""" + + +class TextContent(AIContent): + """Represents text content in a chat. + + Attributes: + text: The text content represented by this instance. + type: The type of content, which is always "text" for this class. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + """ + + text: str + type: Literal["text"] = "text" # type: ignore[assignment] + + def __init__( + self, + text: str, + *, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ): + """Initializes a TextContent instance. + + Args: + text: The text content represented by this instance. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + text=text, + raw_representation=raw_representation, + additional_properties=additional_properties, + **kwargs, + ) + + +class TextReasoningContent(AIContent): + """Represents text reasoning content in a chat. + + Remarks: + This class and `TextContent` are superficially similar, but distinct. + + Attributes: + text: The text content represented by this instance. + type: The type of content, which is always "text_reasoning" for this class. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + + """ # TODO(): Should we merge these two classes, and use a property to distinguish them? + + text: str + type: Literal["text_reasoning"] = "text_reasoning" # type: ignore[assignment] + + def __init__( + self, + text: str, + *, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ): + """Initializes a TextReasoningContent instance. + + Args: + text: The text content represented by this instance. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + text=text, + raw_representation=raw_representation, + additional_properties=additional_properties, + **kwargs, + ) + + +class DataContent(AIContent): + """Represents binary data content with an associated media type (also known as a MIME type). + + Attributes: + uri: The URI of the data represented by this instance, typically in the form of a data URI. + Should be in the form: "data:{media_type};base64,{base64_data}". + type: The type of content, which is always "data" for this class. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + """ + + type: Literal["data"] = "data" # type: ignore[assignment] + uri: str + + @overload + def __init__( + self, + *, + uri: str, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a DataContent instance with a URI. + + Remarks: + This is for binary data that is represented as a data URI, not for online resources. + Use `UriContent` for online resources. + + Args: + uri: The URI of the data represented by this instance. + Should be in the form: "data:{media_type};base64,{base64_data}". + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + + @overload + def __init__( + self, + *, + data: bytes, + media_type: str, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a DataContent instance with binary data. + + Remarks: + This is for binary data that is represented as a data URI, not for online resources. + Use `UriContent` for online resources. + + Args: + data: The binary data represented by this instance. + The data is transformed into a base64-encoded data URI. + media_type: The media type of the data. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + + def __init__( + self, + *, + uri: str | None = None, + data: bytes | None = None, + media_type: str | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a DataContent instance. + + Remarks: + This is for binary data that is represented as a data URI, not for online resources. + Use `UriContent` for online resources. + + Args: + uri: The URI of the data represented by this instance. + Should be in the form: "data:{media_type};base64,{base64_data}". + data: The binary data represented by this instance. + The data is transformed into a base64-encoded data URI. + media_type: The media type of the data. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + if uri is None: + if data is None or media_type is None: + raise ValueError("Either 'data' and 'media_type' or 'uri' must be provided.") + uri = f"data:{media_type};base64,{base64.b64encode(data).decode('utf-8')}" + super().__init__( + uri=uri, + raw_representation=raw_representation, + additional_properties=additional_properties, + **kwargs, + ) + + @field_validator("uri", mode="after") + @classmethod + def _validate_uri(cls, uri: str) -> str: + """Validates the URI format and extracts the media type. + + Minimal data URI parser based on RFC 2397: https://datatracker.ietf.org/doc/html/rfc2397. + """ + match = URI_PATTERN.match(uri) + if not match: + raise ValueError(f"Invalid data URI format: {uri}") + media_type = match.group("media_type") + if media_type not in KNOWN_MEDIA_TYPES: + raise ValueError(f"Unknown media type: {media_type}") + return uri + + +class UriContent(AIContent): + """Represents a URI content. + + Remarks: + This is used for content that is identified by a URI, such as an image or a file. + For (binary) data URIs, use `DataContent` instead. + + Attributes: + uri: The URI of the content, e.g., 'https://example.com/image.png'. + media_type: The media type of the content, e.g., 'image/png', 'application/json', etc. + type: The type of content, which is always "uri" for this class. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + """ + + type: Literal["uri"] = "uri" # type: ignore[assignment] + uri: str + """The URI of the content, e.g., 'https://example.com/image.png'.""" + media_type: str + """The media type of the content, e.g., 'image/png', 'application/json', etc.""" + + def __init__( + self, + uri: str, + media_type: str, + *, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a UriContent instance. + + Remarks: + This is used for content that is identified by a URI, such as an image or a file. + For (binary) data URIs, use `DataContent` instead. + + Args: + uri: The URI of the content. + media_type: The media type of the content. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + uri=uri, + media_type=media_type, + additional_properties=additional_properties, + raw_representation=raw_representation, + **kwargs, + ) + + +class ErrorContent(AIContent): + """Represents an error. + + Remarks: + Typically used for non-fatal errors, where something went wrong as part of the operation, + but the operation was still able to continue. + + Attributes: + type: The type of content, which is always "error" for this class. + error_code: The error code associated with the error. + details: Additional details about the error. + message: The error message. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + + """ + + type: Literal["error"] = "error" # type: ignore[assignment] + error_code: str | None = None + """The error code associated with the error.""" + details: str | None = None + """Additional details about the error.""" + message: str | None + """The error message.""" + + def __init__( + self, + *, + message: str | None = None, + error_code: str | None = None, + details: str | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes an ErrorContent instance. + + Args: + message: The error message. + error_code: The error code associated with the error. + details: Additional details about the error. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + message=message, + error_code=error_code, + details=details, + additional_properties=additional_properties, + raw_representation=raw_representation, + **kwargs, + ) + + def __str__(self) -> str: + """Returns a string representation of the error.""" + return f"Error {self.error_code}: {self.message}" if self.error_code else self.message or "Unknown error" + + +class FunctionCallContent(AIContent): + """Represents a function call request. + + Attributes: + type: The type of content, which is always "function_call" for this class. + call_id: The function call identifier. + name: The name of the function requested. + arguments: The arguments requested to be provided to the function. + exception: Any exception that occurred while mapping the original function call data to this representation. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + """ + + type: Literal["function_call"] = "function_call" # type: ignore[assignment] + call_id: str + """The function call identifier.""" + name: str + """The name of the function requested.""" + arguments: dict[str, Any | None] | None = None + """The arguments requested to be provided to the function.""" + exception: Exception | None = None + """Any exception that occurred while mapping the original function call data to this representation.""" + + def __init__( + self, + *, + call_id: str, + name: str, + arguments: dict[str, Any | None] | None = None, + exception: Exception | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a FunctionCallContent instance. + + Args: + call_id: The function call identifier. + name: The name of the function requested. + arguments: The arguments requested to be provided to the function. + exception: Any exception that occurred while mapping the original function call data to this representation. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + call_id=call_id, + name=name, + arguments=arguments, + exception=exception, + raw_representation=raw_representation, + additional_properties=additional_properties, + **kwargs, + ) + + +class FunctionResultContent(AIContent): + """Represents the result of a function call. + + Attributes: + type: The type of content, which is always "function_result" for this class. + call_id: The identifier of the function call for which this is the result. + result: The result of the function call, or a generic error message if the function call failed. + exception: An exception that occurred if the function call failed. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + """ + + type: Literal["function_result"] = "function_result" # type: ignore[assignment] + call_id: str + """The identifier of the function call for which this is the result.""" + result: Any | None = None + """The result of the function call, or a generic error message if the function call failed.""" + exception: Exception | None = None + """An exception that occurred if the function call failed.""" + + def __init__( + self, + *, + call_id: str, + result: Any | None = None, + exception: Exception | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a FunctionResultContent instance. + + Args: + call_id: The identifier of the function call for which this is the result. + result: The result of the function call, or a generic error message if the function call failed. + exception: An exception that occurred if the function call failed. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + call_id=call_id, + result=result, + exception=exception, + raw_representation=raw_representation, + additional_properties=additional_properties, + **kwargs, + ) + + +class UsageContent(AIContent): + """Represents usage information associated with a chat request and response. + + Attributes: + type: The type of content, which is always "usage" for this class. + details: The usage information, including input and output token counts, and any additional counts. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + + """ + + type: Literal["usage"] = "usage" # type: ignore[assignment] + details: UsageDetails + """The usage information.""" + + def __init__( + self, + details: UsageDetails, + *, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a UsageContent instance. + + Args: + details: The usage information. + additional_properties: Optional additional properties associated with the content. + raw_representation: Optional raw representation of the content. + **kwargs: Any additional keyword arguments. + """ + super().__init__( + type=self.type, + details=details, + raw_representation=raw_representation, + additional_properties=additional_properties, + **kwargs, + ) + + +AIContents = Annotated[ + TextContent + | DataContent + | TextReasoningContent + | UriContent + | FunctionCallContent + | FunctionResultContent + | ErrorContent + | UsageContent, + Field(discriminator="type"), +] + +# region: Chat Response constants + + +class ChatRole(AFBaseModel): + """Describes the intended purpose of a message within a chat interaction. + + Attributes: + value: The string representation of the role. + + Properties: + SYSTEM: The role that instructs or sets the behaviour of the AI system. + USER: The role that provides user input for chat interactions. + ASSISTANT: The role that provides responses to system-instructed, user-prompted input. + TOOL: The role that provides additional information and references in response to tool use requests. + """ + + value: str + + SYSTEM: ClassVar[Self] # type: ignore[assignment] + """The role that instructs or sets the behaviour of the AI system.""" + USER: ClassVar[Self] # type: ignore[assignment] + """The role that provides user input for chat interactions.""" + ASSISTANT: ClassVar[Self] # type: ignore[assignment] + """The role that provides responses to system-instructed, user-prompted input.""" + TOOL: ClassVar[Self] # type: ignore[assignment] + """The role that provides additional information and references in response to tool use requests.""" + + def __str__(self) -> str: + """Returns the string representation of the role.""" + return self.value + + def __repr__(self) -> str: + """Returns the string representation of the role.""" + return f"ChatRole(value={self.value!r})" + + +# Note: ClassVar is used to indicate that these are class-level constants, not instance attributes. +# The type: ignore[assignment] is used to suppress the type checker warning about assigning to a ClassVar, +# it gets assigned immediately after the class definition. +ChatRole.SYSTEM = ChatRole(value="system") # type: ignore[assignment] +ChatRole.USER = ChatRole(value="user") # type: ignore[assignment] +ChatRole.ASSISTANT = ChatRole(value="assistant") # type: ignore[assignment] +ChatRole.TOOL = ChatRole(value="tool") # type: ignore[assignment] + + +class ChatFinishReason(AFBaseModel): + """Represents the reason a chat response completed. + + Attributes: + value: The string representation of the finish reason. + + Properties: + CONTENT_FILTER: The model filtered content, whether for safety, prohibited content, sensitive content, + or other such issues. + LENGTH: The model reached the maximum length allowed for the request and/or response (typically in + terms of tokens). + STOP: The model encountered a natural stop point or provided stop sequence. + TOOL_CALLS: The model requested the use of a tool that was defined in the request. + + """ + + value: str + + CONTENT_FILTER: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model filtering content, whether for safety, prohibited content, + sensitive content, or other such issues.""" + LENGTH: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model reaching the maximum length allowed for the request and/or + response (typically in terms of tokens).""" + STOP: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model encountering a natural stop point or provided stop sequence.""" + TOOL_CALLS: ClassVar[Self] # type: ignore[assignment] + """A ChatFinishReason representing the model requesting the use of a tool that was defined in the request.""" + + +ChatFinishReason.CONTENT_FILTER = ChatFinishReason(value="content_filter") # type: ignore[assignment] +ChatFinishReason.LENGTH = ChatFinishReason(value="length") # type: ignore[assignment] +ChatFinishReason.STOP = ChatFinishReason(value="stop") # type: ignore[assignment] +ChatFinishReason.TOOL_CALLS = ChatFinishReason(value="tool_calls") # type: ignore[assignment] + +# region: ChatMessage + + +class ChatMessage(AFBaseModel): + """Represents a chat message used by a `ModelClient`. + + Attributes: + role: The role of the author of the message. + contents: The chat message content items. + author_name: The name of the author of the message. + message_id: The ID of the chat message. + additional_properties: Any additional properties associated with the chat message. + raw_representation: The raw representation of the chat message from an underlying implementation. + + """ + + role: ChatRole + """The role of the author of the message.""" + contents: list[AIContents] + """The chat message content items.""" + author_name: str | None + """The name of the author of the message.""" + message_id: str | None + """The ID of the chat message.""" + additional_properties: dict[str, Any] | None = None + """Any additional properties associated with the chat message.""" + raw_representation: Any | None = None + """The raw representation of the chat message from an underlying implementation.""" + + @property + def text(self) -> str: + """Returns the text content of the message. + + Remarks: + This property concatenates the text of all TextContent objects in Contents. + """ + return "\n".join(content.text for content in self.contents if isinstance(content, TextContent)) + + @overload + def __init__( + self, + role: ChatRole | Literal["system", "user", "assistant", "tool"], + *, + text: str, + author_name: str | None = None, + message_id: str | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + ) -> None: + """Initializes a ChatMessage with a role and text content. + + Args: + role: The role of the author of the message. + text: The text content of the message. + author_name: Optional name of the author of the message. + message_id: Optional ID of the chat message. + additional_properties: Optional additional properties associated with the chat message. + raw_representation: Optional raw representation of the chat message. + """ + + @overload + def __init__( + self, + role: ChatRole | Literal["system", "user", "assistant", "tool"], + *, + contents: list[AIContents], + author_name: str | None = None, + message_id: str | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + ) -> None: + """Initializes a ChatMessage with a role and optional contents. + + Args: + role: The role of the author of the message. + contents: Optional list of AIContent items to include in the message. + author_name: Optional name of the author of the message. + message_id: Optional ID of the chat message. + additional_properties: Optional additional properties associated with the chat message. + raw_representation: Optional raw representation of the chat message. + """ + + def __init__( + self, + role: ChatRole | Literal["system", "user", "assistant", "tool"], + *, + text: str | None = None, + contents: list[AIContents] | None = None, + author_name: str | None = None, + message_id: str | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + ) -> None: + if contents is None: + contents = [] + if text is not None: + contents.append(TextContent(text=text)) + if isinstance(role, str): + role = ChatRole(value=role) + super().__init__( + role=role, + contents=contents, + author_name=author_name, + message_id=message_id, + additional_properties=additional_properties, + raw_representation=raw_representation, + ) + + +# region: ChatResponse + + +class ChatResponse(AFBaseModel): + """Represents the response to a chat request. + + Attributes: + messages: The list of chat messages in the response. + response_id: The ID of the chat response. + conversation_id: An identifier for the state of the conversation. + ai_model_id: The model ID used in the creation of the chat response. + created_at: A timestamp for the chat response. + finish_reason: The reason for the chat response. + usage_details: The usage details for the chat response. + additional_properties: Any additional properties associated with the chat response. + raw_representation: The raw representation of the chat response from an underlying implementation. + + + """ + + messages: list[ChatMessage] + """The chat response messages.""" + + response_id: str | None = None + """The ID of the chat response.""" + conversation_id: str | None = None + """An identifier for the state of the conversation.""" + ai_model_id: str | None = Field(default=None, alias="model_id") + """The model ID used in the creation of the chat response.""" + created_at: CreatedAtT | None = None # use a datetimeoffset type? + """A timestamp for the chat response.""" + finish_reason: ChatFinishReason | None = None + """The reason for the chat response.""" + usage_details: UsageDetails | None = None + """The usage details for the chat response.""" + additional_properties: dict[str, Any] | None = None + """Any additional properties associated with the chat response.""" + raw_representation: Any | None = None + """The raw representation of the chat response from an underlying implementation.""" + + @overload + def __init__( + self, + *, + messages: ChatMessage | MutableSequence[ChatMessage], + response_id: str | None = None, + conversation_id: str | None = None, + model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + usage_details: UsageDetails | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a ChatResponse with the provided parameters. + + Args: + messages: A single ChatMessage or a sequence of ChatMessage objects to include in the response. + response_id: Optional ID of the chat response. + conversation_id: Optional identifier for the state of the conversation. + model_id: Optional model ID used in the creation of the chat response. + created_at: Optional timestamp for the chat response. + finish_reason: Optional reason for the chat response. + usage_details: Optional usage details for the chat response. + messages: List of ChatMessage objects to include in the response. + additional_properties: Optional additional properties associated with the chat response. + raw_representation: Optional raw representation of the chat response from an underlying implementation. + **kwargs: Any additional keyword arguments. + """ + + @overload + def __init__( + self, + *, + text: TextContent | str, + response_id: str | None = None, + conversation_id: str | None = None, + model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + usage_details: UsageDetails | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a ChatResponse with the provided parameters. + + Args: + text: The text content to include in the response. If provided, it will be added as a ChatMessage. + response_id: Optional ID of the chat response. + conversation_id: Optional identifier for the state of the conversation. + model_id: Optional model ID used in the creation of the chat response. + created_at: Optional timestamp for the chat response. + finish_reason: Optional reason for the chat response. + usage_details: Optional usage details for the chat response. + additional_properties: Optional additional properties associated with the chat response. + raw_representation: Optional raw representation of the chat response from an underlying implementation. + **kwargs: Any additional keyword arguments. + + """ + + def __init__( + self, + *, + messages: ChatMessage | MutableSequence[ChatMessage] | None = None, + text: TextContent | str | None = None, + response_id: str | None = None, + conversation_id: str | None = None, + model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + usage_details: UsageDetails | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a ChatResponse with the provided parameters.""" + if messages is None: + messages = [] + elif not isinstance(messages, MutableSequence): + messages = [messages] + if text is not None: + if isinstance(text, str): + text = TextContent(text=text) + messages.append(ChatMessage(role=ChatRole.ASSISTANT, contents=[text])) + + super().__init__( + messages=messages, + response_id=response_id, + conversation_id=conversation_id, + ai_model_id=model_id, + created_at=created_at, + finish_reason=finish_reason, + usage_details=usage_details, + additional_properties=additional_properties, + raw_representation=raw_representation, + **kwargs, + ) + + @classmethod + def from_chat_response_updates(cls: type[TChatResponse], updates: Sequence["ChatResponseUpdate"]) -> TChatResponse: + """Joins multiple updates into a single ChatResponse.""" + msg = cls(messages=[]) + for update in updates: + _process_update(msg, update) + _finalize_response(msg) + return msg + + @classmethod + async def from_chat_response_generator( + cls: type[TChatResponse], updates: AsyncIterable["ChatResponseUpdate"] + ) -> TChatResponse: + """Joins multiple updates into a single ChatResponse.""" + msg = cls(messages=[]) + async for update in updates: + _process_update(msg, update) + _finalize_response(msg) + return msg + + @property + def text(self) -> str: + """Returns the concatenated text of all messages in the response.""" + return "\n".join(message.text for message in self.messages if isinstance(message, ChatMessage)) + + +class StructuredResponse(ChatResponse, Generic[TValue]): + """Represents a structured response to a chat request. + + Type Parameters: + TValue: The type of the value contained in the structured response. + """ + + value: TValue + """The result value of the chat response as an instance of `TValue`.""" + + @property + def text(self) -> str: + """Returns the concatenated text of all messages in the response.""" + return "\n".join(message.text for message in self.messages) + + @overload + def __init__( + self, + value: TValue, + *, + messages: ChatMessage | MutableSequence[ChatMessage], + response_id: str | None = None, + conversation_id: str | None = None, + model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + usage_details: UsageDetails | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a StructuredResponse with the provided parameters.""" + + @overload + def __init__( + self, + value: TValue, + *, + text: TextContent | str, + response_id: str | None = None, + conversation_id: str | None = None, + model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + usage_details: UsageDetails | None = None, + raw_representation: Any | None = None, + additional_properties: dict[str, Any] | None = None, + **kwargs: Any, + ) -> None: + """Initializes a StructuredResponse with the provided parameters.""" + + def __init__( + self, + value: TValue, + *, + messages: ChatMessage | MutableSequence[ChatMessage] | None = None, + text: TextContent | str | None = None, + response_id: str | None = None, + conversation_id: str | None = None, + model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + usage_details: UsageDetails | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + **kwargs: Any, + ) -> None: + """Initializes a StructuredResponse with the provided parameters.""" + if messages is None: + messages = [] + elif isinstance(messages, ChatMessage): + messages = [messages] + if text is not None: + if isinstance(text, str): + text = TextContent(text=text) + messages.append(ChatMessage(role=ChatRole.ASSISTANT, contents=[text])) + + super().__init__( + value=value, + messages=messages, + conversation_id=conversation_id, + created_at=created_at, + finish_reason=finish_reason, + model_id=model_id, + response_id=response_id, + usage_details=usage_details, + additional_properties=additional_properties, + raw_representation=raw_representation, + **kwargs, + ) + + +# region: ChatResponseUpdate + + +class ChatResponseUpdate(AFBaseModel): + """Represents a single streaming response chunk from a `ModelClient`. + + Attributes: + contents: The chat response update content items. + role: The role of the author of the response update. + author_name: The name of the author of the response update. + response_id: The ID of the response of which this update is a part. + message_id: The ID of the message of which this update is a part. + conversation_id: An identifier for the state of the conversation of which this update is a part. + ai_model_id: The model ID associated with this response update. + created_at: A timestamp for the chat response update. + finish_reason: The finish reason for the operation. + additional_properties: Any additional properties associated with the chat response update. + raw_representation: The raw representation of the chat response update from an underlying implementation. + + """ + + contents: list[AIContents] + """The chat response update content items.""" + + role: ChatRole | None = None + """The role of the author of the response update.""" + author_name: str | None = None + """The name of the author of the response update.""" + response_id: str | None = None + """The ID of the response of which this update is a part.""" + message_id: str | None = None + """The ID of the message of which this update is a part.""" + + conversation_id: str | None = None + """An identifier for the state of the conversation of which this update is a part.""" + ai_model_id: str | None = Field(default=None, alias="model_id") + """The model ID associated with this response update.""" + created_at: CreatedAtT | None = None # use a datetimeoffset type? + """A timestamp for the chat response update.""" + finish_reason: ChatFinishReason | None = None + """The finish reason for the operation.""" + + additional_properties: dict[str, Any] | None = None + """Any additional properties associated with the chat response update.""" + raw_representation: Any | None = None + """The raw representation of the chat response update from an underlying implementation.""" + + @overload + def __init__( + self, + *, + contents: list[AIContent], + role: ChatRole | None = None, + author_name: str | None = None, + response_id: str | None = None, + message_id: str | None = None, + conversation_id: str | None = None, + ai_model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + ) -> None: + """Initializes a ChatResponseUpdate with the provided parameters.""" + + @overload + def __init__( + self, + *, + text: TextContent | str, + role: ChatRole | None = None, + author_name: str | None = None, + response_id: str | None = None, + message_id: str | None = None, + conversation_id: str | None = None, + ai_model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + ) -> None: + """Initializes a ChatResponseUpdate with the provided parameters.""" + + def __init__( + self, + *, + contents: list[AIContent] | None = None, + text: TextContent | str | None = None, + role: ChatRole | None = None, + author_name: str | None = None, + response_id: str | None = None, + message_id: str | None = None, + conversation_id: str | None = None, + ai_model_id: str | None = None, + created_at: CreatedAtT | None = None, + finish_reason: ChatFinishReason | None = None, + additional_properties: dict[str, Any] | None = None, + raw_representation: Any | None = None, + ) -> None: + """Initializes a ChatResponseUpdate with the provided parameters.""" + if contents is None: + contents = [] + if text is not None: + if isinstance(text, str): + text = TextContent(text=text) + contents.append(text) + + super().__init__( + contents=contents, + additional_properties=additional_properties, + author_name=author_name, + conversation_id=conversation_id, + created_at=created_at, + finish_reason=finish_reason, + message_id=message_id, + ai_model_id=ai_model_id, + raw_representation=raw_representation, + response_id=response_id, + role=role, + ) + + @property + def text(self) -> str: + """Returns the concatenated text of all contents in the update.""" + return "\n".join(content.text for content in self.contents if isinstance(content, TextContent)) + + def with_(self, contents: list[AIContent] | None = None, message_id: str | None = None) -> Self: + """Returns a new instance with the specified contents and message_id.""" + if contents is None: + contents = [] + + return self.model_copy( + update={ + "contents": self.contents + contents, + "message_id": message_id or self.message_id, + } + ) + + +# region: ChatOptions + + +class ChatToolMode(AFBaseModel): + """Defines if and how tools are used in a chat request.""" + + mode: Literal["auto", "required", "none"] = "none" + required_function_name: str | None = None + + AUTO: ClassVar[Self] # type: ignore[assignment] + REQUIRED_ANY: ClassVar[Self] # type: ignore[assignment] + NONE: ClassVar[Self] # type: ignore[assignment] + + @classmethod + def REQUIRED(cls: type[TChatToolMode], function_name: str | None = None) -> TChatToolMode: + """Returns a ChatToolMode that requires the specified function to be called.""" + return cls(mode="required", required_function_name=function_name) + + +ChatToolMode.AUTO = ChatToolMode(mode="auto") # type: ignore[assignment] +ChatToolMode.REQUIRED_ANY = ChatToolMode(mode="required") # type: ignore[assignment] +ChatToolMode.NONE = ChatToolMode(mode="none") # type: ignore[assignment] + + +class ChatOptions(AFBaseModel): + """Common request settings for AI services.""" + + ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None + frequency_penalty: Annotated[float | None, Field(ge=-2.0, le=2.0)] = None + logit_bias: dict[str | int, float] | None = None + max_tokens: Annotated[int | None, Field(gt=0)] = None + messages: list[dict[str, Any]] | None = Field(default=None, description="List of messages for the chat completion.") + presence_penalty: Annotated[float | None, Field(ge=-2.0, le=2.0)] = None + response_format: type[BaseModel] | None = Field( + default=None, description="Structured output response format schema. Must be a valid Pydantic model." + ) + seed: int | None = None + stop: str | list[str] | None = None + temperature: Annotated[float | None, Field(ge=0.0, le=2.0)] = None + tool_choice: ChatToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None + tools: list[AITool] | None = None + top_p: Annotated[float | None, Field(ge=0.0, le=1.0)] = None + user: str | None = None + store: bool | None = None + metadata: dict[str, str] | None = None + additional_properties: dict[str, Any] = Field( + default_factory=dict, description="Provider-specific additional properties." + ) + + @field_validator("tool_choice", mode="before") + @classmethod + def _validate_tool_mode(cls, data: dict[str, Any]) -> dict[str, Any]: + """Validates the tool_choice field to ensure it is a valid ChatToolMode.""" + if isinstance(data, dict): + tool_choice = data.get("tool_choice") + if isinstance(tool_choice, str): + if tool_choice == "auto": + data["tool_choice"] = ChatToolMode.AUTO + elif tool_choice == "required": + data["tool_choice"] = ChatToolMode.REQUIRED_ANY + elif tool_choice == "none": + data["tool_choice"] = ChatToolMode.NONE + else: + raise ValueError(f"Invalid tool choice: {tool_choice}") + elif isinstance(tool_choice, dict): + data["tool_choice"] = ChatToolMode.model_validate(tool_choice) + return data + + def to_provider_settings(self, by_alias: bool = True, exclude: set[str] | None = None) -> dict[str, Any]: + """Convert the ChatOptions to a dictionary suitable for provider requests. + + Args: + by_alias: Use alias names for fields if True. + exclude: Additional keys to exclude from the output. + + Returns: + Dictionary of settings for provider. + """ + default_exclude = {"additional_properties"} + merged_exclude = default_exclude if exclude is None else default_exclude | set(exclude) + + settings = self.model_dump(exclude_none=True, by_alias=by_alias, exclude=merged_exclude) + settings = {k: v for k, v in settings.items() if v and not isinstance(v, dict)} + settings.update(self.additional_properties) + for key in merged_exclude: + settings.pop(key, None) + return settings + + +# region: ModelClient Protocol + + +@runtime_checkable +class ModelClient(Protocol, Generic[TInput, TResponse]): + """A protocol for a model client that can generate responses.""" + + async def get_response( + self, + messages: TInput | Sequence[TInput], + **kwargs: Any, + ) -> TResponse: + """Sends input and returns the response. + + Args: + messages: The sequence of input messages to send. + **kwargs: Additional options for the request, such as ai_model_id, temperature, etc. + See `ChatOptions` for more details. + + Returns: + The response messages generated by the client. + + Raises: + ValueError: If the input message sequence is `None`. + """ + ... + + async def get_streaming_response( + self, + messages: TInput | Sequence[TInput], + **kwargs: Any, # kwargs? + ) -> AsyncIterable[TResponse]: + """Sends input messages and streams the response. + + Args: + messages: The sequence of input messages to send. + **kwargs: Additional options for the request, such as ai_model_id, temperature, etc. + See `ChatOptions` for more details. + + Returns: + An async iterable of chat response updates containing the content of the response messages + generated by the client. + + Raises: + ValueError: If the input message sequence is `None`. + """ + ... + + def add_input_guardrails(self, guardrails: list[InputGuardrail[TInput]]) -> None: + """Add input guardrails to the model client. + + Args: + guardrails: The list of input guardrails to add. + """ + ... + + def add_output_guardrails(self, guardrails: list[OutputGuardrail[TResponse | Sequence[TResponse]]]) -> None: + """Add output guardrails to the model client. + + Args: + guardrails: The list of output guardrails to add. + """ + ... diff --git a/python/agent_framework/guard_rails.py b/python/agent_framework/guard_rails.py new file mode 100644 index 0000000000..6083cb2d2b --- /dev/null +++ b/python/agent_framework/guard_rails.py @@ -0,0 +1,25 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from typing import Generic, Protocol, TypeVar, runtime_checkable + +TInput = TypeVar("TInput") +TResponse = TypeVar("TResponse") + + +@runtime_checkable +class InputGuardrail(Protocol, Generic[TInput]): + """A protocol for input guardrails that can validate and transform input messages.""" + + def __call__(self, message: TInput) -> TInput: + """Validate and possibly transform the input message.""" + ... + + +@runtime_checkable +class OutputGuardrail(Protocol, Generic[TResponse]): + """A protocol for output guardrails that can validate and transform output messages.""" + + def __call__(self, message: TResponse) -> TResponse: + """Validate and possibly transform the output message.""" + ... diff --git a/python/extensions/azure/pyproject.toml b/python/extensions/azure/pyproject.toml index f5d1d68941..24557fb6f8 100644 --- a/python/extensions/azure/pyproject.toml +++ b/python/extensions/azure/pyproject.toml @@ -12,6 +12,9 @@ dependencies = [ "openai", ] +[tool.ruff] +extend = "../../pyproject.toml" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/python/extensions/openai/pyproject.toml b/python/extensions/openai/pyproject.toml index 305727200e..cdbd66213d 100644 --- a/python/extensions/openai/pyproject.toml +++ b/python/extensions/openai/pyproject.toml @@ -11,6 +11,9 @@ dependencies = [ "agent-framework", ] +[tool.ruff] +extend = "../../pyproject.toml" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/python/pyproject.toml b/python/pyproject.toml index 2cdbf87c55..8e4fe2cedc 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -26,24 +26,35 @@ dependencies = [ "azure", "openai", "pydantic>=2.11.7", + "typing-extensions>=4.14.0", ] -[dependency-groups] -dev = [ +[tool.uv] +prerelease = "if-necessary-or-explicit" +dev-dependencies = [ "pre-commit >= 3.7", - "pyright>=1.1.400", - "mypy>=1.15.0", "ruff>=0.11.8", "poethepoet>=0.36.0", "pytest>=8.4.1", "pytest-asyncio>=1.0.0", "pytest-cov>=6.2.1", - "pytest-xdist>=3.8.0", + "pytest-xdist[psutil]>=3.8.0", + "pytest-timeout>=2.3.1", + "mypy>=1.16.1", +] +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" ] -[build-system] -requires = ["hatchling", "uv-dynamic-versioning"] -build-backend = "hatchling.build" +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [] +timeout = 120 [tool.uv.workspace] members = ["extensions/*"] @@ -91,6 +102,7 @@ select = [ ignore = [ "D100", # allow missing docstring in public module "D104", # allow missing docstring in public package + "D418", # allow overload to have a docstring "TD003", # allow missing link to todo issue "FIX002" # allow todo ] @@ -116,10 +128,9 @@ include = ["agent_framework", "tests", "samples"] typeCheckingMode = "strict" reportUnnecessaryIsInstance = false reportMissingTypeStubs = false -venvPath = "." -venv = ".venv" [tool.mypy] +plugins = ['pydantic.mypy'] strict = true python_version = "3.10" ignore_missing_imports = true @@ -136,3 +147,11 @@ disallow_any_unimported = true [tool.bandit] targets = ["agent_framework"] exclude_dirs = ["tests"] + +[tool.uv.build-backend] +module-name = "agent_framework" +module-root = "" + +[build-system] +requires = ["uv_build>=0.7.19,<0.8.0"] +build-backend = "uv_build" diff --git a/python/tests/unit/test_tool.py b/python/tests/unit/test_tool.py new file mode 100644 index 0000000000..aa8e69d5dd --- /dev/null +++ b/python/tests/unit/test_tool.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft. All rights reserved. + +from agent_framework import AITool, ai_function + + +def test_ai_function_decorator(): + """Test the ai_function decorator.""" + + @ai_function(name="test_tool", description="A test tool") + def test_tool(x: int, y: int) -> int: + """A simple function that adds two numbers.""" + return x + y + + assert isinstance(test_tool, AITool) + assert test_tool.name == "test_tool" + assert test_tool.description == "A test tool" + assert test_tool.model_json_schema() == { + "properties": {"x": {"title": "X", "type": "integer"}, "y": {"title": "Y", "type": "integer"}}, + "required": ["x", "y"], + "title": "test_tool_input", + "type": "object", + } + assert test_tool(1, 2) == 3 + + +async def test_ai_function_decorator_with_async(): + """Test the ai_function decorator with an async function.""" + + @ai_function(name="async_test_tool", description="An async test tool") + async def async_test_tool(x: int, y: int) -> int: + """An async function that adds two numbers.""" + return x + y + + assert isinstance(async_test_tool, AITool) + assert async_test_tool.name == "async_test_tool" + assert async_test_tool.description == "An async test tool" + assert async_test_tool.model_json_schema() == { + "properties": {"x": {"title": "X", "type": "integer"}, "y": {"title": "Y", "type": "integer"}}, + "required": ["x", "y"], + "title": "async_test_tool_input", + "type": "object", + } + assert (await async_test_tool(1, 2)) == 3 diff --git a/python/tests/unit/test_types.py b/python/tests/unit/test_types.py new file mode 100644 index 0000000000..16f9a56caa --- /dev/null +++ b/python/tests/unit/test_types.py @@ -0,0 +1,466 @@ +# Copyright (c) Microsoft. All rights reserved. + +from pydantic import BaseModel, ValidationError +from pytest import mark, raises + +from agent_framework import ( + AIContent, + AIContents, + ChatMessage, + ChatResponse, + ChatResponseUpdate, + ChatRole, + ChatToolMode, + DataContent, + FunctionCallContent, + FunctionResultContent, + StructuredResponse, + TextContent, + TextReasoningContent, + UriContent, + UsageDetails, +) + +# region: TextContent + + +def test_text_content_positional(): + """Test the TextContent class to ensure it initializes correctly and inherits from AIContent.""" + # Create an instance of TextContent + content = TextContent("Hello, world!", raw_representation="Hello, world!", additional_properties={"version": 1}) + + # Check the type and content + assert content.type == "text" + assert content.text == "Hello, world!" + assert content.raw_representation == "Hello, world!" + assert content.additional_properties["version"] == 1 + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + with raises(ValidationError): + content.type = "ai" + + +def test_text_content_keyword(): + """Test the TextContent class to ensure it initializes correctly and inherits from AIContent.""" + # Create an instance of TextContent + content = TextContent( + text="Hello, world!", raw_representation="Hello, world!", additional_properties={"version": 1} + ) + + # Check the type and content + assert content.type == "text" + assert content.text == "Hello, world!" + assert content.raw_representation == "Hello, world!" + assert content.additional_properties["version"] == 1 + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + with raises(ValidationError): + content.type = "ai" + + +# region: DataContent + + +def test_data_content_bytes(): + """Test the DataContent class to ensure it initializes correctly.""" + # Create an instance of DataContent + content = DataContent(data=b"test", media_type="application/octet-stream", additional_properties={"version": 1}) + + # Check the type and content + assert content.type == "data" + assert content.uri == "data:application/octet-stream;base64,dGVzdA==" + assert content.additional_properties["version"] == 1 + + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + + +def test_data_content_uri(): + """Test the DataContent class to ensure it initializes correctly with a URI.""" + # Create an instance of DataContent with a URI + content = DataContent(uri="data:application/octet-stream;base64,dGVzdA==", additional_properties={"version": 1}) + + # Check the type and content + assert content.type == "data" + assert content.uri == "data:application/octet-stream;base64,dGVzdA==" + assert content.additional_properties["version"] == 1 + + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + + +def test_data_content_invalid(): + """Test the DataContent class to ensure it raises an error for invalid initialization.""" + # Attempt to create an instance of DataContent with invalid data + # not a proper uri + with raises(ValidationError): + DataContent(uri="invalid_uri") + # unknown media type + with raises(ValidationError): + DataContent(uri="data:application/random;base64,dGVzdA==") + # not valid base64 data + + with raises(ValidationError): + DataContent(uri="data:application/json;base64,dGVzdA&") + + +def test_data_content_empty(): + """Test the DataContent class to ensure it raises an error for empty data.""" + # Attempt to create an instance of DataContent with empty data + with raises(ValidationError): + DataContent(data=b"", media_type="application/octet-stream") + + # Attempt to create an instance of DataContent with empty URI + with raises(ValidationError): + DataContent(uri="") + + +# region: UriContent + + +def test_uri_content(): + """Test the UriContent class to ensure it initializes correctly.""" + content = UriContent(uri="http://example.com", media_type="image/jpg", additional_properties={"version": 1}) + + # Check the type and content + assert content.type == "uri" + assert content.uri == "http://example.com" + assert content.media_type == "image/jpg" + assert content.additional_properties["version"] == 1 + + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + + +# region: FunctionCallContent + + +def test_function_call_content(): + """Test the FunctionCallContent class to ensure it initializes correctly.""" + content = FunctionCallContent(call_id="1", name="example_function", arguments={"param1": "value1"}) + + # Check the type and content + assert content.type == "function_call" + assert content.name == "example_function" + assert content.arguments == {"param1": "value1"} + + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + + +# region: FunctionResultContent + + +def test_function_result_content(): + """Test the FunctionResultContent class to ensure it initializes correctly.""" + content = FunctionResultContent(call_id="1", result={"param1": "value1"}) + + # Check the type and content + assert content.type == "function_result" + assert content.result == {"param1": "value1"} + + # Ensure the instance is of type AIContent + assert isinstance(content, AIContent) + + +# region: UsageDetails + + +def test_usage_details(): + usage = UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15) + assert usage.input_token_count == 5 + assert usage.output_token_count == 10 + assert usage.total_token_count == 15 + assert usage.additional_counts == {} + + +def test_usage_details_addition(): + usage1 = UsageDetails( + input_token_count=5, + output_token_count=10, + total_token_count=15, + test1=10, + test2=20, + ) + usage2 = UsageDetails( + input_token_count=3, + output_token_count=6, + total_token_count=9, + test1=10, + test3=30, + ) + + combined_usage = usage1 + usage2 + assert combined_usage.input_token_count == 8 + assert combined_usage.output_token_count == 16 + assert combined_usage.total_token_count == 24 + assert combined_usage.additional_counts["test1"] == 20 + assert combined_usage.additional_counts["test2"] == 20 + assert combined_usage.additional_counts["test3"] == 30 + + +def test_usage_details_fail(): + with raises(ValidationError): + UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15, wrong_type="42.923") + + +def test_usage_details_additional_counts(): + usage = UsageDetails(input_token_count=5, output_token_count=10, total_token_count=15, **{"test": 1}) + assert usage.additional_counts["test"] == 1 + + +# region: AIContent Serialization + + +@mark.parametrize( + "content_type, args", + [ + (TextContent, {"text": "Hello, world!"}), + (DataContent, {"data": b"Hello, world!", "media_type": "text/plain"}), + (UriContent, {"uri": "http://example.com", "media_type": "text/html"}), + (FunctionCallContent, {"call_id": "1", "name": "example_function", "arguments": {}}), + (FunctionResultContent, {"call_id": "1", "result": {}}), + ], +) +def test_ai_content_serialization(content_type: type[AIContent], args: dict): + content = content_type(**args) + serialized = content.model_dump() + deserialized = content_type.model_validate(serialized) + assert deserialized == content + + class TestModel(BaseModel): + content: AIContents + + test_item = TestModel.model_validate({"content": serialized}) + + assert isinstance(test_item.content, content_type) + + +# region: ChatMessage + + +def test_chat_message_text(): + """Test the ChatMessage class to ensure it initializes correctly with text content.""" + # Create a ChatMessage with a role and text content + message = ChatMessage(role="user", text="Hello, how are you?") + + # Check the type and content + assert message.role == ChatRole.USER + assert len(message.contents) == 1 + assert isinstance(message.contents[0], TextContent) + assert message.contents[0].text == "Hello, how are you?" + assert message.text == "Hello, how are you?" + + # Ensure the instance is of type AIContent + assert isinstance(message.contents[0], AIContent) + + +def test_chat_message_contents(): + """Test the ChatMessage class to ensure it initializes correctly with contents.""" + # Create a ChatMessage with a role and multiple contents + content1 = TextContent("Hello, how are you?") + content2 = TextContent("I'm fine, thank you!") + message = ChatMessage(role="user", contents=[content1, content2]) + + # Check the type and content + assert message.role == ChatRole.USER + assert len(message.contents) == 2 + assert isinstance(message.contents[0], TextContent) + assert isinstance(message.contents[1], TextContent) + assert message.contents[0].text == "Hello, how are you?" + assert message.contents[1].text == "I'm fine, thank you!" + assert message.text == "Hello, how are you?\nI'm fine, thank you!" + + +# region: ChatResponse + + +def test_chat_response(): + """Test the ChatResponse class to ensure it initializes correctly with a message.""" + # Create a ChatMessage + message = ChatMessage(role="assistant", text="I'm doing well, thank you!") + + # Create a ChatResponse with the message + response = ChatResponse(messages=message) + + # Check the type and content + assert response.messages[0].role == ChatRole.ASSISTANT + assert response.messages[0].text == "I'm doing well, thank you!" + assert isinstance(response.messages[0], ChatMessage) + + +# region: StructuredResponse + + +def test_structured_response(): + """Test the StructuredResponse class to ensure it initializes correctly with a value.""" + + class ResponseModel(BaseModel): + content: str + action: str + + # Create a StructuredResponse with a value + response = StructuredResponse[ResponseModel]( + value=ResponseModel(content="Hello, world!", action="test"), + text="{'content': 'Hello, world!', 'action': 'test'}", + ) + + # Check the type and content + assert response.value == ResponseModel(content="Hello, world!", action="test") + assert isinstance(response, StructuredResponse) + + +# region: ChatResponseUpdate + + +def test_chat_response_update(): + """Test the ChatResponseUpdate class to ensure it initializes correctly with a message.""" + # Create a ChatMessage + message = TextContent(text="I'm doing well, thank you!") + + # Create a ChatResponseUpdate with the message + response_update = ChatResponseUpdate(contents=[message]) + + # Check the type and content + assert response_update.contents[0].text == "I'm doing well, thank you!" + assert isinstance(response_update.contents[0], TextContent) + + +def test_chat_response_updates_to_chat_response_one(): + """Test converting ChatResponseUpdate to ChatResponse.""" + # Create a ChatMessage + message1 = TextContent("I'm doing well, ") + message2 = TextContent("thank you!") + + # Create a ChatResponseUpdate with the message + response_updates = [ + ChatResponseUpdate(text=message1, message_id="1"), + ChatResponseUpdate(text=message2, message_id="1"), + ] + + # Convert to ChatResponse + chat_response = ChatResponse.from_chat_response_updates(response_updates) + + # Check the type and content + assert len(chat_response.messages) == 1 + assert chat_response.text == "I'm doing well, \nthank you!" + assert isinstance(chat_response.messages[0], ChatMessage) + assert len(chat_response.messages[0].contents) == 1 + assert chat_response.messages[0].message_id == "1" + + +def test_chat_response_updates_to_chat_response_two(): + """Test converting ChatResponseUpdate to ChatResponse.""" + # Create a ChatMessage + message1 = TextContent("I'm doing well, ") + message2 = TextContent("thank you!") + + # Create a ChatResponseUpdate with the message + response_updates = [ + ChatResponseUpdate(text=message1, message_id="1"), + ChatResponseUpdate(text=message2, message_id="2"), + ] + + # Convert to ChatResponse + chat_response = ChatResponse.from_chat_response_updates(response_updates) + + # Check the type and content + assert len(chat_response.messages) == 2 + assert chat_response.text == "I'm doing well, \nthank you!" + assert isinstance(chat_response.messages[0], ChatMessage) + assert chat_response.messages[0].message_id == "1" + assert isinstance(chat_response.messages[1], ChatMessage) + assert chat_response.messages[1].message_id == "2" + + +def test_chat_response_updates_to_chat_response_multiple(): + """Test converting ChatResponseUpdate to ChatResponse.""" + # Create a ChatMessage + message1 = TextContent("I'm doing well, ") + message2 = TextContent("thank you!") + + # Create a ChatResponseUpdate with the message + response_updates = [ + ChatResponseUpdate(text=message1, message_id="1"), + ChatResponseUpdate(contents=[TextReasoningContent(text="Additional context")], message_id="1"), + ChatResponseUpdate(text=message2, message_id="1"), + ] + + # Convert to ChatResponse + chat_response = ChatResponse.from_chat_response_updates(response_updates) + + # Check the type and content + assert len(chat_response.messages) == 1 + assert chat_response.text == "I'm doing well, \nthank you!" + assert isinstance(chat_response.messages[0], ChatMessage) + assert len(chat_response.messages[0].contents) == 3 + assert chat_response.messages[0].message_id == "1" + + +def test_chat_response_updates_to_chat_response_multiple_multiple(): + """Test converting ChatResponseUpdate to ChatResponse.""" + # Create a ChatMessage + message1 = TextContent("I'm doing well, ") + message2 = TextContent("thank you!") + + # Create a ChatResponseUpdate with the message + response_updates = [ + ChatResponseUpdate(text=message1, message_id="1"), + ChatResponseUpdate(text=message2, message_id="1"), + ChatResponseUpdate(contents=[TextReasoningContent(text="Additional context")], message_id="1"), + ChatResponseUpdate(contents=[TextContent(text="More context")], message_id="1"), + ChatResponseUpdate(text="Final part", message_id="1"), + ] + + # Convert to ChatResponse + chat_response = ChatResponse.from_chat_response_updates(response_updates) + + # Check the type and content + assert len(chat_response.messages) == 1 + assert chat_response.text == "I'm doing well, \nthank you!\nMore context\nFinal part" + assert isinstance(chat_response.messages[0], ChatMessage) + assert len(chat_response.messages[0].contents) == 3 + assert chat_response.messages[0].message_id == "1" + + +# region: ChatToolMode + + +def test_chat_tool_mode(): + """Test the ChatToolMode class to ensure it initializes correctly.""" + # Create instances of ChatToolMode + auto_mode = ChatToolMode.AUTO + required_any = ChatToolMode.REQUIRED_ANY + required_mode = ChatToolMode.REQUIRED("example_function") + none_mode = ChatToolMode.NONE + + # Check the type and content + assert auto_mode.mode == "auto" + assert auto_mode.required_function_name is None + assert required_any.mode == "required" + assert required_any.required_function_name is None + assert required_mode.mode == "required" + assert required_mode.required_function_name == "example_function" + assert none_mode.mode == "none" + assert none_mode.required_function_name is None + + # Ensure the instances are of type ChatToolMode + assert isinstance(auto_mode, ChatToolMode) + assert isinstance(required_any, ChatToolMode) + assert isinstance(required_mode, ChatToolMode) + assert isinstance(none_mode, ChatToolMode) + + assert ChatToolMode.REQUIRED("example_function") == ChatToolMode.REQUIRED("example_function") + + +def test_chat_tool_mode_from_dict(): + """Test creating ChatToolMode from a dictionary.""" + mode_dict = {"mode": "required", "required_function_name": "example_function"} + mode = ChatToolMode(**mode_dict) + + # Check the type and content + assert mode.mode == "required" + assert mode.required_function_name == "example_function" + + # Ensure the instance is of type ChatToolMode + assert isinstance(mode, ChatToolMode) diff --git a/python/tests/unit/test_version.py b/python/tests/unit/test_version.py new file mode 100644 index 0000000000..56bc0d9acb --- /dev/null +++ b/python/tests/unit/test_version.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from agent_framework import __version__ + + +def test_version(): + assert __version__ is not None diff --git a/python/uv.lock b/python/uv.lock index a29c0dd01b..adf45ec4e4 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -1,6 +1,16 @@ version = 1 revision = 2 requires-python = ">=3.10" +resolution-markers = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'", +] +supported-markers = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'", +] [manifest] members = [ @@ -14,22 +24,23 @@ name = "agent-framework" version = "0.1.0b1" source = { editable = "." } dependencies = [ - { name = "azure" }, - { name = "openai" }, - { name = "pydantic" }, + { name = "azure", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.dev-dependencies] dev = [ - { name = "mypy" }, - { name = "poethepoet" }, - { name = "pre-commit" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "pytest-xdist" }, - { name = "ruff" }, + { name = "mypy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "poethepoet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pre-commit", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-asyncio", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-timeout", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest-xdist", extra = ["psutil"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] @@ -37,18 +48,19 @@ requires-dist = [ { name = "azure", editable = "extensions/azure" }, { name = "openai", editable = "extensions/openai" }, { name = "pydantic", specifier = ">=2.11.7" }, + { name = "typing-extensions", specifier = ">=4.14.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "mypy", specifier = ">=1.15.0" }, + { name = "mypy", specifier = ">=1.16.1" }, { name = "poethepoet", specifier = ">=0.36.0" }, { name = "pre-commit", specifier = ">=3.7" }, - { name = "pyright", specifier = ">=1.1.400" }, { name = "pytest", specifier = ">=8.4.1" }, { name = "pytest-asyncio", specifier = ">=1.0.0" }, { name = "pytest-cov", specifier = ">=6.2.1" }, - { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "pytest-xdist", extras = ["psutil"], specifier = ">=3.8.0" }, { name = "ruff", specifier = ">=0.11.8" }, ] @@ -66,8 +78,8 @@ name = "azure" version = "0.1.0" source = { editable = "extensions/azure" } dependencies = [ - { name = "agent-framework" }, - { name = "openai" }, + { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] @@ -96,71 +108,71 @@ wheels = [ [[package]] name = "coverage" -version = "7.9.1" +version = "7.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" }, - { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" }, - { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" }, - { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" }, - { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, - { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, - { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, - { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, - { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, - { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, - { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, - { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, - { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, - { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, - { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/5c2114fd776c207bd55068ae8dc1bef63ecd1b767b3389984a8e58f2b926/coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912", size = 212039, upload-time = "2025-07-03T10:52:38.955Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/dc51f40492dc2d5fcd31bb44577bc0cc8920757d6bc5d3e4293146524ef9/coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f", size = 212428, upload-time = "2025-07-03T10:52:41.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a3/55cb3ff1b36f00df04439c3993d8529193cdf165a2467bf1402539070f16/coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f", size = 241534, upload-time = "2025-07-03T10:52:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c9/a8410b91b6be4f6e9c2e9f0dce93749b6b40b751d7065b4410bf89cb654b/coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf", size = 239408, upload-time = "2025-07-03T10:52:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c4/6f3e56d467c612b9070ae71d5d3b114c0b899b5788e1ca3c93068ccb7018/coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547", size = 240552, upload-time = "2025-07-03T10:52:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/fd/20/04eda789d15af1ce79bce5cc5fd64057c3a0ac08fd0576377a3096c24663/coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45", size = 240464, upload-time = "2025-07-03T10:52:46.809Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5a/217b32c94cc1a0b90f253514815332d08ec0812194a1ce9cca97dda1cd20/coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2", size = 239134, upload-time = "2025-07-03T10:52:48.149Z" }, + { url = "https://files.pythonhosted.org/packages/34/73/1d019c48f413465eb5d3b6898b6279e87141c80049f7dbf73fd020138549/coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e", size = 239405, upload-time = "2025-07-03T10:52:49.687Z" }, + { url = "https://files.pythonhosted.org/packages/49/6c/a2beca7aa2595dad0c0d3f350382c381c92400efe5261e2631f734a0e3fe/coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e", size = 214519, upload-time = "2025-07-03T10:52:51.036Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c8/91e5e4a21f9a51e2c7cdd86e587ae01a4fcff06fc3fa8cde4d6f7cf68df4/coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c", size = 215400, upload-time = "2025-07-03T10:52:52.313Z" }, + { url = "https://files.pythonhosted.org/packages/39/40/916786453bcfafa4c788abee4ccd6f592b5b5eca0cd61a32a4e5a7ef6e02/coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba", size = 212152, upload-time = "2025-07-03T10:52:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/9f/66/cc13bae303284b546a030762957322bbbff1ee6b6cb8dc70a40f8a78512f/coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa", size = 212540, upload-time = "2025-07-03T10:52:55.196Z" }, + { url = "https://files.pythonhosted.org/packages/0f/3c/d56a764b2e5a3d43257c36af4a62c379df44636817bb5f89265de4bf8bd7/coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a", size = 245097, upload-time = "2025-07-03T10:52:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/b1/46/bd064ea8b3c94eb4ca5d90e34d15b806cba091ffb2b8e89a0d7066c45791/coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc", size = 242812, upload-time = "2025-07-03T10:52:57.842Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/d91992c2b29bc7afb729463bc918ebe5f361be7f1daae93375a5759d1e28/coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2", size = 244617, upload-time = "2025-07-03T10:52:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4f/8fadff6bf56595a16d2d6e33415841b0163ac660873ed9a4e9046194f779/coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c", size = 244263, upload-time = "2025-07-03T10:53:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/e0be7446a2bba11739edb9f9ba4eff30b30d8257370e237418eb44a14d11/coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd", size = 242314, upload-time = "2025-07-03T10:53:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/dcbac9345000121b8b57a3094c2dfcf1ccc52d8a14a40c1d4bc89f936f80/coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74", size = 242904, upload-time = "2025-07-03T10:53:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/11e8db0a0c0510cf31bbbdc8caf5d74a358b696302a45948d7c768dfd1cf/coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6", size = 214553, upload-time = "2025-07-03T10:53:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/751794ec8907a15e257136e48dc1021b1f671220ecccfd6c4eaf30802714/coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7", size = 215441, upload-time = "2025-07-03T10:53:06.472Z" }, + { url = "https://files.pythonhosted.org/packages/62/5b/34abcedf7b946c1c9e15b44f326cb5b0da852885312b30e916f674913428/coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62", size = 213873, upload-time = "2025-07-03T10:53:07.699Z" }, + { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, + { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, + { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, + { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, + { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/f8bbefac27d286386961c25515431482a425967e23d3698b75a250872924/coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050", size = 204013, upload-time = "2025-07-03T10:54:12.084Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, ] [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, + { name = "tomli", marker = "(python_full_version <= '3.11' and sys_platform == 'darwin') or (python_full_version <= '3.11' and sys_platform == 'linux') or (python_full_version <= '3.11' and sys_platform == 'win32')" }, ] [[package]] @@ -177,7 +189,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -204,11 +216,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.10" +version = "2.6.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, ] [[package]] @@ -222,40 +234,41 @@ wheels = [ [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, + { name = "mypy-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pathspec", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload-time = "2025-02-05T03:49:29.145Z" }, - { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload-time = "2025-02-05T03:49:16.986Z" }, - { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload-time = "2025-02-05T03:49:46.908Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload-time = "2025-02-05T03:50:05.89Z" }, - { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload-time = "2025-02-05T03:49:33.56Z" }, - { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload-time = "2025-02-05T03:49:38.981Z" }, - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, ] [[package]] @@ -281,7 +294,7 @@ name = "openai" version = "0.1.0" source = { editable = "extensions/openai" } dependencies = [ - { name = "agent-framework" }, + { name = "agent-framework", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] [package.metadata] @@ -306,12 +319,21 @@ wheels = [ ] [[package]] -name = "platformdirs" -version = "4.3.7" +name = "pathspec" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] @@ -328,9 +350,9 @@ name = "poethepoet" version = "0.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pastel" }, - { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "pastel", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/ac/311c8a492dc887f0b7a54d0ec3324cb2f9538b7b78ea06e5f7ae1f167e52/poethepoet-0.36.0.tar.gz", hash = "sha256:2217b49cb4e4c64af0b42ff8c4814b17f02e107d38bc461542517348ede25663", size = 66854, upload-time = "2025-06-29T19:54:50.444Z" } wheels = [ @@ -342,26 +364,41 @@ name = "pre-commit" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, + { name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "identify", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "nodeenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] 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" } 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" }, ] +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ @@ -373,7 +410,7 @@ name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ @@ -464,31 +501,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pyright" -version = "1.1.400" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546, upload-time = "2025-04-24T12:55:18.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460, upload-time = "2025-04-24T12:55:17.002Z" }, -] - [[package]] name = "pytest" version = "8.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, + { name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "tomli", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } wheels = [ @@ -500,7 +524,7 @@ name = "pytest-asyncio" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } wheels = [ @@ -512,28 +536,45 @@ name = "pytest-cov" version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, + { name = "coverage", extra = ["toml"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, ] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + [[package]] name = "pytest-xdist" version = "3.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "execnet" }, - { name = "pytest" }, + { name = "execnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[package.optional-dependencies] +psutil = [ + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -580,27 +621,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.8" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f6/adcf73711f31c9f5393862b4281c875a462d9f639f4ccdf69dc368311c20/ruff-0.11.8.tar.gz", hash = "sha256:6d742d10626f9004b781f4558154bb226620a7242080e11caeffab1a40e99df8", size = 4086399, upload-time = "2025-05-01T14:53:24.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/38/796a101608a90494440856ccfb52b1edae90de0b817e76bfade66b12d320/ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c", size = 4413426, upload-time = "2025-06-26T20:34:14.784Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/60/c6aa9062fa518a9f86cb0b85248245cddcd892a125ca00441df77d79ef88/ruff-0.11.8-py3-none-linux_armv6l.whl", hash = "sha256:896a37516c594805e34020c4a7546c8f8a234b679a7716a3f08197f38913e1a3", size = 10272473, upload-time = "2025-05-01T14:52:37.252Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/0325e50d106dc87c00695f7bcd5044c6d252ed5120ebf423773e00270f50/ruff-0.11.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab86d22d3d721a40dd3ecbb5e86ab03b2e053bc93c700dc68d1c3346b36ce835", size = 11040862, upload-time = "2025-05-01T14:52:41.022Z" }, - { url = "https://files.pythonhosted.org/packages/e6/27/b87ea1a7be37fef0adbc7fd987abbf90b6607d96aa3fc67e2c5b858e1e53/ruff-0.11.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:258f3585057508d317610e8a412788cf726efeefa2fec4dba4001d9e6f90d46c", size = 10385273, upload-time = "2025-05-01T14:52:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f7/3346161570d789045ed47a86110183f6ac3af0e94e7fd682772d89f7f1a1/ruff-0.11.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727d01702f7c30baed3fc3a34901a640001a2828c793525043c29f7614994a8c", size = 10578330, upload-time = "2025-05-01T14:52:45.48Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c3/327fb950b4763c7b3784f91d3038ef10c13b2d42322d4ade5ce13a2f9edb/ruff-0.11.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dca977cc4fc8f66e89900fa415ffe4dbc2e969da9d7a54bfca81a128c5ac219", size = 10122223, upload-time = "2025-05-01T14:52:47.675Z" }, - { url = "https://files.pythonhosted.org/packages/de/c7/ba686bce9adfeb6c61cb1bbadc17d58110fe1d602f199d79d4c880170f19/ruff-0.11.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c657fa987d60b104d2be8b052d66da0a2a88f9bd1d66b2254333e84ea2720c7f", size = 11697353, upload-time = "2025-05-01T14:52:50.264Z" }, - { url = "https://files.pythonhosted.org/packages/53/8e/a4fb4a1ddde3c59e73996bb3ac51844ff93384d533629434b1def7a336b0/ruff-0.11.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f2e74b021d0de5eceb8bd32919f6ff8a9b40ee62ed97becd44993ae5b9949474", size = 12375936, upload-time = "2025-05-01T14:52:52.394Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a1/9529cb1e2936e2479a51aeb011307e7229225df9ac64ae064d91ead54571/ruff-0.11.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9b5ef39820abc0f2c62111f7045009e46b275f5b99d5e59dda113c39b7f4f38", size = 11850083, upload-time = "2025-05-01T14:52:55.424Z" }, - { url = "https://files.pythonhosted.org/packages/3e/94/8f7eac4c612673ae15a4ad2bc0ee62e03c68a2d4f458daae3de0e47c67ba/ruff-0.11.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1dba3135ca503727aa4648152c0fa67c3b1385d3dc81c75cd8a229c4b2a1458", size = 14005834, upload-time = "2025-05-01T14:52:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7c/6f63b46b2be870cbf3f54c9c4154d13fac4b8827f22fa05ac835c10835b2/ruff-0.11.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f024d32e62faad0f76b2d6afd141b8c171515e4fb91ce9fd6464335c81244e5", size = 11503713, upload-time = "2025-05-01T14:53:01.244Z" }, - { url = "https://files.pythonhosted.org/packages/3a/91/57de411b544b5fe072779678986a021d87c3ee5b89551f2ca41200c5d643/ruff-0.11.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d365618d3ad747432e1ae50d61775b78c055fee5936d77fb4d92c6f559741948", size = 10457182, upload-time = "2025-05-01T14:53:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/01/49/cfe73e0ce5ecdd3e6f1137bf1f1be03dcc819d1bfe5cff33deb40c5926db/ruff-0.11.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4d9aaa91035bdf612c8ee7266153bcf16005c7c7e2f5878406911c92a31633cb", size = 10101027, upload-time = "2025-05-01T14:53:06.555Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/a5cfe47c62b3531675795f38a0ef1c52ff8de62eaddf370d46634391a3fb/ruff-0.11.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0eba551324733efc76116d9f3a0d52946bc2751f0cd30661564117d6fd60897c", size = 11111298, upload-time = "2025-05-01T14:53:08.825Z" }, - { url = "https://files.pythonhosted.org/packages/36/98/f76225f87e88f7cb669ae92c062b11c0a1e91f32705f829bd426f8e48b7b/ruff-0.11.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:161eb4cff5cfefdb6c9b8b3671d09f7def2f960cee33481dd898caf2bcd02304", size = 11566884, upload-time = "2025-05-01T14:53:11.626Z" }, - { url = "https://files.pythonhosted.org/packages/de/7e/fff70b02e57852fda17bd43f99dda37b9bcf3e1af3d97c5834ff48d04715/ruff-0.11.8-py3-none-win32.whl", hash = "sha256:5b18caa297a786465cc511d7f8be19226acf9c0a1127e06e736cd4e1878c3ea2", size = 10451102, upload-time = "2025-05-01T14:53:14.303Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/eaa571eb70648c9bde3120a1d5892597de57766e376b831b06e7c1e43945/ruff-0.11.8-py3-none-win_amd64.whl", hash = "sha256:6e70d11043bef637c5617297bdedec9632af15d53ac1e1ba29c448da9341b0c4", size = 11597410, upload-time = "2025-05-01T14:53:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/cd/be/f6b790d6ae98f1f32c645f8540d5c96248b72343b0a56fab3a07f2941897/ruff-0.11.8-py3-none-win_arm64.whl", hash = "sha256:304432e4c4a792e3da85b7699feb3426a0908ab98bf29df22a31b0cdd098fac2", size = 10713129, upload-time = "2025-05-01T14:53:22.27Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/3dba52c1d12ab5e78d75bd78ad52fb85a6a1f29cc447c2423037b82bed0d/ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b", size = 10305649, upload-time = "2025-06-26T20:33:39.242Z" }, + { url = "https://files.pythonhosted.org/packages/8c/65/dab1ba90269bc8c81ce1d499a6517e28fe6f87b2119ec449257d0983cceb/ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0", size = 11120201, upload-time = "2025-06-26T20:33:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3e/2d819ffda01defe857fa2dd4cba4d19109713df4034cc36f06bbf582d62a/ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be", size = 10466769, upload-time = "2025-06-26T20:33:44.102Z" }, + { url = "https://files.pythonhosted.org/packages/63/37/bde4cf84dbd7821c8de56ec4ccc2816bce8125684f7b9e22fe4ad92364de/ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff", size = 10660902, upload-time = "2025-06-26T20:33:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3a/390782a9ed1358c95e78ccc745eed1a9d657a537e5c4c4812fce06c8d1a0/ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d", size = 10167002, upload-time = "2025-06-26T20:33:47.81Z" }, + { url = "https://files.pythonhosted.org/packages/6d/05/f2d4c965009634830e97ffe733201ec59e4addc5b1c0efa035645baa9e5f/ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd", size = 11751522, upload-time = "2025-06-26T20:33:49.857Z" }, + { url = "https://files.pythonhosted.org/packages/35/4e/4bfc519b5fcd462233f82fc20ef8b1e5ecce476c283b355af92c0935d5d9/ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010", size = 12520264, upload-time = "2025-06-26T20:33:52.199Z" }, + { url = "https://files.pythonhosted.org/packages/85/b2/7756a6925da236b3a31f234b4167397c3e5f91edb861028a631546bad719/ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e", size = 12133882, upload-time = "2025-06-26T20:33:54.231Z" }, + { url = "https://files.pythonhosted.org/packages/dd/00/40da9c66d4a4d51291e619be6757fa65c91b92456ff4f01101593f3a1170/ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed", size = 11608941, upload-time = "2025-06-26T20:33:56.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/e7/f898391cc026a77fbe68dfea5940f8213622474cb848eb30215538a2dadf/ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc", size = 11602887, upload-time = "2025-06-26T20:33:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/0891872fc6aab8678084f4cf8826f85c5d2d24aa9114092139a38123f94b/ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9", size = 10521742, upload-time = "2025-06-26T20:34:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/2a/98/d6534322c74a7d47b0f33b036b2498ccac99d8d8c40edadb552c038cecf1/ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13", size = 10149909, upload-time = "2025-06-26T20:34:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/34/5c/9b7ba8c19a31e2b6bd5e31aa1e65b533208a30512f118805371dbbbdf6a9/ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c", size = 11136005, upload-time = "2025-06-26T20:34:04.723Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/9bbefa4d0ff2c000e4e533f591499f6b834346025e11da97f4ded21cb23e/ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6", size = 11648579, upload-time = "2025-06-26T20:34:06.766Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1c/20cdb593783f8f411839ce749ec9ae9e4298c2b2079b40295c3e6e2089e1/ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245", size = 10519495, upload-time = "2025-06-26T20:34:08.718Z" }, + { url = "https://files.pythonhosted.org/packages/cf/56/7158bd8d3cf16394928f47c637d39a7d532268cd45220bdb6cd622985760/ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013", size = 11547485, upload-time = "2025-06-26T20:34:11.008Z" }, + { url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209, upload-time = "2025-06-26T20:34:12.928Z" }, ] [[package]] @@ -644,11 +685,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, ] [[package]] @@ -656,7 +697,7 @@ name = "typing-inspection" version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ @@ -665,14 +706,14 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.31.1" +version = "20.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, + { name = "distlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "filelock", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/07/655f4fb9592967f49197b00015bb5538d3ed1f8f96621a10bebc3bb822e2/virtualenv-20.31.1.tar.gz", hash = "sha256:65442939608aeebb9284cd30baca5865fcd9f12b58bb740a24b220030df46d26", size = 6076234, upload-time = "2025-05-05T22:45:39.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/67/7d7559264a6f8ec9ce4e397ddd9157a510be1e174dc98be898b6c18eeef4/virtualenv-20.31.1-py3-none-any.whl", hash = "sha256:f448cd2f1604c831afb9ea238021060be2c0edbcad8eb0a4e8b4e14ff11a5482", size = 6057843, upload-time = "2025-05-05T22:45:37.127Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, ]