mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-20 22:08:07 +00:00
Compare commits
1 Commits
nh/middlew
...
sr/anthrop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
856b8770e1 |
@@ -13,12 +13,20 @@ from langchain_anthropic.middleware.file_search import (
|
||||
from langchain_anthropic.middleware.prompt_caching import (
|
||||
AnthropicPromptCachingMiddleware,
|
||||
)
|
||||
from langchain_anthropic.middleware.skills import (
|
||||
ClaudeSkillsMiddleware,
|
||||
LocalSkillConfig,
|
||||
SkillConfig,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AnthropicPromptCachingMiddleware",
|
||||
"ClaudeBashToolMiddleware",
|
||||
"ClaudeSkillsMiddleware",
|
||||
"FilesystemClaudeMemoryMiddleware",
|
||||
"FilesystemClaudeTextEditorMiddleware",
|
||||
"LocalSkillConfig",
|
||||
"SkillConfig",
|
||||
"StateClaudeMemoryMiddleware",
|
||||
"StateClaudeTextEditorMiddleware",
|
||||
"StateFileSearchMiddleware",
|
||||
|
||||
572
libs/partners/anthropic/langchain_anthropic/middleware/skills.py
Normal file
572
libs/partners/anthropic/langchain_anthropic/middleware/skills.py
Normal file
@@ -0,0 +1,572 @@
|
||||
"""Anthropic Skills middleware.
|
||||
|
||||
Requires:
|
||||
- langchain: For agent middleware framework
|
||||
- langchain-anthropic: For ChatAnthropic model (already a dependency)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import zipfile
|
||||
from collections.abc import Awaitable, Callable
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
from warnings import warn
|
||||
|
||||
from langchain_anthropic.chat_models import ChatAnthropic
|
||||
|
||||
try:
|
||||
from langchain.agents.middleware.types import (
|
||||
AgentMiddleware,
|
||||
ModelCallResult,
|
||||
ModelRequest,
|
||||
ModelResponse,
|
||||
)
|
||||
except ImportError as e:
|
||||
msg = (
|
||||
"ClaudeSkillsMiddleware requires 'langchain' to be installed. "
|
||||
"This middleware is designed for use with LangChain agents. "
|
||||
"Install it with: pip install langchain"
|
||||
)
|
||||
raise ImportError(msg) from e
|
||||
|
||||
|
||||
# Pre-built Anthropic skill IDs
|
||||
ANTHROPIC_SKILLS = {"pptx", "xlsx", "docx", "pdf"}
|
||||
|
||||
# Required beta headers for Skills functionality
|
||||
REQUIRED_BETAS = [
|
||||
"code-execution-2025-08-25",
|
||||
"skills-2025-10-02",
|
||||
"files-api-2025-04-14",
|
||||
]
|
||||
|
||||
|
||||
class SkillConfig:
|
||||
"""Configuration for a single skill.
|
||||
|
||||
Args:
|
||||
skill_id: The skill identifier. For Anthropic pre-built skills, use
|
||||
`'pptx'`, `'xlsx'`, `'docx'`, or `'pdf'`. For custom skills, use
|
||||
the ID from the `/v1/skills` endpoint.
|
||||
type: The skill type. `'anthropic'` for pre-built skills or `'custom'`
|
||||
for user-uploaded skills.
|
||||
version: The skill version. Use `'latest'` for the most recent version,
|
||||
or specify a date-based version for Anthropic skills (e.g., `'20251002'`)
|
||||
or epoch timestamp for custom skills.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
skill_id: str,
|
||||
type: Literal["anthropic", "custom"] = "anthropic", # noqa: A002
|
||||
version: str = "latest",
|
||||
) -> None:
|
||||
"""Initialize skill configuration."""
|
||||
self.skill_id = skill_id
|
||||
self.type = type
|
||||
self.version = version
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
"""Convert skill configuration to API format.
|
||||
|
||||
Returns:
|
||||
Dictionary with skill configuration for API requests.
|
||||
"""
|
||||
return {
|
||||
"type": self.type,
|
||||
"skill_id": self.skill_id,
|
||||
"version": self.version,
|
||||
}
|
||||
|
||||
|
||||
class LocalSkillConfig(SkillConfig):
|
||||
"""Configuration for a skill loaded from local files.
|
||||
|
||||
This class handles loading skills from the local filesystem and uploading
|
||||
them to the Anthropic API. Skills are expected to be in a directory containing
|
||||
a `SKILL.md` file with YAML frontmatter.
|
||||
|
||||
Args:
|
||||
path: Path to the skill directory or zip file containing the skill.
|
||||
The directory must contain a `SKILL.md` file with required frontmatter.
|
||||
display_title: Optional display title for the skill when uploaded.
|
||||
If not provided, the name from the SKILL.md frontmatter will be used.
|
||||
auto_upload: If `True`, automatically upload the skill when the middleware
|
||||
is initialized. If `False`, you must manually upload via the
|
||||
`upload_skill` method.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from langchain_anthropic.middleware import (
|
||||
ClaudeSkillsMiddleware,
|
||||
LocalSkillConfig,
|
||||
)
|
||||
|
||||
# Create middleware with local skill
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=[
|
||||
"pptx", # Pre-built skill
|
||||
LocalSkillConfig(path="./my-custom-skill"), # Local skill
|
||||
]
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: str | Path,
|
||||
*,
|
||||
display_title: str | None = None,
|
||||
auto_upload: bool = True,
|
||||
) -> None:
|
||||
"""Initialize local skill configuration.
|
||||
|
||||
Args:
|
||||
path: Path to the skill directory or zip file.
|
||||
display_title: Optional display title for the skill.
|
||||
auto_upload: Whether to automatically upload the skill.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the path does not exist.
|
||||
ValueError: If the path is not a valid skill directory or zip file.
|
||||
"""
|
||||
self.path = Path(path)
|
||||
self.display_title = display_title
|
||||
self.auto_upload = auto_upload
|
||||
self._uploaded_skill_id: str | None = None
|
||||
self._content_hash: str | None = None
|
||||
|
||||
# Validate path exists
|
||||
if not self.path.exists():
|
||||
msg = f"Skill path does not exist: {self.path}"
|
||||
raise FileNotFoundError(msg)
|
||||
|
||||
# Validate it's a directory or zip file
|
||||
if not (self.path.is_dir() or self.path.suffix == ".zip"):
|
||||
msg = f"Skill path must be a directory or .zip file, got: {self.path}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# For directories, validate SKILL.md exists
|
||||
if self.path.is_dir():
|
||||
skill_md = self.path / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
msg = f"Skill directory must contain a SKILL.md file: {self.path}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# Initialize parent class with placeholder values
|
||||
# These will be set after upload
|
||||
super().__init__(
|
||||
skill_id="", # Will be set after upload
|
||||
type="custom",
|
||||
version="latest",
|
||||
)
|
||||
|
||||
def _compute_content_hash(self) -> str:
|
||||
"""Compute a hash of the skill content for caching.
|
||||
|
||||
Returns:
|
||||
SHA256 hash of the skill content.
|
||||
"""
|
||||
hasher = hashlib.sha256()
|
||||
|
||||
if self.path.is_dir():
|
||||
# Hash all files in the directory
|
||||
for root, _dirs, files in os.walk(self.path):
|
||||
for file in sorted(files): # Sort for consistent hashing
|
||||
file_path = Path(root) / file
|
||||
hasher.update(str(file_path.relative_to(self.path)).encode())
|
||||
hasher.update(file_path.read_bytes())
|
||||
else:
|
||||
# Hash the zip file
|
||||
hasher.update(self.path.read_bytes())
|
||||
|
||||
return hasher.hexdigest()
|
||||
|
||||
def _create_zip_bytes(self) -> bytes:
|
||||
"""Create a zip file from the skill directory or read existing zip.
|
||||
|
||||
The zip must contain a top-level folder with all files inside it,
|
||||
as required by the Anthropic Skills API.
|
||||
|
||||
Returns:
|
||||
Bytes of the zip file.
|
||||
"""
|
||||
if self.path.suffix == ".zip":
|
||||
return self.path.read_bytes()
|
||||
|
||||
# Create zip file in memory with top-level folder
|
||||
# The directory name becomes the top-level folder in the zip
|
||||
zip_buffer = BytesIO()
|
||||
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for root, _dirs, files in os.walk(self.path):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
# Create path relative to parent, preserving directory name
|
||||
rel_path = file_path.relative_to(self.path.parent)
|
||||
zip_file.write(file_path, str(rel_path))
|
||||
|
||||
return zip_buffer.getvalue()
|
||||
|
||||
def upload_skill(self, client: Any) -> str:
|
||||
"""Upload the skill to Anthropic API.
|
||||
|
||||
Args:
|
||||
client: Anthropic client instance.
|
||||
|
||||
Returns:
|
||||
The uploaded skill ID.
|
||||
|
||||
Raises:
|
||||
ValueError: If upload fails.
|
||||
"""
|
||||
# Check if already uploaded with same content
|
||||
content_hash = self._compute_content_hash()
|
||||
if self._uploaded_skill_id and self._content_hash == content_hash:
|
||||
return self._uploaded_skill_id
|
||||
|
||||
# Create zip file
|
||||
zip_bytes = self._create_zip_bytes()
|
||||
|
||||
# Upload to Anthropic
|
||||
try:
|
||||
response = client.beta.skills.create(
|
||||
files=[("skill.zip", zip_bytes, "application/zip")],
|
||||
display_title=self.display_title,
|
||||
betas=REQUIRED_BETAS,
|
||||
)
|
||||
|
||||
# Store the skill ID and hash
|
||||
self._uploaded_skill_id = response.id
|
||||
self._content_hash = content_hash
|
||||
self.skill_id = response.id
|
||||
|
||||
return response.id
|
||||
|
||||
except Exception as e:
|
||||
msg = f"Failed to upload skill from {self.path}: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
@property
|
||||
def is_uploaded(self) -> bool:
|
||||
"""Check if the skill has been uploaded.
|
||||
|
||||
Returns:
|
||||
`True` if the skill has been uploaded, `False` otherwise.
|
||||
"""
|
||||
return self._uploaded_skill_id is not None
|
||||
|
||||
|
||||
class ClaudeSkillsMiddleware(AgentMiddleware):
|
||||
"""Skills Middleware for Claude.
|
||||
|
||||
Enables Claude to use Skills - specialized capabilities for document processing,
|
||||
data manipulation, and other domain-specific tasks. Skills are folders containing
|
||||
instructions, scripts, and resources that Claude loads when relevant.
|
||||
|
||||
This middleware:
|
||||
|
||||
- Adds skills to model requests via the `container` parameter
|
||||
- Injects required beta headers for Skills functionality
|
||||
- Automatically adds code execution tool if not present (required for Skills)
|
||||
- Supports both Anthropic pre-built skills and custom skills
|
||||
- Handles uploading local skills to the Anthropic API
|
||||
|
||||
Requires both 'langchain' and 'langchain-anthropic' packages to be installed.
|
||||
|
||||
Learn more about Claude Skills
|
||||
[here](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview).
|
||||
|
||||
Example:
|
||||
Basic usage with pre-built skills:
|
||||
|
||||
```python
|
||||
from langchain_anthropic.middleware import ClaudeSkillsMiddleware
|
||||
|
||||
# Enable PowerPoint and Excel skills
|
||||
skills_middleware = ClaudeSkillsMiddleware(skills=["pptx", "xlsx"])
|
||||
|
||||
# Use with an agent
|
||||
agent = create_react_agent(
|
||||
model=ChatAnthropic(model="claude-sonnet-4-5-20250929"),
|
||||
tools=[...],
|
||||
middleware=[skills_middleware],
|
||||
)
|
||||
```
|
||||
|
||||
Advanced usage with custom skills from API:
|
||||
|
||||
```python
|
||||
from langchain_anthropic.middleware import (
|
||||
ClaudeSkillsMiddleware,
|
||||
SkillConfig,
|
||||
)
|
||||
|
||||
# Mix pre-built and custom skills
|
||||
skills_middleware = ClaudeSkillsMiddleware(
|
||||
skills=[
|
||||
"pptx", # Pre-built skill
|
||||
SkillConfig(
|
||||
skill_id="custom-skill-123",
|
||||
type="custom",
|
||||
version="latest",
|
||||
),
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Using local skill files:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from langchain_anthropic.middleware import (
|
||||
ClaudeSkillsMiddleware,
|
||||
LocalSkillConfig,
|
||||
)
|
||||
|
||||
# Load skill from local directory
|
||||
skills_middleware = ClaudeSkillsMiddleware(
|
||||
skills=[
|
||||
"xlsx", # Pre-built skill
|
||||
LocalSkillConfig(
|
||||
path="./my-custom-skill", # Directory with SKILL.md
|
||||
display_title="My Custom Skill",
|
||||
auto_upload=True, # Automatically upload when first used
|
||||
),
|
||||
]
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
skills: list[str | SkillConfig | LocalSkillConfig],
|
||||
unsupported_model_behavior: Literal["ignore", "warn", "raise"] = "warn",
|
||||
code_execution_tool_name: str = "code_execution",
|
||||
) -> None:
|
||||
"""Initialize the middleware with skills configuration.
|
||||
|
||||
Args:
|
||||
skills: List of skills to enable. Can be:
|
||||
- Skill ID strings for Anthropic pre-built skills
|
||||
(`'pptx'`, `'xlsx'`, `'docx'`, `'pdf'`)
|
||||
- `SkillConfig` objects for custom skills or fine-grained control
|
||||
- `LocalSkillConfig` objects for skills loaded from local files
|
||||
unsupported_model_behavior: Behavior when a non-Anthropic model is used.
|
||||
- `'ignore'`: Skip skills injection silently
|
||||
- `'warn'`: Log a warning and skip skills injection
|
||||
- `'raise'`: Raise an error
|
||||
code_execution_tool_name: Name of the code execution tool. If not present
|
||||
in the request, it will be automatically added.
|
||||
|
||||
Raises:
|
||||
ValueError: If more than 8 skills are provided (API limit) or if
|
||||
invalid skill configuration is detected.
|
||||
"""
|
||||
if len(skills) > 8:
|
||||
msg = (
|
||||
"Anthropic API supports a maximum of 8 skills per request, "
|
||||
f"but {len(skills)} were provided."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
self.skills = self._normalize_skills(skills)
|
||||
self.unsupported_model_behavior = unsupported_model_behavior
|
||||
self.code_execution_tool_name = code_execution_tool_name
|
||||
|
||||
def _normalize_skills(
|
||||
self, skills: list[str | SkillConfig | LocalSkillConfig]
|
||||
) -> list[SkillConfig | LocalSkillConfig]:
|
||||
"""Normalize skill configurations.
|
||||
|
||||
Args:
|
||||
skills: List of skill IDs, SkillConfig objects, or LocalSkillConfig objects.
|
||||
|
||||
Returns:
|
||||
List of SkillConfig or LocalSkillConfig objects.
|
||||
"""
|
||||
normalized = []
|
||||
for skill in skills:
|
||||
if isinstance(skill, str):
|
||||
# Infer type based on skill_id
|
||||
skill_type: Literal["anthropic", "custom"] = (
|
||||
"anthropic" if skill in ANTHROPIC_SKILLS else "custom"
|
||||
)
|
||||
normalized.append(
|
||||
SkillConfig(skill_id=skill, type=skill_type, version="latest")
|
||||
)
|
||||
else:
|
||||
normalized.append(skill)
|
||||
return normalized
|
||||
|
||||
def _should_apply_skills(self, request: ModelRequest) -> bool:
|
||||
"""Check if skills should be applied to the request.
|
||||
|
||||
Args:
|
||||
request: The model request to check.
|
||||
|
||||
Returns:
|
||||
`True` if skills should be applied, `False` otherwise.
|
||||
|
||||
Raises:
|
||||
ValueError: If model is unsupported and behavior is set to `'raise'`.
|
||||
"""
|
||||
if not isinstance(request.model, ChatAnthropic):
|
||||
msg = (
|
||||
"ClaudeSkillsMiddleware only supports Anthropic models, "
|
||||
f"not instances of {type(request.model)}"
|
||||
)
|
||||
if self.unsupported_model_behavior == "raise":
|
||||
raise ValueError(msg)
|
||||
if self.unsupported_model_behavior == "warn":
|
||||
warn(msg, stacklevel=3)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _ensure_code_execution_tool(self, request: ModelRequest) -> None:
|
||||
"""Ensure code execution tool is present in the request.
|
||||
|
||||
Skills require the code execution tool to be enabled. If not present,
|
||||
this method will automatically add it.
|
||||
|
||||
Args:
|
||||
request: The model request to check and modify.
|
||||
"""
|
||||
tools = request.tools or []
|
||||
|
||||
# Check if code execution tool exists
|
||||
has_code_execution = any(
|
||||
(
|
||||
isinstance(tool, dict)
|
||||
and tool.get("name") == self.code_execution_tool_name
|
||||
)
|
||||
or (isinstance(tool, str) and tool == self.code_execution_tool_name)
|
||||
or (hasattr(tool, "name") and tool.name == self.code_execution_tool_name)
|
||||
for tool in tools
|
||||
)
|
||||
|
||||
if not has_code_execution:
|
||||
# Automatically add code execution tool
|
||||
tools = list(tools)
|
||||
tools.append(
|
||||
{
|
||||
"type": "code_execution_20250825",
|
||||
"name": self.code_execution_tool_name,
|
||||
}
|
||||
)
|
||||
request.tools = tools
|
||||
|
||||
def _upload_local_skills(self, request: ModelRequest) -> None:
|
||||
"""Upload any local skills that need uploading.
|
||||
|
||||
Args:
|
||||
request: The model request to get Anthropic client from.
|
||||
|
||||
Raises:
|
||||
ValueError: If upload fails or if model doesn't have a client.
|
||||
"""
|
||||
# Check if there are any local skills that need uploading
|
||||
has_local_skills = any(
|
||||
isinstance(skill, LocalSkillConfig)
|
||||
and skill.auto_upload
|
||||
and not skill.is_uploaded
|
||||
for skill in self.skills
|
||||
)
|
||||
|
||||
if not has_local_skills:
|
||||
return
|
||||
|
||||
# Get Anthropic client from the model
|
||||
if not isinstance(request.model, ChatAnthropic):
|
||||
return
|
||||
|
||||
# Get the underlying client
|
||||
if not hasattr(request.model, "_client"):
|
||||
msg = "ChatAnthropic model does not have a _client attribute"
|
||||
raise ValueError(msg)
|
||||
|
||||
client = request.model._client # noqa: SLF001
|
||||
|
||||
# Upload any local skills that need it
|
||||
for skill in self.skills:
|
||||
if (
|
||||
isinstance(skill, LocalSkillConfig)
|
||||
and skill.auto_upload
|
||||
and not skill.is_uploaded
|
||||
):
|
||||
skill.upload_skill(client)
|
||||
|
||||
def _inject_skills_config(self, request: ModelRequest) -> None:
|
||||
"""Inject skills configuration into the request.
|
||||
|
||||
Args:
|
||||
request: The model request to modify.
|
||||
"""
|
||||
# Add required beta headers
|
||||
model_settings = request.model_settings or {}
|
||||
existing_betas = model_settings.get("betas", [])
|
||||
|
||||
# Merge with required betas, avoiding duplicates
|
||||
all_betas = list(set(existing_betas + REQUIRED_BETAS))
|
||||
model_settings["betas"] = all_betas
|
||||
|
||||
# Create or update container configuration
|
||||
container = model_settings.get("container", {})
|
||||
container["skills"] = [skill.to_dict() for skill in self.skills]
|
||||
model_settings["container"] = container
|
||||
|
||||
# Update request
|
||||
request.model_settings = model_settings
|
||||
|
||||
def wrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], ModelResponse],
|
||||
) -> ModelCallResult:
|
||||
"""Modify the model request to add skills configuration.
|
||||
|
||||
Args:
|
||||
request: The model request to potentially modify.
|
||||
handler: The handler to execute the model request.
|
||||
|
||||
Returns:
|
||||
The model response from the handler.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model is unsupported and behavior is set to `'raise'`.
|
||||
"""
|
||||
if not self._should_apply_skills(request):
|
||||
return handler(request)
|
||||
|
||||
self._ensure_code_execution_tool(request)
|
||||
self._upload_local_skills(request)
|
||||
self._inject_skills_config(request)
|
||||
return handler(request)
|
||||
|
||||
async def awrap_model_call(
|
||||
self,
|
||||
request: ModelRequest,
|
||||
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
||||
) -> ModelCallResult:
|
||||
"""Modify the model request to add skills configuration (async version).
|
||||
|
||||
Args:
|
||||
request: The model request to potentially modify.
|
||||
handler: The async handler to execute the model request.
|
||||
|
||||
Returns:
|
||||
The model response from the handler.
|
||||
|
||||
Raises:
|
||||
ValueError: If the model is unsupported and behavior is set to `'raise'`.
|
||||
"""
|
||||
if not self._should_apply_skills(request):
|
||||
return await handler(request)
|
||||
|
||||
self._ensure_code_execution_tool(request)
|
||||
self._upload_local_skills(request)
|
||||
self._inject_skills_config(request)
|
||||
return await handler(request)
|
||||
@@ -0,0 +1,744 @@
|
||||
"""Tests for Anthropic Skills middleware."""
|
||||
|
||||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForLLMRun,
|
||||
CallbackManagerForLLMRun,
|
||||
)
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
|
||||
from langchain_core.outputs import ChatGeneration, ChatResult
|
||||
from langgraph.runtime import Runtime
|
||||
|
||||
from langchain_anthropic.chat_models import ChatAnthropic
|
||||
from langchain_anthropic.middleware import (
|
||||
ClaudeSkillsMiddleware,
|
||||
LocalSkillConfig,
|
||||
SkillConfig,
|
||||
)
|
||||
|
||||
|
||||
class FakeToolCallingModel(BaseChatModel):
|
||||
"""Fake model for testing middleware."""
|
||||
|
||||
def _generate(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: CallbackManagerForLLMRun | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResult:
|
||||
"""Top Level call"""
|
||||
messages_string = "-".join([str(m.content) for m in messages])
|
||||
message = AIMessage(content=messages_string, id="0")
|
||||
return ChatResult(generations=[ChatGeneration(message=message)])
|
||||
|
||||
async def _agenerate(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
stop: list[str] | None = None,
|
||||
run_manager: AsyncCallbackManagerForLLMRun | None = None,
|
||||
**kwargs: Any,
|
||||
) -> ChatResult:
|
||||
"""Async top level call"""
|
||||
messages_string = "-".join([str(m.content) for m in messages])
|
||||
message = AIMessage(content=messages_string, id="0")
|
||||
return ChatResult(generations=[ChatGeneration(message=message)])
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
return "fake-tool-call-model"
|
||||
|
||||
|
||||
def test_skill_config_initialization() -> None:
|
||||
"""Test SkillConfig initialization and to_dict method."""
|
||||
# Test with default values (Anthropic skill)
|
||||
skill = SkillConfig(skill_id="pptx")
|
||||
assert skill.skill_id == "pptx"
|
||||
assert skill.type == "anthropic"
|
||||
assert skill.version == "latest"
|
||||
assert skill.to_dict() == {
|
||||
"type": "anthropic",
|
||||
"skill_id": "pptx",
|
||||
"version": "latest",
|
||||
}
|
||||
|
||||
# Test with custom skill and specific version
|
||||
skill = SkillConfig(
|
||||
skill_id="custom-123",
|
||||
type="custom",
|
||||
version="1234567890",
|
||||
)
|
||||
assert skill.skill_id == "custom-123"
|
||||
assert skill.type == "custom"
|
||||
assert skill.version == "1234567890"
|
||||
assert skill.to_dict() == {
|
||||
"type": "custom",
|
||||
"skill_id": "custom-123",
|
||||
"version": "1234567890",
|
||||
}
|
||||
|
||||
|
||||
def test_claude_skills_middleware_initialization() -> None:
|
||||
"""Test ClaudeSkillsMiddleware initialization."""
|
||||
# Test with string skill IDs (Anthropic pre-built skills)
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx", "xlsx"])
|
||||
assert len(middleware.skills) == 2
|
||||
assert middleware.skills[0].skill_id == "pptx"
|
||||
assert middleware.skills[0].type == "anthropic"
|
||||
assert middleware.skills[1].skill_id == "xlsx"
|
||||
assert middleware.skills[1].type == "anthropic"
|
||||
|
||||
# Test with SkillConfig objects
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=[
|
||||
SkillConfig(skill_id="pptx", type="anthropic", version="20251002"),
|
||||
SkillConfig(skill_id="custom-123", type="custom", version="latest"),
|
||||
]
|
||||
)
|
||||
assert len(middleware.skills) == 2
|
||||
assert middleware.skills[0].version == "20251002"
|
||||
assert middleware.skills[1].type == "custom"
|
||||
|
||||
# Test with mixed skills
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=[
|
||||
"pptx",
|
||||
SkillConfig(skill_id="custom-456", type="custom"),
|
||||
]
|
||||
)
|
||||
assert len(middleware.skills) == 2
|
||||
assert middleware.skills[0].skill_id == "pptx"
|
||||
assert middleware.skills[1].skill_id == "custom-456"
|
||||
|
||||
|
||||
def test_claude_skills_middleware_max_skills_validation() -> None:
|
||||
"""Test that ClaudeSkillsMiddleware validates maximum skill count."""
|
||||
# Should raise error when more than 8 skills are provided
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Anthropic API supports a maximum of 8 skills per request",
|
||||
):
|
||||
ClaudeSkillsMiddleware(skills=["pptx"] * 9)
|
||||
|
||||
|
||||
def test_claude_skills_middleware_code_execution_auto_added() -> None:
|
||||
"""Test that code execution tool is automatically added if missing."""
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx"])
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
# Request without code execution tool
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Create a presentation")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[], # No code execution tool initially
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Create a presentation")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
# Should automatically add code_execution tool
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
# Verify code_execution was added to tools
|
||||
assert len(fake_request.tools) == 1
|
||||
tool = fake_request.tools[0]
|
||||
assert isinstance(tool, dict)
|
||||
assert tool["name"] == "code_execution"
|
||||
assert tool["type"] == "code_execution_20250825"
|
||||
|
||||
|
||||
def test_claude_skills_middleware_basic_functionality() -> None:
|
||||
"""Test ClaudeSkillsMiddleware basic functionality."""
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx", "xlsx"])
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Create a presentation")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Create a presentation")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
# Check that model_settings were properly configured
|
||||
assert "betas" in fake_request.model_settings
|
||||
assert "code-execution-2025-08-25" in fake_request.model_settings["betas"]
|
||||
assert "skills-2025-10-02" in fake_request.model_settings["betas"]
|
||||
assert "files-api-2025-04-14" in fake_request.model_settings["betas"]
|
||||
|
||||
# Check that container with skills was added
|
||||
assert "container" in fake_request.model_settings
|
||||
assert "skills" in fake_request.model_settings["container"]
|
||||
skills = fake_request.model_settings["container"]["skills"]
|
||||
assert len(skills) == 2
|
||||
assert skills[0]["skill_id"] == "pptx"
|
||||
assert skills[1]["skill_id"] == "xlsx"
|
||||
|
||||
|
||||
def test_claude_skills_middleware_preserves_existing_betas() -> None:
|
||||
"""Test that middleware preserves existing beta headers."""
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx"])
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Create a presentation")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Create a presentation")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={"betas": ["existing-beta-header"]},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
middleware.wrap_model_call(fake_request, mock_handler)
|
||||
|
||||
# Check that existing betas are preserved
|
||||
betas = fake_request.model_settings["betas"]
|
||||
assert "existing-beta-header" in betas
|
||||
assert "code-execution-2025-08-25" in betas
|
||||
assert "skills-2025-10-02" in betas
|
||||
assert "files-api-2025-04-14" in betas
|
||||
|
||||
|
||||
def test_claude_skills_middleware_unsupported_model() -> None:
|
||||
"""Test ClaudeSkillsMiddleware with unsupported model."""
|
||||
fake_request = ModelRequest(
|
||||
model=FakeToolCallingModel(),
|
||||
messages=[HumanMessage("Hello")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Hello")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], unsupported_model_behavior="raise"
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
# Test raise behavior
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="ClaudeSkillsMiddleware only supports Anthropic models",
|
||||
):
|
||||
middleware.wrap_model_call(fake_request, mock_handler)
|
||||
|
||||
# Test warn behavior
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], unsupported_model_behavior="warn"
|
||||
)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
assert len(w) == 1
|
||||
assert "ClaudeSkillsMiddleware only supports Anthropic models" in str(
|
||||
w[-1].message
|
||||
)
|
||||
|
||||
# Test ignore behavior
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], unsupported_model_behavior="ignore"
|
||||
)
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
|
||||
def test_claude_skills_middleware_custom_tool_name() -> None:
|
||||
"""Test middleware with custom code execution tool name."""
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], code_execution_tool_name="custom_code_exec"
|
||||
)
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
# Request with custom tool name
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Create a presentation")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "custom_exec", "name": "custom_code_exec"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Create a presentation")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
# Should work with custom tool name
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
|
||||
async def test_claude_skills_middleware_async() -> None:
|
||||
"""Test ClaudeSkillsMiddleware async path."""
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx", "docx"])
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Create a document")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Create a document")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
async def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
result = await middleware.awrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
# Verify skills were configured
|
||||
assert "container" in fake_request.model_settings
|
||||
skills = fake_request.model_settings["container"]["skills"]
|
||||
assert len(skills) == 2
|
||||
assert skills[0]["skill_id"] == "pptx"
|
||||
assert skills[1]["skill_id"] == "docx"
|
||||
|
||||
|
||||
async def test_claude_skills_middleware_async_unsupported_model() -> None:
|
||||
"""Test ClaudeSkillsMiddleware async path with unsupported model."""
|
||||
fake_request = ModelRequest(
|
||||
model=FakeToolCallingModel(),
|
||||
messages=[HumanMessage("Hello")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Hello")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], unsupported_model_behavior="raise"
|
||||
)
|
||||
|
||||
async def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
# Test raise behavior
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="ClaudeSkillsMiddleware only supports Anthropic models",
|
||||
):
|
||||
await middleware.awrap_model_call(fake_request, mock_handler)
|
||||
|
||||
# Test warn behavior
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], unsupported_model_behavior="warn"
|
||||
)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
result = await middleware.awrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
assert len(w) == 1
|
||||
assert "ClaudeSkillsMiddleware only supports Anthropic models" in str(
|
||||
w[-1].message
|
||||
)
|
||||
|
||||
# Test ignore behavior
|
||||
middleware = ClaudeSkillsMiddleware(
|
||||
skills=["pptx"], unsupported_model_behavior="ignore"
|
||||
)
|
||||
result = await middleware.awrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
|
||||
async def test_claude_skills_middleware_async_code_execution_auto_added() -> None:
|
||||
"""Test that async path automatically adds code execution tool."""
|
||||
middleware = ClaudeSkillsMiddleware(skills=["xlsx"])
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
# Request without code execution tool
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Process spreadsheet")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[], # No code execution tool initially
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Process spreadsheet")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
async def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
# Should automatically add code_execution tool
|
||||
result = await middleware.awrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
# Verify code_execution was added to tools
|
||||
assert len(fake_request.tools) == 1
|
||||
tool = fake_request.tools[0]
|
||||
assert isinstance(tool, dict)
|
||||
assert tool["name"] == "code_execution"
|
||||
assert tool["type"] == "code_execution_20250825"
|
||||
|
||||
|
||||
def test_claude_skills_middleware_all_pre_built_skills() -> None:
|
||||
"""Test middleware with all pre-built Anthropic skills."""
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx", "xlsx", "docx", "pdf"])
|
||||
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Process documents")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Process documents")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
# Verify all skills are configured
|
||||
skills = fake_request.model_settings["container"]["skills"]
|
||||
assert len(skills) == 4
|
||||
skill_ids = {skill["skill_id"] for skill in skills}
|
||||
assert skill_ids == {"pptx", "xlsx", "docx", "pdf"}
|
||||
# All should be anthropic type with latest version by default
|
||||
for skill in skills:
|
||||
assert skill["type"] == "anthropic"
|
||||
assert skill["version"] == "latest"
|
||||
|
||||
|
||||
def test_local_skill_config_initialization() -> None:
|
||||
"""Test LocalSkillConfig initialization and validation."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
|
||||
# Create a valid SKILL.md file
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text(
|
||||
"""---
|
||||
name: My Custom Skill
|
||||
description: A test skill for unit testing
|
||||
---
|
||||
|
||||
# My Custom Skill
|
||||
|
||||
This is a test skill."""
|
||||
)
|
||||
|
||||
# Test successful initialization
|
||||
skill = LocalSkillConfig(path=skill_dir, display_title="Test Skill")
|
||||
assert skill.path == skill_dir
|
||||
assert skill.display_title == "Test Skill"
|
||||
assert skill.type == "custom"
|
||||
assert skill.version == "latest"
|
||||
assert skill.auto_upload is True
|
||||
assert not skill.is_uploaded
|
||||
|
||||
# Test with auto_upload=False
|
||||
skill = LocalSkillConfig(path=skill_dir, auto_upload=False)
|
||||
assert skill.auto_upload is False
|
||||
|
||||
|
||||
def test_local_skill_config_missing_path() -> None:
|
||||
"""Test LocalSkillConfig with non-existent path."""
|
||||
with pytest.raises(FileNotFoundError, match="Skill path does not exist"):
|
||||
LocalSkillConfig(path="/nonexistent/path")
|
||||
|
||||
|
||||
def test_local_skill_config_invalid_path_type() -> None:
|
||||
"""Test LocalSkillConfig with invalid path type."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a regular file instead of a directory
|
||||
invalid_path = Path(temp_dir) / "invalid.txt"
|
||||
invalid_path.write_text("not a skill")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"Skill path must be a directory or \.zip file",
|
||||
):
|
||||
LocalSkillConfig(path=invalid_path)
|
||||
|
||||
|
||||
def test_local_skill_config_missing_skill_md() -> None:
|
||||
"""Test LocalSkillConfig with directory missing SKILL.md."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "incomplete-skill"
|
||||
skill_dir.mkdir()
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"Skill directory must contain a SKILL\.md file",
|
||||
):
|
||||
LocalSkillConfig(path=skill_dir)
|
||||
|
||||
|
||||
def test_local_skill_config_zip_file() -> None:
|
||||
"""Test LocalSkillConfig with zip file."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a zip file
|
||||
import zipfile
|
||||
|
||||
zip_path = Path(temp_dir) / "skill.zip"
|
||||
with zipfile.ZipFile(zip_path, "w") as zf:
|
||||
zf.writestr(
|
||||
"SKILL.md",
|
||||
"""---
|
||||
name: Zipped Skill
|
||||
description: A skill from a zip file
|
||||
---
|
||||
|
||||
# Zipped Skill""",
|
||||
)
|
||||
|
||||
# Should initialize successfully
|
||||
skill = LocalSkillConfig(path=zip_path)
|
||||
assert skill.path == zip_path
|
||||
assert skill.type == "custom"
|
||||
|
||||
|
||||
def test_local_skill_config_content_hash() -> None:
|
||||
"""Test that content hash is computed correctly."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text(
|
||||
"""---
|
||||
name: Hash Test
|
||||
description: Testing hash computation
|
||||
---
|
||||
|
||||
# Hash Test"""
|
||||
)
|
||||
|
||||
skill = LocalSkillConfig(path=skill_dir)
|
||||
hash1 = skill._compute_content_hash()
|
||||
|
||||
# Hash should be consistent
|
||||
hash2 = skill._compute_content_hash()
|
||||
assert hash1 == hash2
|
||||
|
||||
# Modify content
|
||||
skill_md.write_text(
|
||||
"""---
|
||||
name: Hash Test Modified
|
||||
description: Modified content
|
||||
---
|
||||
|
||||
# Modified"""
|
||||
)
|
||||
|
||||
# Hash should change
|
||||
hash3 = skill._compute_content_hash()
|
||||
assert hash1 != hash3
|
||||
|
||||
|
||||
def test_local_skill_config_create_zip() -> None:
|
||||
"""Test creating zip from skill directory."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
|
||||
# Create multiple files
|
||||
(skill_dir / "SKILL.md").write_text("# Test Skill")
|
||||
(skill_dir / "helper.py").write_text("def helper(): pass")
|
||||
|
||||
# Create subdirectory with file
|
||||
subdir = skill_dir / "scripts"
|
||||
subdir.mkdir()
|
||||
(subdir / "script.py").write_text("print('hello')")
|
||||
|
||||
skill = LocalSkillConfig(path=skill_dir)
|
||||
zip_bytes = skill._create_zip_bytes()
|
||||
|
||||
# Verify it's valid zip
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
|
||||
with zipfile.ZipFile(BytesIO(zip_bytes), "r") as zf:
|
||||
names = zf.namelist()
|
||||
# Zip should contain top-level folder with all files inside
|
||||
assert "my-skill/SKILL.md" in names
|
||||
assert "my-skill/helper.py" in names
|
||||
assert "my-skill/scripts/script.py" in names
|
||||
|
||||
|
||||
def test_local_skill_config_upload() -> None:
|
||||
"""Test uploading a local skill."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text("# Test")
|
||||
|
||||
skill = LocalSkillConfig(path=skill_dir)
|
||||
|
||||
# Mock the Anthropic client
|
||||
mock_client = MagicMock()
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = "skill-123"
|
||||
mock_client.beta.skills.create.return_value = mock_response
|
||||
|
||||
# Upload
|
||||
skill_id = skill.upload_skill(mock_client)
|
||||
|
||||
assert skill_id == "skill-123"
|
||||
assert skill.skill_id == "skill-123"
|
||||
assert skill.is_uploaded
|
||||
|
||||
# Verify upload was called
|
||||
mock_client.beta.skills.create.assert_called_once()
|
||||
|
||||
# Upload again with same content - should not call API again
|
||||
skill_id2 = skill.upload_skill(mock_client)
|
||||
assert skill_id2 == "skill-123"
|
||||
assert mock_client.beta.skills.create.call_count == 1 # Still just 1 call
|
||||
|
||||
|
||||
def test_claude_skills_middleware_with_local_skill() -> None:
|
||||
"""Test ClaudeSkillsMiddleware with LocalSkillConfig."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "my-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text("# Test Skill")
|
||||
|
||||
# Create middleware with local skill (auto_upload=False)
|
||||
local_skill = LocalSkillConfig(path=skill_dir, auto_upload=False)
|
||||
middleware = ClaudeSkillsMiddleware(skills=["pptx", local_skill])
|
||||
|
||||
# Mock chat model with _client
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
mock_client = MagicMock()
|
||||
mock_chat_anthropic._client = mock_client
|
||||
|
||||
# Manually upload the skill first
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = "local-skill-456"
|
||||
mock_client.beta.skills.create.return_value = mock_response
|
||||
local_skill.upload_skill(mock_client)
|
||||
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Use skills")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Use skills")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
|
||||
# Verify both skills are configured
|
||||
skills = fake_request.model_settings["container"]["skills"]
|
||||
assert len(skills) == 2
|
||||
skill_ids = {skill["skill_id"] for skill in skills}
|
||||
assert "pptx" in skill_ids
|
||||
assert "local-skill-456" in skill_ids
|
||||
|
||||
|
||||
def test_claude_skills_middleware_auto_upload() -> None:
|
||||
"""Test that middleware auto-uploads local skills when auto_upload=True."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
skill_dir = Path(temp_dir) / "auto-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "SKILL.md").write_text("# Auto Upload Test")
|
||||
|
||||
# Create middleware with auto_upload=True (default)
|
||||
local_skill = LocalSkillConfig(path=skill_dir)
|
||||
middleware = ClaudeSkillsMiddleware(skills=[local_skill])
|
||||
|
||||
# Mock chat model with _client
|
||||
mock_chat_anthropic = MagicMock(spec=ChatAnthropic)
|
||||
mock_client = MagicMock()
|
||||
mock_chat_anthropic._client = mock_client
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.id = "auto-uploaded-789"
|
||||
mock_client.beta.skills.create.return_value = mock_response
|
||||
|
||||
fake_request = ModelRequest(
|
||||
model=mock_chat_anthropic,
|
||||
messages=[HumanMessage("Test")],
|
||||
system_prompt=None,
|
||||
tool_choice=None,
|
||||
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
|
||||
response_format=None,
|
||||
state={"messages": [HumanMessage("Test")]},
|
||||
runtime=cast(Runtime, object()),
|
||||
model_settings={},
|
||||
)
|
||||
|
||||
def mock_handler(req: ModelRequest) -> ModelResponse:
|
||||
return ModelResponse(result=[AIMessage(content="mock response")])
|
||||
|
||||
# Should auto-upload during wrap_model_call
|
||||
assert not local_skill.is_uploaded
|
||||
result = middleware.wrap_model_call(fake_request, mock_handler)
|
||||
assert isinstance(result, ModelResponse)
|
||||
assert local_skill.is_uploaded
|
||||
assert local_skill.skill_id == "auto-uploaded-789"
|
||||
|
||||
# Verify upload was called
|
||||
mock_client.beta.skills.create.assert_called_once()
|
||||
203
libs/partners/anthropic/uv.lock
generated
203
libs/partners/anthropic/uv.lock
generated
@@ -10,9 +10,6 @@ resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_python_implementation == 'PyPy'",
|
||||
]
|
||||
|
||||
[options]
|
||||
prerelease-mode = "allow"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
@@ -247,11 +244,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "defusedxml"
|
||||
version = "0.8.0rc2"
|
||||
version = "0.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/3b/b8849dcc3f96913924137dc4ea041d74aa513a3c5dda83d8366491290c74/defusedxml-0.8.0rc2.tar.gz", hash = "sha256:138c7d540a78775182206c7c97fe65b246a2f40b29471e1a2f1b0da76e7a3942", size = 52575, upload-time = "2023-09-29T08:01:27.517Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/c7/6b4ad89ca6f7732ff97ce5e9caa6fe739600d26c5d53c20d0bf9abb79ec5/defusedxml-0.8.0rc2-py2.py3-none-any.whl", hash = "sha256:1c812964311154c3bf4aaf3bc1443b31ee13530b7f255eaaa062c0553c76103d", size = 25756, upload-time = "2023-09-29T08:01:25.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -456,7 +453,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain"
|
||||
version = "1.0.0rc1"
|
||||
version = "1.0.0rc2"
|
||||
source = { editable = "../../langchain_v1" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -600,7 +597,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.0.0rc2"
|
||||
version = "1.0.0rc3"
|
||||
source = { editable = "../../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -703,7 +700,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph"
|
||||
version = "1.0.0a4"
|
||||
version = "1.0.0rc1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
@@ -713,9 +710,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "xxhash" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/9e/fcf7d2dd275654a077c224c38e33674b1e5753146b7c7a228f3074767c3c/langgraph-1.0.0a4.tar.gz", hash = "sha256:7263648049967d4e2c15c6c94532c4398a05ef785ad09bd50cb2821f137ad0b9", size = 465213, upload-time = "2025-09-29T12:13:43.729Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/6b/27863bc2197fe2cae5ae3241a79e1868b98f8dfa03991eea4f607dba177a/langgraph-1.0.0rc1.tar.gz", hash = "sha256:0acc0eddbed6b353334a93de6943bb49820054cf14e1ca7dab0a91ac7add1ce2", size = 466052, upload-time = "2025-10-17T00:56:12.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/f1/6d19322b63840290d95defaf9787014455e8aa8394dcb67bcdd57a6f72fa/langgraph-1.0.0a4-py3-none-any.whl", hash = "sha256:91d26fd0f8045c3008673858c79d8aae7821cdef269443946d192719416c0e49", size = 154928, upload-time = "2025-09-29T12:13:42.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/5b/7d4aaf30f8c08d14ec16a49e89530c05ab9ccd7c00a54da6bd8adabefd26/langgraph-1.0.0rc1-py3-none-any.whl", hash = "sha256:9d84da21ae8bcc5b05dfa2e63396eb642d39c54d670406b7319810ede0c5ab26", size = 155229, upload-time = "2025-10-17T00:56:10.956Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -733,15 +730,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-prebuilt"
|
||||
version = "0.7.0a2"
|
||||
version = "0.7.0rc1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "langgraph-checkpoint" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/a2/8c82bad7400328a10953e52355933a9e79778fbb7bc3389be6240be541af/langgraph_prebuilt-0.7.0a2.tar.gz", hash = "sha256:ecf154a68be5eb3316544c2df47a19e4cc0e2ce1e2bbd971ba28533695fa9ddc", size = 113658, upload-time = "2025-09-02T17:07:02.547Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/d3/6474eecd1cc95cead25fe0f1717f0b76e1f6edbc8631fc773f6bf7ac03ad/langgraph_prebuilt-0.7.0rc1.tar.gz", hash = "sha256:23f2c1c0a3f0c643a45f90d99ac951a5e1d1be3e711ae10d91b03e34c05b306b", size = 114860, upload-time = "2025-10-17T00:51:56.719Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b9/e59ecfa7cac69fdcfa1274a7a575de64ba0351da30cf35be9dcb7f3b33c7/langgraph_prebuilt-0.7.0a2-py3-none-any.whl", hash = "sha256:757b93a3e44802ba18623bdca46384fae109736758496a83b043ce4b5074bc47", size = 28398, upload-time = "2025-09-02T17:07:01.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/70/6a46ebd63304028617b1de1377a159dedd95f0ce2e68a6d8b50c3378448c/langgraph_prebuilt-0.7.0rc1-py3-none-any.whl", hash = "sha256:7a2032683f1cab23d19f11f89805bc84b668502c491b6f041afb6932c8870e70", size = 28387, upload-time = "2025-10-17T00:51:55.75Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1589,14 +1586,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.24.0a0"
|
||||
version = "0.23.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0a/b7/cf54bb6825bb654ca6a07dc3efee715cc1fd0d3cbd00a31a095b09cc86d3/pytest_asyncio-0.24.0a0.tar.gz", hash = "sha256:4c599f8e3657cdd8b15ad050eba551fb740b7e99c1a7dcc4a23b2141b10aee41", size = 49299, upload-time = "2024-07-30T13:44:17.677Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/b4/0b378b7bf26a8ae161c3890c0b48a91a04106c5713ce81b4b080ea2f4f18/pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3", size = 46920, upload-time = "2024-07-17T17:39:34.617Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/5b/9310aecc65cb4eef33793ee36e24fb74f758c568a7cd726a28be7352913e/pytest_asyncio-0.24.0a0-py3-none-any.whl", hash = "sha256:f6e24c9779fab29235463b2437eb121dde748e39e2f194489517d8a780d26a02", size = 18294, upload-time = "2024-07-30T13:44:16.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", size = 17663, upload-time = "2024-07-17T17:39:32.478Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1614,24 +1611,24 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-codspeed"
|
||||
version = "4.2.0b0"
|
||||
version = "4.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "pytest" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/d3/6f64533d723ad43d7e398c847d6efa763607e4340418ca823537516c1dd5/pytest_codspeed-4.2.0b0.tar.gz", hash = "sha256:4c7839eba10d6cba6b0ba78c1b87c73032fbd7fb1aec1f5b586a752076d4e32f", size = 113177, upload-time = "2025-10-07T15:47:15.114Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/ae/5d89a787151868d3ebd813b24a13fe4ac7e60148cf1b3454d351c8b99f69/pytest_codspeed-4.1.1.tar.gz", hash = "sha256:9acc3394cc8aafd4543193254831d87de6be79accfdbd43475919fdaa2fc8d81", size = 113149, upload-time = "2025-10-07T16:30:02.709Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/80/37b05042e6fbbbc3149a394208947eff12fecc7400b391bad47beb03e89d/pytest_codspeed-4.2.0b0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46aebf7edd4ae50acd8238735bb0eb1508fd73931141179dc324ca25461245e1", size = 261976, upload-time = "2025-10-07T15:47:00.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/13/739c65bcec74a849714cec404ba316a94677b784fa6a0d1bae38fe9d8241/pytest_codspeed-4.2.0b0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee3ba60a4cfb922cfd3eaf8087877ea8672859cbfbf42d48ecf0f6bea2f55f18", size = 249290, upload-time = "2025-10-07T15:47:02.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/ad/23f4f0767983867fd1d870e4f1de7b7de998fa5970080a95222bc0183402/pytest_codspeed-4.2.0b0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c3a2fd0e48a92f78d162b11b6fb514b2479b687a5be2e77901a753657f682a", size = 262013, upload-time = "2025-10-07T15:47:03.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/65/72c98d2d08534d0a1e153e6ef8d4a48842df0690ce614f849b1d4f2dd1a6/pytest_codspeed-4.2.0b0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9dacfb63b383add866ca15841e0e7f1502765fad0d24d4a0d5ff2be8b0943a", size = 249305, upload-time = "2025-10-07T15:47:05.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2b/0c34918b045d4cec003ebdd88c77561dd3930eeac6365f785358bd4c1f4e/pytest_codspeed-4.2.0b0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31891269353f2847a1c75f8ae656788b0535acf3a4b9479dc097fc99c122441", size = 262235, upload-time = "2025-10-07T15:47:06.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/9c/5c8f8c8b5e6c6b5b17f8c598f97e768e005dc6a553b89846e03838b5aec8/pytest_codspeed-4.2.0b0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb520b7c6808d13bc7d22ee3ab12c8a64bec8be7598efb7f0caa32d7b3569a0", size = 249587, upload-time = "2025-10-07T15:47:07.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/ab/8e61e3e8db98c327aa84841487c4904ef63b3ed51fcd02da6efa878631f4/pytest_codspeed-4.2.0b0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799a242cf48f4728bf48d92f76a08d775160d77cfcbf0818f787bdbfeb22128", size = 262243, upload-time = "2025-10-07T15:47:08.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/29/15152232deb737aacd5edd835564d05134dca1b36966427bc0a617773b15/pytest_codspeed-4.2.0b0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21897ab2f25c26b3c8ebd7b8e1a69c473e8ab4d4dabeb354b46a4e9e86c11b97", size = 249591, upload-time = "2025-10-07T15:47:10.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/d6/92d43712617761cba42393d0232ee9a59d2450f9f37ba0362f59cd902278/pytest_codspeed-4.2.0b0-py3-none-any.whl", hash = "sha256:3d298c135f4fa7ef61df1cc392a065acf09e7ff0d843705fe58f947a50a87c13", size = 113638, upload-time = "2025-10-07T15:47:13.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/b4/6e76539ef17e398c06464c6d92da1b8978b46180df5446cdb76e2293c254/pytest_codspeed-4.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa83a1a0aaeb6bdb9918a18294708eebe765a3b5a855adccf9213629d2a0d302", size = 261944, upload-time = "2025-10-07T16:29:50.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/6a/ba8aca4a07bb8f19fdeba752c7276b35cb63ff743695b137c2f68a53cf21/pytest_codspeed-4.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6fe213b2589ffe6f2189b3b21ca14717c9346b226e6028d2e2b4d4d7dac750f", size = 249258, upload-time = "2025-10-07T16:29:52.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/13/be5ab2167d7fb477ba86061dfb6dee3b6feb698ef7d35e8912ba0cce94d5/pytest_codspeed-4.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b3bd5a71bfab4478e9a9b5058237cf2b34938570b43495093c2ea213175bd5", size = 261978, upload-time = "2025-10-07T16:29:53.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/7f/60445f5e9bdaff4b9da1d7e4964e34d87bc160867aec2d99c1119e38b3f8/pytest_codspeed-4.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfc1efdbcc92fb4b4cbc8eaa8d7387664b063c17e025985ece4816100f1fff29", size = 249269, upload-time = "2025-10-07T16:29:54.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/bf/b1d78c982d575636b38688f284bfd6836f5f232d95cef661a6345a91cc7b/pytest_codspeed-4.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:506d446d2911e5188aca7be702c2850a9b8680a72ed241a633d7edaeef00ac13", size = 262202, upload-time = "2025-10-07T16:29:55.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/23/9ace1fcd72a84d845f522e06badada7d85f6a5a4d4aed54509b51af87068/pytest_codspeed-4.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1773c74394c98317c6846e9eb60c352222c031bdf1ded109f5c35772a3ce6dc2", size = 249554, upload-time = "2025-10-07T16:29:56.681Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/45/09fa57357d46e46de968554cd0a20e4a8b2a48971fec9564c215c67ebcde/pytest_codspeed-4.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f8af528f7f950cb745971fc1e9f59ebc52cc4c51a7eac7a931577fd55d21b94", size = 262209, upload-time = "2025-10-07T16:29:57.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/e5/ac697d8e249be059d4bec9ebcb22a5626e0d18fd6944665b6d0edca3a508/pytest_codspeed-4.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db8b2b71cabde1a7ae77a29a3ce67bcb852c28d5599b4eb7428fdb26cd067815", size = 249559, upload-time = "2025-10-07T16:29:58.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/b0/6f0502ca6497e4c53f2f425b57854b759f29d186804a64d090d9ac2878ca/pytest_codspeed-4.1.1-py3-none-any.whl", hash = "sha256:a0a7aa318b09d87541f4f65db9cd473b53d4f1589598d883b238fe208ae2ac8b", size = 113601, upload-time = "2025-10-07T16:30:01.58Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2051,95 +2048,71 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "wrapt"
|
||||
version = "2.0.0rc5"
|
||||
version = "1.17.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/57/0f1c9dd2471e9d2950788273533ba98dbfdc5e64e65ebf55ea47992d3859/wrapt-2.0.0rc5.tar.gz", hash = "sha256:9622d2d12dacc3f1bd6f015599cc6d406d00cdb8076ca52f6ba3c84bd32a6240", size = 80232, upload-time = "2025-10-16T05:42:39.044Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/b6/9b8946816f393009463c5075f9d2f7dee1424ec93c56ea116e519fca9874/wrapt-2.0.0rc5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:202cdeca52c6432f6429c9a54fc030bfb1ac8df7a5271be2cb521e1ef0e4d80b", size = 77394, upload-time = "2025-10-16T05:40:17.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/4b/dd9464f56eefd367490e741680ab77482adbbfd9caf6985d3bb741b64728/wrapt-2.0.0rc5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7cfb8a36af0a649e3a76ee1a4203964ea078a0552cd84a43a555f98fb7a2976", size = 60661, upload-time = "2025-10-16T05:40:19.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/49/3170249cce2ae5019fdce0e6dbd5ca96a124875b6fc22ee78747812252e2/wrapt-2.0.0rc5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a7f3b6416aebd3ea89f60fc274b6f6e6fa49d809d3a4c420aebb0ab139fe446", size = 61513, upload-time = "2025-10-16T05:40:20.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/b5/70742a2253a210767304f9af8d17b3e4274821406f8b6c8b9c1955afcdd0/wrapt-2.0.0rc5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a186a1529212ebc9c34ee6fa1227a424281f850645c661a3f20567fadbec0583", size = 113296, upload-time = "2025-10-16T05:40:24.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/f1/c2858d9441c127ecd1a1d96e703ec6ab5e0753c203cd262b514663971f02/wrapt-2.0.0rc5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:512308945e9f25254a06c2ed9d2e1db64525c2723d00eaba20b3e0ef2b09760b", size = 115194, upload-time = "2025-10-16T05:40:26.573Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/1d/33ecd8195e75e164e3ce428f6152b6e54e3078617b5647927c711ccf6771/wrapt-2.0.0rc5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27979ed1676fced05548fb798dfe1575d90402ec7c86d981fdd082a284022e23", size = 111738, upload-time = "2025-10-16T05:40:22.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/06/ddccbc8e059f3bafed125ac500a14434c7dbfe8a2bcddb316ce491154274/wrapt-2.0.0rc5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3e57c5c7b2dcabc87e51de23275a664d299b36155456c7c6d232c9f0dbae739e", size = 114360, upload-time = "2025-10-16T05:40:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/92/9d61235b7fda5e133e87bde542d3fc325985badefac12c9217e8483433a9/wrapt-2.0.0rc5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0d30a77881f72bf2c0a68d7b53080090cf2d3e005924f37f1cb678c9a7cdeeda", size = 110969, upload-time = "2025-10-16T05:40:29.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/56/fc0b0ff00136dbfc56b3f0ef972074b059e7aa2cef716857754613a901d9/wrapt-2.0.0rc5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cef66cb8637531fb6e8d5a20d7c6139089581c0bb71cb7e2f1706332d952a81b", size = 112998, upload-time = "2025-10-16T05:40:32.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ae/63195ed2b669c3b6066f80276159843dc29eba103f63913c64f671d8f307/wrapt-2.0.0rc5-cp310-cp310-win32.whl", hash = "sha256:56dc4c09f4eb96fd0421330590d930c0aa2453d3e571be0b0fcc2c11fb86c238", size = 57958, upload-time = "2025-10-16T05:40:35.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/b4/4b55bbe3fcb04dad58191f9d3b3cc542af5c0b788623ea3189eacedd462f/wrapt-2.0.0rc5-cp310-cp310-win_amd64.whl", hash = "sha256:46584e91020119217fb31c77e48c4b5e742a55156d679bbbafaae9120e7d5bc1", size = 60276, upload-time = "2025-10-16T05:40:33.361Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2d/299518dc59228c833a826765d0787f09efef40fb62cb5cfe97f0386047dd/wrapt-2.0.0rc5-cp310-cp310-win_arm64.whl", hash = "sha256:4136c8c3b692f424a6bafd30d88e8f8b9359c85adf8f7ee4ebdd85b4860a65f8", size = 58882, upload-time = "2025-10-16T05:40:34.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/4d/4feccc63fb79804ae63de34284e6876c1998fda8e318eef98bf8c460dba1/wrapt-2.0.0rc5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:970718971cf968b146c01e6e6f9c41236d53675ab85592ec6647ba3882260b35", size = 77393, upload-time = "2025-10-16T05:40:36.644Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/a1/9ff0517fe43255720da6d3b44e4c78752f17c7cacaf7ac0d95673bf93f00/wrapt-2.0.0rc5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6f0fa8c2a0a81c99a12f439e9deaa807e8bd7fe785385069a8f6fc09de73e333", size = 60663, upload-time = "2025-10-16T05:40:37.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/2d/f2d399b8146635e45a71f9c1dc5f40118fc2af036a35166b1b64551c7ff0/wrapt-2.0.0rc5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e31e8e9aadf9db9ea09c4c5a68e31a3bb2d3eeb6f6fec3d94114ea2c56815dc7", size = 61512, upload-time = "2025-10-16T05:40:39.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/af/2e33164b7f30bd2dfbfb865b225271aac61b087949c716ea079f6ad82cad/wrapt-2.0.0rc5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04d62146236f04db2cfb2849044ea7cb7de4a09c72957a021b8d86a38578834f", size = 113709, upload-time = "2025-10-16T05:40:41.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/35/3862beefc28581f57980615de6f294fcd0f7ba275cc132e5e55e7951a44a/wrapt-2.0.0rc5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d286db7933503eea60a40cc851a8be298f039713c3d4c3563451297aaea075d", size = 115630, upload-time = "2025-10-16T05:40:42.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/1b/e566e8ccdeec74dde73d84afb7e9b3fe7f99ba4a98c36d299cd29ee831f6/wrapt-2.0.0rc5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:95f02381a72a937f0c10750ded29461473fac5f0cf348494f60315c07510a934", size = 112177, upload-time = "2025-10-16T05:40:40.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/58/2d2c06fc73c30e9c87ac0c5d7e908449267336e2d20bf3e94f0dea5ac0db/wrapt-2.0.0rc5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42c9eac812abb0f4674df39182ba11c410a1b90d52d08d9407d29eb394883d85", size = 114916, upload-time = "2025-10-16T05:40:44.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/f0/5e2abfe90c54f37284de6d82b6fd899a9de1dd08b7d67ba2b2576d8c3717/wrapt-2.0.0rc5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:116bc9b2de8e2513631134e4b3d34de2adf110f0cac69d3827dc361adf251b1b", size = 111427, upload-time = "2025-10-16T05:40:46.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/7e/aba0d44ad0a9d4af9e9a2dbde7df4945ffda929bf065325f10d684dfee9a/wrapt-2.0.0rc5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c7640251fe9fb23cdc9833b3c2df5c9b6e6115a8102616c9e3fe8fa821b352a1", size = 113509, upload-time = "2025-10-16T05:40:47.787Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/cc/ab66d10fb4035e3cfefa8c3770ceb8590e8d80d0eda49073a3fcf7206904/wrapt-2.0.0rc5-cp311-cp311-win32.whl", hash = "sha256:5ceab372f3bd870a754a86ee2442b8629d580f762337aab5fb033a5e6b10f84c", size = 57952, upload-time = "2025-10-16T05:40:51.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/fa/e1f741c716cb1461a072e2221a456543a9c30ae56d05cbc87133f289f728/wrapt-2.0.0rc5-cp311-cp311-win_amd64.whl", hash = "sha256:bc9792570ec814aeece8e979ef5bcb941503dfdef038d035414ae9196576548f", size = 60275, upload-time = "2025-10-16T05:40:49.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/5f/1544b2edd6576d4741fe79859513fc4c7f1555d50af9bf2efb5c85977a58/wrapt-2.0.0rc5-cp311-cp311-win_arm64.whl", hash = "sha256:1000196191cb81b7e1a01d90ec69905aa1f4755fb803ce1a4633259e1fd768ef", size = 58880, upload-time = "2025-10-16T05:40:50.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/76/0b99e2d2a456b4dc1677dc0875582631601b75ac959abb283d484171ec7f/wrapt-2.0.0rc5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4750b305c80712e1a94c6ddfcdcbde374555628a16f89f7e4e7a944f8cecd37e", size = 78093, upload-time = "2025-10-16T05:40:52.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/73/3ceb99b6e434ded4bb421419aee260f3ce9097bfc9e1f9b76f0db6ea44fc/wrapt-2.0.0rc5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e33249b14933a737589ccdc8b005f30b1c995e4c98cfaa323922585d13d13399", size = 61171, upload-time = "2025-10-16T05:40:53.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/4e/ea2077d976fcf708ed01eafe08270b343c684ab155f6a25b36eb071c8616/wrapt-2.0.0rc5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9cbcd3d49e37edc15944be15c4c5e617acc46feb9b0fad8396a76888291c439", size = 61691, upload-time = "2025-10-16T05:40:54.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/11/117e9811478420fa9b8fbb0d370ed6dd6f8e730f9929e0798e012d1dda87/wrapt-2.0.0rc5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5bd3aba6c71bcb395b790e03ef22e9d440461e0446826f59e4d9f4926cbccf60", size = 121117, upload-time = "2025-10-16T05:40:57.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4c/e3053962a47dd42c7302a978b197efd91ee8a4160fa98a46cef33c74a5ba/wrapt-2.0.0rc5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a9a3b6700b569e8b8477356ef45840dc02a25150b503159c0a8d31ffe5e2954", size = 122468, upload-time = "2025-10-16T05:40:58.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/72/cf3f7fe0f4bcb4875024197f549362ad1e2a29d8e61e1c048c9727e95f29/wrapt-2.0.0rc5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ee79ae5fd4565ac8bcebbf5b0e328131806f2d6d58a97293bf818caf95d5ccb5", size = 116790, upload-time = "2025-10-16T05:40:55.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/60/94c42ac93e6812080475edddd637cd6d043d0efda05de06cbf7af9445003/wrapt-2.0.0rc5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7d5d7888fa87eaf12f5b304d3d1bb3ef754139efd434173de283ea7372b193ff", size = 120881, upload-time = "2025-10-16T05:40:59.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1d/7df35971283985d4c7e459ae3b60ea6772991724fa9da027e0ef85445ce0/wrapt-2.0.0rc5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:535830b64fc4be8c335cc55283c22bd82487b08713279dc628639c97ad3003c4", size = 115805, upload-time = "2025-10-16T05:41:00.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/05/4f78dbc6beff02cf4f4df6a1bf43cc5e11cdc823c7b049792d2567296ef5/wrapt-2.0.0rc5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b902ac39edbd7dac763ac7e8347a49176a216ba48821135d4dcdc393d26cdd7", size = 120221, upload-time = "2025-10-16T05:41:01.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c1/3bdbd54489a19a1e138dfef819487e6063943946e906a75d25a316197c73/wrapt-2.0.0rc5-cp312-cp312-win32.whl", hash = "sha256:d5b203c694e06064ebcdac91548857e3416ceba27615c341ee7f98e7305a563c", size = 58190, upload-time = "2025-10-16T05:41:05.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/ed/5c3156a2613b3f054ef40d0dfc335c7bf1f399bea78aced9253d3113dfcf/wrapt-2.0.0rc5-cp312-cp312-win_amd64.whl", hash = "sha256:31415c6779045134665e1f5ee5413793fcd780cff669ad21c24a46e17e575098", size = 60421, upload-time = "2025-10-16T05:41:03.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/29/e2e1b6da585e1225946e9c649859a04a672a5ef9541625ef0da84e199786/wrapt-2.0.0rc5-cp312-cp312-win_arm64.whl", hash = "sha256:8ee44dfae990e273a615f220760834ac95a1c6bb1c687b2d834cfc18f3753a67", size = 58942, upload-time = "2025-10-16T05:41:04.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/54/24dbd27f95960048722e472c8adb1382d602bc2b3f3e62a66dab6417ecbe/wrapt-2.0.0rc5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a3d4a0ea3e4deecc54e480759285047383243e06697b5b93059a786a1c478ed", size = 78099, upload-time = "2025-10-16T05:41:06.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/cd/ccd6e81754ccc132ce768ea9b8b27d7c12797399b3356a91035e23ae56f6/wrapt-2.0.0rc5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:743c9f74aad945e77bb987b79bb9563703fb934cb9a178ca2ca842b2a52e8cd7", size = 61173, upload-time = "2025-10-16T05:41:07.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bd/c738081209cca7e88a1a7edabccee7e80f6ea263ed06c08edb37d1fcfb19/wrapt-2.0.0rc5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:213cdf861283244f6d018a83e4a632842e1dac15f177574cf2bb5de0dfe42c79", size = 61690, upload-time = "2025-10-16T05:41:09.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/02/11c51633c58723d43216c73ed764a77b40b3a699c3387305994f0ccbcd49/wrapt-2.0.0rc5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bf37aa84c34ac526acf92bda8f4597840f21166505a3fc6a24fb6bd352f9784", size = 121141, upload-time = "2025-10-16T05:41:11.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/24/f482f1da723ae5fcbf2344f01db3de600504007a4d942a39f3eeff94e44e/wrapt-2.0.0rc5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1476b93afdce3b8fa5899e432b34209b159df7ea13d86f4255e229a49da2bf2e", size = 122515, upload-time = "2025-10-16T05:41:12.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/c8/896c6c6f5e3de8e07dc65856482fd08eba95279b8d0b5bb96ba44af5de87/wrapt-2.0.0rc5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d611ff3648403dd4f89ba61492c3271bfaf990542e08976a7b289d7a9414aefc", size = 116834, upload-time = "2025-10-16T05:41:10.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/4c/41762f6eb45bad4ff21e3bd9dc90d8b58631176977b8369ec8f8e815a0e9/wrapt-2.0.0rc5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8d46f194c3fbaab3501d462963c852fd726566a6a60b8d060ffe13db167054f", size = 120924, upload-time = "2025-10-16T05:41:14.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ff/01f4d28c85b221d3e58224902dda9e993a6c6f3d766fb73c233d069cb085/wrapt-2.0.0rc5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b5c6d44e67e36a6c52568861228f6c1e87b30e350e403e8351fc19a13c9f8a1", size = 115842, upload-time = "2025-10-16T05:41:15.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8b/8dd9de3ca56e51be772895b477c431a31e549b2e959f54bddc6aba2952c4/wrapt-2.0.0rc5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:59cd63af9b936aa0a4470c54f5a6621cf5ad21668978a9fc125bac2c10cf2e3a", size = 120252, upload-time = "2025-10-16T05:41:17.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/c1/606d4a0c335ca7182b8464b1e70eb338a20d085e68ed4fac8432b9e0daf5/wrapt-2.0.0rc5-cp313-cp313-win32.whl", hash = "sha256:7ee672ec5f0f5edb112b82fd8ec2ffc16c0391c24506c2b46018dd8ecf13d707", size = 58193, upload-time = "2025-10-16T05:41:21.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/66/c21260d7f0229d1e7eb40089d462b4cf3af511896a79867a57fd276741e5/wrapt-2.0.0rc5-cp313-cp313-win_amd64.whl", hash = "sha256:c8446447678f95f4cf11967385f5f40df947d19dbab554165e56b4f3f752697e", size = 60423, upload-time = "2025-10-16T05:41:18.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/ea/2c3c917d4972b737ac9c5ac738767351f8995d5b4a168e9114bb0c30e502/wrapt-2.0.0rc5-cp313-cp313-win_arm64.whl", hash = "sha256:5748ec10a65b1e607167dc3dcd4cd5fa985528b8e51e088580c4033eabdb02ec", size = 58947, upload-time = "2025-10-16T05:41:19.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/91/fe5f4c97d0b0d77ee910c1c4cd4a67038c3b9c73ebddcd256c84f6823f29/wrapt-2.0.0rc5-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:69702e7538fce422906c135554c828f0c50393149df624bde1ad52da6d5e2c70", size = 81935, upload-time = "2025-10-16T05:41:22.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/a9/ac98fe189e18f1d8adae56bf34f3521336d99e02625f72211c4aed7d4d93/wrapt-2.0.0rc5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:852f374dc49d4f13d4494f26dc35eae4e8ad4e79b40cbdf23a579e192ae0bbb8", size = 62926, upload-time = "2025-10-16T05:41:24.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a4/b4b429f5ca80a4d8c0ef73e77c3102a8055d5ee1a61cca35dc5e5ea1c377/wrapt-2.0.0rc5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9e19366bfe505342bf9a226dbf7ae5e7faef5b054ba78c5bcb6e3cb8b03c27cf", size = 63605, upload-time = "2025-10-16T05:41:25.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/30/a41d42a258b7b6abbe8e1cb05daccfa60ed8a20800c2da9ae30ec33c4797/wrapt-2.0.0rc5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c95fc0cde2a48bbdc0ef0ce7980b43547641685aef9dae06e7e55a28ce9418", size = 151635, upload-time = "2025-10-16T05:41:28.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/41/b8b6b36a7fcb97daaa836c6fafde11b75dd89bea00c8e42e6e556a814d91/wrapt-2.0.0rc5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9a8c28d3b3a70d257dff74c8a94ab3eba6bdab6db3d4f149f6ef4adc2f9d0a2", size = 157529, upload-time = "2025-10-16T05:41:29.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/1e/cd31b69c5dde92586bcab050c5a61c8b54bc31d7be7c4dbd9f2d72fc2946/wrapt-2.0.0rc5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:140e10b4321cca537d4c1804399cee0c1e9c509e0ce3d50739879a39925d2b19", size = 145036, upload-time = "2025-10-16T05:41:26.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/b8/ea5b8cc725419d97f2e7541858222a592e69307842b816499a0160ce5804/wrapt-2.0.0rc5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b780a2f93f8e121e4d7e83d0c9a3315d0792e5231e8b6507dee4cc30fc306f7", size = 154548, upload-time = "2025-10-16T05:41:30.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/16/335516a54ac3c22315b0e3e5cf5cf1f610303356c60023143ecac29cc3c4/wrapt-2.0.0rc5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:78af75c5f022b5417ac6e8e27aa1a4698f93ddfaf164a3039687873877ff6c22", size = 143456, upload-time = "2025-10-16T05:41:32.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/18/f20e96034eeb7b123f3c4782b33a2c372ef3187ec77e1591ef393c2dea42/wrapt-2.0.0rc5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e0ce6ef3919afc4546e3b83b5c5399624dfd1b1b0a78eeb05c79020b94c5a6a2", size = 149372, upload-time = "2025-10-16T05:41:34.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/b5/f52dea903ea2fd10984b5848998117888518796c1eda34b9b73950d3c3fc/wrapt-2.0.0rc5-cp313-cp313t-win32.whl", hash = "sha256:9f0ba54cf5922f7c106a07b1aede2a1810289bf1433c79a04cf68157c48c8892", size = 59854, upload-time = "2025-10-16T05:41:38.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/83/56546f16fd5b4c24e4caf0259e6197610bef2a481415685f826c177db6b2/wrapt-2.0.0rc5-cp313-cp313t-win_amd64.whl", hash = "sha256:fed4a4294327bcc3111cb6ba9efb09adccd04288ecf7e23e7629496c4547e19a", size = 63118, upload-time = "2025-10-16T05:41:35.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bb/d4a8ed4d6d4bc262f82f69c79508f9b03d7efd465278f28177e4c73edfe4/wrapt-2.0.0rc5-cp313-cp313t-win_arm64.whl", hash = "sha256:13f5fbc142f52082efb597d2e47536c3a52bfb91c1f9a414d86942c8bf0cb866", size = 60371, upload-time = "2025-10-16T05:41:36.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/e8/363993e86e5a2ce1fa2370d8ee48f8f9a7c1fc3088c875530b4aafb29a4a/wrapt-2.0.0rc5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da413190cf2ce273eede818d842c120baebc9e8c957b40db74f5544dc0342a86", size = 78229, upload-time = "2025-10-16T05:41:39.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/93/ecb551e36be23624ace6238de64a3906c339e856b68b8e8d4100e04d47b4/wrapt-2.0.0rc5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a2ddb7a2eb6fcd5715b5f7dff78cc7b4319b6799ecf7820938b595fba24d8a2", size = 61234, upload-time = "2025-10-16T05:41:40.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/d9/f13de10a2e834d9a7834f19b47ffe521bc7a1171dba7c265ac49f1d28d40/wrapt-2.0.0rc5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c448153a7fc56e4e26668d26947b6e9b17a310e53b00a4197f59a6f1f3687be", size = 61760, upload-time = "2025-10-16T05:41:41.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/76/4ea95e003f7d408dea6ac0435ce4f7b3e67399587eca27881aec36157d98/wrapt-2.0.0rc5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ea15caf6c969525a9a13a072ed7f66b7e200bd226bafa06af9b1cbb5c5ebcba3", size = 119969, upload-time = "2025-10-16T05:41:44.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/c8/23fefaca5cdf2f3dca489ff88e392c915933a548d5652ea63e00c3b3d823/wrapt-2.0.0rc5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd043239057835d4ad9ed8eb04e641bf00b39a23a86646b6dffc57559c0e76e9", size = 122266, upload-time = "2025-10-16T05:41:45.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/8b/83f6f7ee3adfb84506678ffc288cda846fd0ade7ee60f2b32f12b30bf0e1/wrapt-2.0.0rc5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63465c656e4f3c2125ca72e0fcd02fd9410aa7c4696a236f880adff04c43d539", size = 116945, upload-time = "2025-10-16T05:41:43.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/f4/456731e349b5bc3acbe8ca700fec12cfc4bb0bb040f57d806ce21748d3c2/wrapt-2.0.0rc5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cb12e4f3b4ab65d5c9645a426978e1216cd7dd540f310b157423d26a412353d0", size = 120785, upload-time = "2025-10-16T05:41:47.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/a1/bffdd04e3e67682595af320f9ceb0082f974b16f2d22c6439b1ec4086ae7/wrapt-2.0.0rc5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7c10865f52f17bb2764db9e537c8e264180ef7f67819ff50b8ab0ff3a0ba4a78", size = 115854, upload-time = "2025-10-16T05:41:48.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/bb/7eaf21e694c80c1fad6bdbef8610e8c2f6d89fafb4cdd3075fd3454bde5f/wrapt-2.0.0rc5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:823a8850352480ed99fda30464492451acf2feffc2eb41df12fe79bdcd9234d7", size = 119176, upload-time = "2025-10-16T05:41:49.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/62/03170931bc28bd9d02acc6122fa34c528eb10adc4b2d239a578e403f82e8/wrapt-2.0.0rc5-cp314-cp314-win32.whl", hash = "sha256:1258c10a8a0eb2e6aff8f06ed5d32a1bfaad24b1dbbec2ef917c4bae387dc7a6", size = 58673, upload-time = "2025-10-16T05:41:53.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/11/9659431c85af2f860d9c46ca4b220b52783ceddf0a1121b1e452f81635e3/wrapt-2.0.0rc5-cp314-cp314-win_amd64.whl", hash = "sha256:1df509f43a220cde4016d621e9ce9d776bd443f8e583ee72aad87156e0f1b808", size = 60945, upload-time = "2025-10-16T05:41:51.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/fc/83d1d42c59b45b35920a09f49f9017e523a17232e2db1e82f0d23228a2a3/wrapt-2.0.0rc5-cp314-cp314-win_arm64.whl", hash = "sha256:480628da3585e0637da62acbdf87ba19a1a00508b058b22577f42c7d84ffb14d", size = 59343, upload-time = "2025-10-16T05:41:52.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/3e/4f41b295649868e2f2d00e0a1d3ce7226e4baa0c8ce6c821442292e6e3ca/wrapt-2.0.0rc5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:0b513277cd94eba52395e277d50ccd5bf2ecdc40d116dca50baad4de2f47be46", size = 81942, upload-time = "2025-10-16T05:41:54.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1d/952c9f176784d02989b0da0f3619665021c6dad98cfb461f27a85bdc1acb/wrapt-2.0.0rc5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:90dda73836f8bde801fa53b59e16b7863f7897b19e3aa8bea938c4db33a2fc9b", size = 62926, upload-time = "2025-10-16T05:41:55.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/5d/4f46b72d6719ba9049e0e093c260f700a2793a01c47b83bbae7ad3529982/wrapt-2.0.0rc5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e91eedff840e90de3abd694096aefe4916564e0391429b9af5851aecdfd961e", size = 63606, upload-time = "2025-10-16T05:41:56.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/4f/b56c4d7516789fb246c48df8de0a000478c511583bafb3776dec16260145/wrapt-2.0.0rc5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3f9b6a6138dabe45948630836ed1bd090f594f900b70b8ecaedd65c2d0ed9465", size = 151635, upload-time = "2025-10-16T05:41:59.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/da/85fd0733a2f8107a1fa96bc26c72b0874df3528ae621b8dbc3ac97a81c99/wrapt-2.0.0rc5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2130d94898b13fc6a1469daa3897c4fffa7b6c3abfa2469785e696555498f42f", size = 157530, upload-time = "2025-10-16T05:42:00.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/65/39c4b726f3bc42d3918b75fab8d52c234d9a6113e458e832f52df22b51c7/wrapt-2.0.0rc5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4aae5942fe69d27409f2d700afaaaf76fbb7ca27436f16aad9baf0220d68c258", size = 145056, upload-time = "2025-10-16T05:41:58.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/f3/47b32f02b9fe1aa87ed532e007c70f24a9fca6c12a4f2bfe6720ef37c13d/wrapt-2.0.0rc5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ed20a56e98c9c5474894c4bf1c88024ec4d76e519440f4508191b4d6292eab4", size = 154541, upload-time = "2025-10-16T05:42:01.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/eb/0ce25991243d2ed79bc106175f2f1356654905ed4b5b590d08ca48bf7732/wrapt-2.0.0rc5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:524777f776b4f886aa35baff21a2c5f1a967d3d1b2d26d423094e25e8fe98cf1", size = 143448, upload-time = "2025-10-16T05:42:03.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/4a/627dc73f9b3b0441660a98d2fb0b77d27fc9a0a83a9789b58e1297565ee3/wrapt-2.0.0rc5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b3a02c3313cc82001d5dbb9b7b452377ca01e0ac34fb1a1aeee784a0fe80de5", size = 149391, upload-time = "2025-10-16T05:42:04.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/45/0688a00c33e3bc177ad262b6a7cffc69d547abb86b7c9901b0295029f889/wrapt-2.0.0rc5-cp314-cp314t-win32.whl", hash = "sha256:273c8244de7714fa79c7510c8b787a8bb6fab8bbbe299866d392b99e9c34f98f", size = 60579, upload-time = "2025-10-16T05:42:07.649Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/ed/0707864c474f931b791210b98cb2de9a9dd7ed6c9cadb4d1fd5520afd6e4/wrapt-2.0.0rc5-cp314-cp314t-win_amd64.whl", hash = "sha256:d8f89df76f0ff8b876065424c0699e93f0e82e4dd23e3454b8fcdf3322319788", size = 63894, upload-time = "2025-10-16T05:42:05.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/53/98586856330a8bca326b2cfec360ead24b8de03327401194ec6baba4a6c4/wrapt-2.0.0rc5-cp314-cp314t-win_arm64.whl", hash = "sha256:4d9be2e727a88a68acb1466f9ab9803940f858effbc1b972480b194db18d8a5b", size = 60624, upload-time = "2025-10-16T05:42:06.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/d5/0db5eac4d795bce584f20e11bd71c0690a38e2577128ce73ccbbcebc587a/wrapt-2.0.0rc5-py3-none-any.whl", hash = "sha256:f0c89fd91cda69a9e02cdaa19d2734df6c005cb3f36a3a7079f266b0a731d95b", size = 29389, upload-time = "2025-10-16T05:42:37.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user