mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-24 16:39:52 +08:00
chore: initialize
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
## Overview
|
||||
|
||||
我正在设计一个新的架构,以实现插件与核心系统的运行时环境隔离,以换取更佳的安全性和兼容性。这个架构将会形成一个 SDK,供插件开发者使用。以下是我目前的设计思路和功能规划:
|
||||
|
||||
这个 SDK 主要用于新的插件的 CLI bootstrap、Plugin Runtime 以及开发平台。
|
||||
|
||||
## 功能规划
|
||||
|
||||
### 对插件端:插件脚手架
|
||||
|
||||
1. CLI 指令: 初始化插件模版、指令等组件,作为 bootstraper
|
||||
```bash
|
||||
# === Scaffold ===
|
||||
astr init # 新的插件模版
|
||||
astr add command # 注册一个指令 / 指令组 handler 类
|
||||
astr add listener # 注册一个监听器
|
||||
astr add llmtool # 注册一个 LLM Tool
|
||||
|
||||
# 交互式创建,参考 Vue 脚手架,如:
|
||||
# Is command group: [Y]es / [N]o
|
||||
# Command Name: calc
|
||||
# Description: xxxxxx
|
||||
|
||||
# === Deployment ===
|
||||
astr tree # 解析 filters 已注册的 handlers,按类型列出
|
||||
astr sync # 解析 filters 已注册的 handlers,并刷写到 plugin.yaml / metadata.yaml
|
||||
astr dev # 启动开发环境(WebSockets 自动连接到 AstrBot Core)
|
||||
astr build # 打包并构建资产
|
||||
astr publish # 发布到 GitHub Issue / 插件市场!
|
||||
```
|
||||
2. 抽象 - 提供完整的插件开发时要用到的类和类方法的抽象
|
||||
3. 注册器 - 接受插件注册的所有 Handlers
|
||||
4. 通信 - 与 AstrBot Core 通信
|
||||
|
||||
### 对核心系统端:插件运行时环境
|
||||
|
||||
- 通信 - 与插件端的双向通信
|
||||
- 插件管理 - 封装通信方法(如获取一个事件激活的 star handlers / 调用某个 Star Handler / 禁用某个插件)
|
||||
|
||||
## 架构
|
||||
|
||||
我们将旧插件命名为 LegacyStar,将新插件命名为 NewStar。LegacyStar 直接运行在 AstrBot Core 进程中,而 NewStar 则运行在一个独立的进程中,通过 IPC 与 AstrBot Core 通信。NewStar 进程将使用 astrbot-sdk 作为其运行时环境。
|
||||
|
||||
对于 NewStar 与 Core 之间的通信,我们将使用 stdio 或者 WebSockets 作为 IPC 的通信通道。
|
||||
|
||||
我们会设计一个 VirtualPluginLayer,以让 Core 端可以透明地调用 NewStar 的 Handlers,就像调用 LegacyStar 一样。
|
||||
|
||||
## 通信过程
|
||||
|
||||
通信过程应该是全双工的。
|
||||
|
||||
1. Core 调用 `VirtualPluginLayer.initialize`,启动插件进程。
|
||||
2. 插件进程启动后,Core 调用 `VirtualPluginLayer.handshake()`,进行握手,获取插件的元数据,如支持的 Handlers 列表等。
|
||||
3. 当消息平台有事件(AstrMessageEvent)下发时,Core 调用 `VirtualPluginLayer.get_triggered_handlers(event)`,获取需要处理该事件的 Handlers 列表。这一步不需要通信,因为 SDK 已经在上一步缓存了插件的 Handlers 列表元数据。
|
||||
4. 如果有 handler 触发,Core 调用 `VirtualPluginLayer.call_handler(event, xxxx)`,调用某个 Handler 处理事件,等待结果返回。
|
||||
5. 4 步骤期间,handler 可能会调用一些 Core 中的方法,如发送消息、获取对话历史等,这些调用通过 RPC 方式进行。
|
||||
6. 插件 handler 处理完事件后,返回结果给 Core,Core 继续后续的事件处理流程。
|
||||
7. 插件可能会主动向 Core 发送事件通知,Core 接收到后,进行相应的处理。
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
|
||||
.DS_Store
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -0,0 +1,12 @@
|
||||
from astrbot.api.v1.components.command import CommandComponent
|
||||
from astrbot.api.v1.event import AstrMessageEvent, filter
|
||||
from astrbot.api.v1.context import Context
|
||||
|
||||
|
||||
class HelloCommand(CommandComponent):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
@filter.command("hello")
|
||||
async def hello(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("Hello, Astrbot!")
|
||||
@@ -0,0 +1,12 @@
|
||||
from astrbot.api.v1.components.listener import ListenerComponent
|
||||
from astrbot.api.v1.event import AstrMessageEvent, filter
|
||||
from astrbot.api.v1.context import Context
|
||||
|
||||
|
||||
class EchoListener(ListenerComponent):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
@filter.platform_adapter_type(filter.PlatformAdapterType.ALL)
|
||||
async def on_message(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("Hello, Astrbot!")
|
||||
@@ -0,0 +1,31 @@
|
||||
from mcp.types import CallToolResult
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from astrbot.api.v1.components.agent.tool import FunctionTool
|
||||
from astrbot.api.v1.components.agent import ContextWrapper, AstrAgentContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class HelloWorldTool(FunctionTool):
|
||||
name: str = "hello_world" # 工具名称
|
||||
description: str = "Say hello to the world." # 工具描述
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"greeting": {
|
||||
"type": "string",
|
||||
"description": "The greeting message.",
|
||||
},
|
||||
},
|
||||
"required": ["greeting"],
|
||||
}
|
||||
) # 工具参数定义,见 OpenAI 官网或 https://json-schema.org/understanding-json-schema/
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> str | CallToolResult:
|
||||
# event 在 context.context.event 中可用
|
||||
greeting = kwargs.get("greeting", "Hello")
|
||||
return f"{greeting}, World!" # 也支持 mcp.types.CallToolResult 类型
|
||||
@@ -0,0 +1,22 @@
|
||||
_schema_version: 2
|
||||
name: astrbot_plugin_helloworld
|
||||
display_name: HelloWorld 插件
|
||||
desc: 一个简单的问候插件示例
|
||||
author: Soulter
|
||||
version: 0.1.0
|
||||
components: # 组件列表,将支持自动生成
|
||||
- class: plugin_sample.commands.hello:HelloCommand
|
||||
type: command
|
||||
name: hello
|
||||
description: 发送问候消息
|
||||
subcommands:
|
||||
- name: wow
|
||||
description: 发送 "Hello, Astrbot!" 消息
|
||||
- class: plugin_sample.tools.echo:EchoTool
|
||||
type: llm_tool
|
||||
name: echo_tool
|
||||
description: 回显输入的消息
|
||||
- class: plugin_sample.listeners.message_listener:MessageListener
|
||||
type: listener
|
||||
name: message_logger
|
||||
description: 监听并记录所有消息
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "astrbot-sdk"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"aiohttp>=3.13.2",
|
||||
"certifi>=2025.10.5",
|
||||
"docstring-parser>=0.17.0",
|
||||
"loguru>=0.7.3",
|
||||
"pydantic>=2.12.3",
|
||||
"pyyaml>=6.0.3",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
class AstrBotConfig(dict): ...
|
||||
@@ -0,0 +1,221 @@
|
||||
# from astr_agent_sdk.message import AssistantMessageSegment, UserMessageSegment
|
||||
|
||||
|
||||
class BaseConversationManager:
|
||||
"""负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。"""
|
||||
|
||||
async def _trigger_session_deleted(self, unified_msg_origin: str) -> None:
|
||||
"""触发会话删除回调.
|
||||
|
||||
Args:
|
||||
unified_msg_origin: 会话ID
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def new_conversation(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
platform_id: str | None = None,
|
||||
content: list[dict] | None = None,
|
||||
title: str | None = None,
|
||||
persona_id: str | None = None,
|
||||
) -> str:
|
||||
"""新建对话,并将当前会话的对话转移到新对话.
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
Returns:
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def switch_conversation(self, unified_msg_origin: str, conversation_id: str):
|
||||
"""切换会话的对话
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def delete_conversation(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
conversation_id: str | None = None,
|
||||
):
|
||||
"""删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def delete_conversations_by_user_id(self, unified_msg_origin: str):
|
||||
"""删除会话的所有对话
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_curr_conversation_id(self, unified_msg_origin: str) -> str | None:
|
||||
"""获取会话当前的对话 ID
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
Returns:
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
# async def get_conversation(
|
||||
# self,
|
||||
# unified_msg_origin: str,
|
||||
# conversation_id: str,
|
||||
# create_if_not_exists: bool = False,
|
||||
# ) -> Conversation | None:
|
||||
# """获取会话的对话.
|
||||
|
||||
# Args:
|
||||
# unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
# conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
# create_if_not_exists (bool): 如果对话不存在,是否创建一个新的对话
|
||||
# Returns:
|
||||
# conversation (Conversation): 对话对象
|
||||
|
||||
# """
|
||||
# ...
|
||||
|
||||
# async def get_conversations(
|
||||
# self,
|
||||
# unified_msg_origin: str | None = None,
|
||||
# platform_id: str | None = None,
|
||||
# ) -> list[Conversation]:
|
||||
# """获取对话列表.
|
||||
|
||||
# Args:
|
||||
# unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id,可选
|
||||
# platform_id (str): 平台 ID, 可选参数, 用于过滤对话
|
||||
# Returns:
|
||||
# conversations (List[Conversation]): 对话对象列表
|
||||
|
||||
# """
|
||||
# ...
|
||||
|
||||
# async def get_filtered_conversations(
|
||||
# self,
|
||||
# page: int = 1,
|
||||
# page_size: int = 20,
|
||||
# platform_ids: list[str] | None = None,
|
||||
# search_query: str = "",
|
||||
# **kwargs,
|
||||
# ) -> tuple[list[Conversation], int]:
|
||||
# """获取过滤后的对话列表.
|
||||
|
||||
# Args:
|
||||
# page (int): 页码, 默认为 1
|
||||
# page_size (int): 每页大小, 默认为 20
|
||||
# platform_ids (list[str]): 平台 ID 列表, 可选
|
||||
# search_query (str): 搜索查询字符串, 可选
|
||||
# Returns:
|
||||
# conversations (list[Conversation]): 对话对象列表
|
||||
|
||||
# """
|
||||
# ...
|
||||
|
||||
async def update_conversation(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
conversation_id: str | None = None,
|
||||
history: list[dict] | None = None,
|
||||
title: str | None = None,
|
||||
persona_id: str | None = None,
|
||||
) -> None:
|
||||
"""更新会话的对话.
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
history (List[Dict]): 对话历史记录, 是一个字典列表, 每个字典包含 role 和 content 字段
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def update_conversation_title(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
title: str,
|
||||
conversation_id: str | None = None,
|
||||
) -> None:
|
||||
"""更新会话的对话标题.
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
title (str): 对话标题
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
Deprecated:
|
||||
Use `update_conversation` with `title` parameter instead.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def update_conversation_persona_id(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
persona_id: str,
|
||||
conversation_id: str | None = None,
|
||||
) -> None:
|
||||
"""更新会话的对话 Persona ID.
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
persona_id (str): 对话 Persona ID
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
Deprecated:
|
||||
Use `update_conversation` with `persona_id` parameter instead.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
# async def add_message_pair(
|
||||
# self,
|
||||
# cid: str,
|
||||
# user_message: UserMessageSegment | dict,
|
||||
# assistant_message: AssistantMessageSegment | dict,
|
||||
# ) -> None:
|
||||
# """Add a user-assistant message pair to the conversation history.
|
||||
|
||||
# Args:
|
||||
# cid (str): Conversation ID
|
||||
# user_message (UserMessageSegment | dict): OpenAI-format user message object or dict
|
||||
# assistant_message (AssistantMessageSegment | dict): OpenAI-format assistant message object or dict
|
||||
|
||||
# Raises:
|
||||
# Exception: If the conversation with the given ID is not found
|
||||
# """
|
||||
# ...
|
||||
|
||||
async def get_human_readable_context(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
conversation_id: str,
|
||||
page: int = 1,
|
||||
page_size: int = 10,
|
||||
) -> tuple[list[str], int]:
|
||||
"""获取人类可读的上下文.
|
||||
|
||||
Args:
|
||||
unified_msg_origin (str): 统一的消息来源字符串。格式为 platform_name:message_type:session_id
|
||||
conversation_id (str): 对话 ID, 是 uuid 格式的字符串
|
||||
page (int): 页码
|
||||
page_size (int): 每页大小
|
||||
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,2 @@
|
||||
class CommandComponent:
|
||||
pass
|
||||
@@ -0,0 +1,5 @@
|
||||
from .astr_message_event import AstrMessageEvent
|
||||
|
||||
__all__ = [
|
||||
"AstrMessageEvent",
|
||||
]
|
||||
@@ -0,0 +1,370 @@
|
||||
from __future__ import annotations
|
||||
import typing as T
|
||||
from .astrbot_message import AstrBotMessage, Group
|
||||
from ...api.platform.platform_metadata import PlatformMetadata
|
||||
from ...api.event.message_type import MessageType
|
||||
from ...api.event.message_session import MessageSession
|
||||
from ...api.event.event_result import MessageEventResult
|
||||
from ...api.message.chain import MessageChain
|
||||
from ...api.message.components import BaseMessageComponent
|
||||
from dataclasses import dataclass, field
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AstrMessageEventModel(BaseModel):
|
||||
message_str: str
|
||||
message_obj: AstrBotMessage
|
||||
platform_meta: PlatformMetadata
|
||||
session_id: str
|
||||
role: T.Literal["admin", "member"] = "member"
|
||||
is_wake: bool = False
|
||||
is_at_or_wake_command: bool = False
|
||||
extras: dict = Field(default_factory=dict)
|
||||
result: MessageEventResult | None = None
|
||||
has_send_oper: bool = False
|
||||
call_llm: bool = False
|
||||
plugins_name: list[str] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_event(cls, event: AstrMessageEvent) -> AstrMessageEventModel:
|
||||
return cls(
|
||||
message_str=event.message_str,
|
||||
message_obj=event.message_obj,
|
||||
platform_meta=event.platform_meta,
|
||||
session_id=event.session_id,
|
||||
role=event.role,
|
||||
is_wake=event.is_wake,
|
||||
is_at_or_wake_command=event.is_at_or_wake_command,
|
||||
extras=event._extras,
|
||||
result=event._result,
|
||||
has_send_oper=event.has_send_oper,
|
||||
call_llm=event.call_llm,
|
||||
plugins_name=event._plugins_name,
|
||||
)
|
||||
|
||||
def to_event(self) -> AstrMessageEvent:
|
||||
event = AstrMessageEvent(
|
||||
message_str=self.message_str,
|
||||
message_obj=self.message_obj,
|
||||
platform_meta=self.platform_meta,
|
||||
session_id=self.session_id,
|
||||
role=self.role,
|
||||
is_wake=self.is_wake,
|
||||
is_at_or_wake_command=self.is_at_or_wake_command,
|
||||
_extras=self.extras,
|
||||
_result=self.result,
|
||||
has_send_oper=self.has_send_oper,
|
||||
call_llm=self.call_llm,
|
||||
_plugins_name=self.plugins_name,
|
||||
)
|
||||
return event
|
||||
|
||||
|
||||
@dataclass
|
||||
class AstrMessageEvent:
|
||||
message_str: str
|
||||
"""消息的纯文本内容"""
|
||||
|
||||
message_obj: AstrBotMessage
|
||||
"""消息对象"""
|
||||
|
||||
platform_meta: PlatformMetadata
|
||||
"""平台适配器的元信息"""
|
||||
|
||||
session_id: str
|
||||
"""会话 ID"""
|
||||
|
||||
role: T.Literal["admin", "member"] = "member"
|
||||
"""消息发送者的角色,如 "admin", "member" 等"""
|
||||
|
||||
is_wake: bool = False
|
||||
"""是否唤醒(是否通过 WakingStage)"""
|
||||
|
||||
is_at_or_wake_command: bool = False
|
||||
"""是否艾特机器人或通过唤醒命令触发的消息"""
|
||||
|
||||
_extras: dict = field(default_factory=dict)
|
||||
"""存储额外的信息"""
|
||||
|
||||
_result: MessageEventResult | None = None
|
||||
"""消息事件的结果"""
|
||||
|
||||
has_send_oper: bool = False
|
||||
"""是否已经发送过操作"""
|
||||
|
||||
call_llm: bool = False
|
||||
"""是否调用 LLM"""
|
||||
|
||||
_plugins_name: list[str] = field(default_factory=list)
|
||||
"""处理该事件的插件名称列表"""
|
||||
|
||||
def __post_init__(self):
|
||||
self.session = MessageSession(
|
||||
platform_name=self.platform_meta.id,
|
||||
message_type=self.message_obj.type,
|
||||
session_id=self.session_id,
|
||||
)
|
||||
self.unified_msg_origin = str(self.session)
|
||||
self.platform = self.platform_meta # back compatibility
|
||||
|
||||
def get_platform_name(self) -> str:
|
||||
"""
|
||||
获取这个事件所属的平台的类型(如 aiocqhttp, slack, discord 等)。
|
||||
NOTE: 用户可能会同时运行多个相同类型的平台适配器。
|
||||
"""
|
||||
return self.platform_meta.name
|
||||
|
||||
def get_platform_id(self):
|
||||
"""
|
||||
获取这个事件所属的平台的 ID。
|
||||
NOTE: 用户可能会同时运行多个相同类型的平台适配器,但能确定的是 ID 是唯一的。
|
||||
"""
|
||||
return self.platform_meta.id
|
||||
|
||||
def get_message_str(self) -> str:
|
||||
"""获取消息字符串。"""
|
||||
return self.message_str
|
||||
|
||||
def get_messages(self) -> list[BaseMessageComponent]:
|
||||
"""获取消息链。"""
|
||||
return self.message_obj.message
|
||||
|
||||
def get_message_type(self) -> MessageType:
|
||||
"""获取消息类型。"""
|
||||
return self.message_obj.type
|
||||
|
||||
def get_session_id(self) -> str:
|
||||
"""获取会话id。"""
|
||||
return self.session_id
|
||||
|
||||
def get_group_id(self) -> str:
|
||||
"""获取群组id。如果不是群组消息,返回空字符串。"""
|
||||
return self.message_obj.group_id
|
||||
|
||||
def get_self_id(self) -> str:
|
||||
"""获取机器人自身的id。"""
|
||||
return self.message_obj.self_id
|
||||
|
||||
def get_sender_id(self) -> str:
|
||||
"""获取消息发送者的id。"""
|
||||
return self.message_obj.sender.user_id
|
||||
|
||||
def get_sender_name(self) -> str | None:
|
||||
"""获取消息发送者的名称。(可能会返回空字符串)"""
|
||||
return self.message_obj.sender.nickname
|
||||
|
||||
def set_extra(self, key, value):
|
||||
"""设置额外的信息。"""
|
||||
self._extras[key] = value
|
||||
|
||||
def get_extra(self, key: str | None = None, default=None) -> T.Any:
|
||||
"""获取额外的信息。"""
|
||||
if key is None:
|
||||
return self._extras
|
||||
return self._extras.get(key, default)
|
||||
|
||||
def clear_extra(self):
|
||||
"""清除额外的信息。"""
|
||||
self._extras.clear()
|
||||
|
||||
def is_private_chat(self) -> bool:
|
||||
"""是否是私聊。"""
|
||||
return self.message_obj.type.value == (MessageType.FRIEND_MESSAGE).value
|
||||
|
||||
def is_wake_up(self) -> bool:
|
||||
"""是否是唤醒机器人的事件。"""
|
||||
return self.is_wake
|
||||
|
||||
def is_admin(self) -> bool:
|
||||
"""是否是管理员。"""
|
||||
return self.role == "admin"
|
||||
|
||||
# async def send_streaming(
|
||||
# self,
|
||||
# generator: AsyncGenerator[MessageChain, None],
|
||||
# use_fallback: bool = False,
|
||||
# ):
|
||||
# """发送流式消息到消息平台,使用异步生成器。
|
||||
# 目前仅支持: telegram,qq official 私聊。
|
||||
# Fallback仅支持 aiocqhttp。
|
||||
# """
|
||||
# asyncio.create_task(
|
||||
# Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name),
|
||||
# )
|
||||
# self._has_send_oper = True
|
||||
|
||||
def set_result(self, result: MessageEventResult | str):
|
||||
"""设置消息事件的结果。
|
||||
|
||||
Note:
|
||||
事件处理器可以通过设置结果来控制事件是否继续传播,并向消息适配器发送消息。
|
||||
|
||||
如果没有设置 `MessageEventResult` 中的 result_type,默认为 CONTINUE。即事件将会继续向后面的 listener 或者 command 传播。
|
||||
|
||||
Example:
|
||||
```
|
||||
async def ban_handler(self, event: AstrMessageEvent):
|
||||
if event.get_sender_id() in self.blacklist:
|
||||
event.set_result(MessageEventResult().set_console_log("由于用户在黑名单,因此消息事件中断处理。")).set_result_type(EventResultType.STOP)
|
||||
return
|
||||
|
||||
async def check_count(self, event: AstrMessageEvent):
|
||||
self.count += 1
|
||||
event.set_result(MessageEventResult().set_console_log("数量已增加", logging.DEBUG).set_result_type(EventResultType.CONTINUE))
|
||||
return
|
||||
```
|
||||
|
||||
"""
|
||||
if isinstance(result, str):
|
||||
result = MessageEventResult().message(result)
|
||||
# 兼容外部插件或调用方传入的 chain=None 的情况,确保为可迭代列表
|
||||
if isinstance(result, MessageEventResult) and result.chain is None:
|
||||
result.chain = []
|
||||
self._result = result
|
||||
|
||||
def stop_event(self):
|
||||
"""终止事件传播。"""
|
||||
if self._result is None:
|
||||
self.set_result(MessageEventResult().stop_event())
|
||||
else:
|
||||
self._result.stop_event()
|
||||
|
||||
def continue_event(self):
|
||||
"""继续事件传播。"""
|
||||
if self._result is None:
|
||||
self.set_result(MessageEventResult().continue_event())
|
||||
else:
|
||||
self._result.continue_event()
|
||||
|
||||
def is_stopped(self) -> bool:
|
||||
"""是否终止事件传播。"""
|
||||
if self._result is None:
|
||||
return False # 默认是继续传播
|
||||
return self._result.is_stopped()
|
||||
|
||||
def should_call_llm(self, call_llm: bool):
|
||||
"""是否在此消息事件中禁止默认的 LLM 请求。
|
||||
|
||||
只会阻止 AstrBot 默认的 LLM 请求链路,不会阻止插件中的 LLM 请求。
|
||||
"""
|
||||
self.call_llm = call_llm
|
||||
|
||||
def get_result(self) -> MessageEventResult | None:
|
||||
"""获取消息事件的结果。"""
|
||||
return self._result
|
||||
|
||||
def clear_result(self):
|
||||
"""清除消息事件的结果。"""
|
||||
self._result = None
|
||||
|
||||
"""消息链相关"""
|
||||
|
||||
def make_result(self) -> MessageEventResult:
|
||||
"""创建一个空的消息事件结果。
|
||||
|
||||
Example:
|
||||
```python
|
||||
# 纯文本回复
|
||||
yield event.make_result().message("Hi")
|
||||
# 发送图片
|
||||
yield event.make_result().url_image("https://example.com/image.jpg")
|
||||
yield event.make_result().file_image("image.jpg")
|
||||
```
|
||||
|
||||
"""
|
||||
return MessageEventResult()
|
||||
|
||||
def plain_result(self, text: str) -> MessageEventResult:
|
||||
"""创建一个空的消息事件结果,只包含一条文本消息。"""
|
||||
return MessageEventResult().message(text)
|
||||
|
||||
def image_result(self, url_or_path: str) -> MessageEventResult:
|
||||
"""创建一个空的消息事件结果,只包含一条图片消息。
|
||||
|
||||
根据开头是否包含 http 来判断是网络图片还是本地图片。
|
||||
"""
|
||||
if url_or_path.startswith("http"):
|
||||
return MessageEventResult().url_image(url_or_path)
|
||||
return MessageEventResult().file_image(url_or_path)
|
||||
|
||||
def chain_result(self, chain: list[BaseMessageComponent]) -> MessageEventResult:
|
||||
"""创建一个空的消息事件结果,包含指定的消息链。"""
|
||||
mer = MessageEventResult()
|
||||
mer.chain = chain
|
||||
return mer
|
||||
|
||||
# """LLM 请求相关"""
|
||||
|
||||
# def request_llm(
|
||||
# self,
|
||||
# prompt: str,
|
||||
# func_tool_manager=None,
|
||||
# session_id: str | None = None,
|
||||
# image_urls: list[str] | None = None,
|
||||
# contexts: list | None = None,
|
||||
# system_prompt: str = "",
|
||||
# conversation: Conversation | None = None,
|
||||
# ) -> ProviderRequest:
|
||||
# """创建一个 LLM 请求。
|
||||
|
||||
# Examples:
|
||||
# ```py
|
||||
# yield event.request_llm(prompt="hi")
|
||||
# ```
|
||||
# prompt: 提示词
|
||||
|
||||
# system_prompt: 系统提示词
|
||||
|
||||
# session_id: 已经过时,留空即可
|
||||
|
||||
# image_urls: 可以是 base64:// 或者 http:// 开头的图片链接,也可以是本地图片路径。
|
||||
|
||||
# contexts: 当指定 contexts 时,将会使用 contexts 作为上下文。如果同时传入了 conversation,将会忽略 conversation。
|
||||
|
||||
# func_tool_manager: 函数工具管理器,用于调用函数工具。用 self.context.get_llm_tool_manager() 获取。
|
||||
|
||||
# conversation: 可选。如果指定,将在指定的对话中进行 LLM 请求。对话的人格会被用于 LLM 请求,并且结果将会被记录到对话中。
|
||||
|
||||
# """
|
||||
# if image_urls is None:
|
||||
# image_urls = []
|
||||
# if contexts is None:
|
||||
# contexts = []
|
||||
# if len(contexts) > 0 and conversation:
|
||||
# conversation = None
|
||||
|
||||
# return ProviderRequest(
|
||||
# prompt=prompt,
|
||||
# session_id=session_id,
|
||||
# image_urls=image_urls,
|
||||
# func_tool=func_tool_manager,
|
||||
# contexts=contexts,
|
||||
# system_prompt=system_prompt,
|
||||
# conversation=conversation,
|
||||
# )
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
"""发送消息到消息平台。
|
||||
|
||||
Args:
|
||||
message (MessageChain): 消息链,具体使用方式请参考文档。
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
async def react(self, emoji: str):
|
||||
"""对消息添加表情回应。
|
||||
|
||||
默认实现为发送一条包含该表情的消息。
|
||||
注意:此实现并不一定符合所有平台的原生“表情回应”行为。
|
||||
如需支持平台原生的消息反应功能,请在对应平台的子类中重写本方法。
|
||||
"""
|
||||
...
|
||||
|
||||
async def get_group(self, group_id: str | None = None, **kwargs) -> Group | None:
|
||||
"""获取一个群聊的数据, 如果不填写 group_id: 如果是私聊消息,返回 None。如果是群聊消息,返回当前群聊的数据。
|
||||
|
||||
适配情况:
|
||||
|
||||
- aiocqhttp(OneBotv11)
|
||||
"""
|
||||
@@ -0,0 +1,98 @@
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .message_type import MessageType
|
||||
from ..message.components import BaseMessageComponent
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageMember:
|
||||
user_id: str
|
||||
nickname: str | None = None
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"User ID: {self.user_id},"
|
||||
f"Nickname: {self.nickname if self.nickname else 'N/A'}"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Group:
|
||||
group_id: str
|
||||
"""群号"""
|
||||
group_name: str | None = None
|
||||
"""群名称"""
|
||||
group_avatar: str | None = None
|
||||
"""群头像"""
|
||||
group_owner: str | None = None
|
||||
"""群主 id"""
|
||||
group_admins: list[str] | None = None
|
||||
"""群管理员 id"""
|
||||
members: list[MessageMember] | None = None
|
||||
"""所有群成员"""
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"Group ID: {self.group_id}\n"
|
||||
f"Name: {self.group_name if self.group_name else 'N/A'}\n"
|
||||
f"Avatar: {self.group_avatar if self.group_avatar else 'N/A'}\n"
|
||||
f"Owner ID: {self.group_owner if self.group_owner else 'N/A'}\n"
|
||||
f"Admin IDs: {self.group_admins if self.group_admins else 'N/A'}\n"
|
||||
f"Members Len: {len(self.members) if self.members else 0}\n"
|
||||
f"First Member: {self.members[0] if self.members else 'N/A'}\n"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AstrBotMessage:
|
||||
"""AstrBot 的消息对象"""
|
||||
|
||||
type: MessageType
|
||||
"""消息类型"""
|
||||
self_id: str
|
||||
"""机器人自身 ID"""
|
||||
session_id: str
|
||||
"""会话 ID"""
|
||||
message_id: str
|
||||
"""消息 ID"""
|
||||
sender: MessageMember
|
||||
"""发送者"""
|
||||
message: list[BaseMessageComponent]
|
||||
"""消息链组件列表"""
|
||||
message_str: str
|
||||
"""纯文本消息字符串"""
|
||||
raw_message: dict
|
||||
"""原始消息对象"""
|
||||
timestamp: int
|
||||
"""消息时间戳"""
|
||||
group: Group | None = None
|
||||
"""群信息,如果是私聊则为 None"""
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.timestamp = int(time.time())
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.__dict__)
|
||||
|
||||
@property
|
||||
def group_id(self) -> str:
|
||||
"""向后兼容的 group_id 属性
|
||||
群组id,如果为私聊,则为空
|
||||
"""
|
||||
if self.group:
|
||||
return self.group.group_id
|
||||
return ""
|
||||
|
||||
@group_id.setter
|
||||
def group_id(self, value: str):
|
||||
"""设置 group_id"""
|
||||
if value:
|
||||
if self.group:
|
||||
self.group.group_id = value
|
||||
else:
|
||||
self.group = Group(group_id=value)
|
||||
else:
|
||||
self.group = None
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from dataclasses import dataclass, field
|
||||
from typing import AsyncGenerator
|
||||
from ..message.chain import MessageChain
|
||||
|
||||
|
||||
class EventResultType(enum.Enum):
|
||||
"""用于描述事件处理的结果类型。
|
||||
|
||||
Attributes:
|
||||
CONTINUE: 事件将会继续传播
|
||||
STOP: 事件将会终止传播
|
||||
|
||||
"""
|
||||
|
||||
CONTINUE = enum.auto()
|
||||
STOP = enum.auto()
|
||||
|
||||
|
||||
class ResultContentType(enum.Enum):
|
||||
"""用于描述事件结果的内容的类型。"""
|
||||
|
||||
LLM_RESULT = enum.auto()
|
||||
"""调用 LLM 产生的结果"""
|
||||
GENERAL_RESULT = enum.auto()
|
||||
"""普通的消息结果"""
|
||||
STREAMING_RESULT = enum.auto()
|
||||
"""调用 LLM 产生的流式结果"""
|
||||
STREAMING_FINISH = enum.auto()
|
||||
"""流式输出完成"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageEventResult(MessageChain):
|
||||
"""MessageEventResult 描述了一整条消息中带有的所有组件以及事件处理的结果。
|
||||
现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。
|
||||
|
||||
Attributes:
|
||||
`chain` (list): 用于顺序存储各个组件。
|
||||
`use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。
|
||||
`result_type` (EventResultType): 事件处理的结果类型。
|
||||
|
||||
"""
|
||||
|
||||
result_type: EventResultType | None = field(
|
||||
default_factory=lambda: EventResultType.CONTINUE,
|
||||
)
|
||||
|
||||
result_content_type: ResultContentType | None = field(
|
||||
default_factory=lambda: ResultContentType.GENERAL_RESULT,
|
||||
)
|
||||
|
||||
# async_stream: AsyncGenerator | None = None
|
||||
# """异步流"""
|
||||
|
||||
def stop_event(self) -> MessageEventResult:
|
||||
"""终止事件传播。"""
|
||||
self.result_type = EventResultType.STOP
|
||||
return self
|
||||
|
||||
def continue_event(self) -> MessageEventResult:
|
||||
"""继续事件传播。"""
|
||||
self.result_type = EventResultType.CONTINUE
|
||||
return self
|
||||
|
||||
def is_stopped(self) -> bool:
|
||||
"""是否终止事件传播。"""
|
||||
return self.result_type == EventResultType.STOP
|
||||
|
||||
def set_async_stream(self, stream: AsyncGenerator) -> MessageEventResult:
|
||||
"""设置异步流。"""
|
||||
self.async_stream = stream
|
||||
return self
|
||||
|
||||
def set_result_content_type(self, typ: ResultContentType) -> MessageEventResult:
|
||||
"""设置事件处理的结果类型。
|
||||
|
||||
Args:
|
||||
result_type (EventResultType): 事件处理的结果类型。
|
||||
|
||||
"""
|
||||
self.result_content_type = typ
|
||||
return self
|
||||
|
||||
def is_llm_result(self) -> bool:
|
||||
"""是否为 LLM 结果。"""
|
||||
return self.result_content_type == ResultContentType.LLM_RESULT
|
||||
|
||||
|
||||
# 为了兼容旧版代码,保留 CommandResult 的别名
|
||||
CommandResult = MessageEventResult
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
|
||||
|
||||
class EventType(enum.Enum):
|
||||
"""表示一个 AstrBot 内部事件的类型。如适配器消息事件、LLM 请求事件、发送消息前的事件等
|
||||
|
||||
用于对 Handler 的职能分组。
|
||||
"""
|
||||
|
||||
OnAstrBotLoadedEvent = enum.auto() # AstrBot 加载完成
|
||||
OnPlatformLoadedEvent = enum.auto() # 平台加载完成
|
||||
|
||||
AdapterMessageEvent = enum.auto() # 收到适配器发来的消息
|
||||
OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件)
|
||||
OnLLMResponseEvent = enum.auto() # LLM 响应后
|
||||
OnDecoratingResultEvent = enum.auto() # 发送消息前
|
||||
OnCallingFuncToolEvent = enum.auto() # 调用函数工具
|
||||
OnAfterMessageSentEvent = enum.auto() # 发送消息后
|
||||
@@ -0,0 +1,52 @@
|
||||
from ...runtime.stars.filter.custom_filter import CustomFilter
|
||||
from ...runtime.stars.filter.event_message_type import (
|
||||
EventMessageType,
|
||||
EventMessageTypeFilter,
|
||||
)
|
||||
from ...runtime.stars.filter.permission import PermissionType, PermissionTypeFilter
|
||||
from ...runtime.stars.filter.platform_adapter_type import (
|
||||
PlatformAdapterType,
|
||||
PlatformAdapterTypeFilter,
|
||||
)
|
||||
from ...runtime.stars.registry.register import register_after_message_sent as after_message_sent
|
||||
from ...runtime.stars.registry.register import register_command as command
|
||||
from ...runtime.stars.registry.register import register_command_group as command_group
|
||||
from ...runtime.stars.registry.register import register_custom_filter as custom_filter
|
||||
from ...runtime.stars.registry.register import register_event_message_type as event_message_type
|
||||
# from ...runtime.stars.registry.register import register_llm_tool as llm_tool
|
||||
from ...runtime.stars.registry.register import register_on_astrbot_loaded as on_astrbot_loaded
|
||||
from ...runtime.stars.registry.register import (
|
||||
register_on_decorating_result as on_decorating_result,
|
||||
)
|
||||
from ...runtime.stars.registry.register import register_on_llm_request as on_llm_request
|
||||
from ...runtime.stars.registry.register import register_on_llm_response as on_llm_response
|
||||
from ...runtime.stars.registry.register import register_on_platform_loaded as on_platform_loaded
|
||||
from ...runtime.stars.registry.register import register_permission_type as permission_type
|
||||
from ...runtime.stars.registry.register import (
|
||||
register_platform_adapter_type as platform_adapter_type,
|
||||
)
|
||||
from ...runtime.stars.registry.register import register_regex as regex
|
||||
|
||||
__all__ = [
|
||||
"CustomFilter",
|
||||
"EventMessageType",
|
||||
"EventMessageTypeFilter",
|
||||
"PermissionType",
|
||||
"PermissionTypeFilter",
|
||||
"PlatformAdapterType",
|
||||
"PlatformAdapterTypeFilter",
|
||||
"after_message_sent",
|
||||
"command",
|
||||
"command_group",
|
||||
"custom_filter",
|
||||
"event_message_type",
|
||||
# "llm_tool",
|
||||
"on_astrbot_loaded",
|
||||
"on_decorating_result",
|
||||
"on_llm_request",
|
||||
"on_llm_response",
|
||||
"on_platform_loaded",
|
||||
"permission_type",
|
||||
"platform_adapter_type",
|
||||
"regex",
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..event.message_type import MessageType
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageSession:
|
||||
"""
|
||||
描述一条消息在 AstrBot 中对应的会话的唯一标识。
|
||||
如果您需要实例化 MessageSession,请不要给 platform_id 赋值(或者同时给 platform_name 和 platform_id 赋值相同值)。
|
||||
它会在 __post_init__ 中自动设置为 platform_name 的值。
|
||||
"""
|
||||
|
||||
platform_name: str
|
||||
"""平台适配器实例的唯一标识符。自 AstrBot v4.0.0 起,该字段实际为 platform_id。"""
|
||||
message_type: MessageType
|
||||
session_id: str
|
||||
platform_id: str | None = None
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.platform_id}:{self.message_type.value}:{self.session_id}"
|
||||
|
||||
def __post_init__(self):
|
||||
self.platform_id = self.platform_name
|
||||
|
||||
@staticmethod
|
||||
def from_str(session_str: str):
|
||||
platform_id, message_type, session_id = session_str.split(":")
|
||||
return MessageSession(platform_id, MessageType(message_type), session_id)
|
||||
|
||||
|
||||
MessageSesion = MessageSession # back compatibility
|
||||
@@ -0,0 +1,7 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MessageType(Enum):
|
||||
GROUP_MESSAGE = "GroupMessage" # 群组形式的消息
|
||||
FRIEND_MESSAGE = "FriendMessage" # 私聊、好友等单聊消息
|
||||
OTHER_MESSAGE = "OtherMessage" # 其他类型的消息,如系统消息等
|
||||
@@ -0,0 +1,136 @@
|
||||
from . import components as Comp
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class MessageChain:
|
||||
"""MessageChain 描述了一整条消息中带有的所有组件。
|
||||
现代消息平台的一条富文本消息中可能由多个组件构成,如文本、图片、At 等,并且保留了顺序。
|
||||
|
||||
Attributes:
|
||||
`chain` (list): 用于顺序存储各个组件。
|
||||
`use_t2i_` (bool): 用于标记是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。
|
||||
|
||||
"""
|
||||
|
||||
chain: list[Comp.BaseMessageComponent] = field(default_factory=list)
|
||||
use_t2i_: bool | None = None # None 为跟随用户设置
|
||||
type: str | None = None
|
||||
"""消息链承载的消息的类型。可选,用于让消息平台区分不同业务场景的消息链。"""
|
||||
|
||||
def message(self, message: str):
|
||||
"""添加一条文本消息到消息链 `chain` 中。
|
||||
|
||||
Example:
|
||||
CommandResult().message("Hello ").message("world!")
|
||||
# 输出 Hello world!
|
||||
|
||||
"""
|
||||
self.chain.append(Comp.Plain(text=message))
|
||||
return self
|
||||
|
||||
def at(self, name: str, qq: str):
|
||||
"""添加一条 At 消息到消息链 `chain` 中。
|
||||
|
||||
Example:
|
||||
CommandResult().at("张三", "12345678910")
|
||||
# 输出 @张三
|
||||
|
||||
"""
|
||||
self.chain.append(Comp.At(user_id=qq, user_name=name))
|
||||
return self
|
||||
|
||||
def at_all(self):
|
||||
"""添加一条 AtAll 消息到消息链 `chain` 中。
|
||||
|
||||
Example:
|
||||
CommandResult().at_all()
|
||||
# 输出 @所有人
|
||||
|
||||
"""
|
||||
self.chain.append(Comp.AtAll())
|
||||
return self
|
||||
|
||||
def error(self, message: str):
|
||||
"""[Deprecated] 添加一条错误消息到消息链 `chain` 中
|
||||
|
||||
Example:
|
||||
CommandResult().error("解析失败")
|
||||
|
||||
"""
|
||||
self.chain.append(Comp.Plain(text=message))
|
||||
return self
|
||||
|
||||
def url_image(self, url: str):
|
||||
"""添加一条图片消息(https 链接)到消息链 `chain` 中。
|
||||
|
||||
Note:
|
||||
如果需要发送本地图片,请使用 `file_image` 方法。
|
||||
|
||||
Example:
|
||||
CommandResult().image("https://example.com/image.jpg")
|
||||
|
||||
"""
|
||||
self.chain.append(Comp.Image(file=url))
|
||||
return self
|
||||
|
||||
def file_image(self, path: str):
|
||||
"""添加一条图片消息(本地文件路径)到消息链 `chain` 中。
|
||||
|
||||
Note:
|
||||
如果需要发送网络图片,请使用 `url_image` 方法。
|
||||
|
||||
Example:
|
||||
CommandResult().file_image("image.jpg")
|
||||
"""
|
||||
self.chain.append(Comp.Image(file=path))
|
||||
return self
|
||||
|
||||
def base64_image(self, base64_str: str):
|
||||
"""添加一条图片消息(base64 编码字符串)到消息链 `chain` 中。
|
||||
|
||||
Example:
|
||||
CommandResult().base64_image("iVBORw0KGgoAAAANSUhEUgAAAAUA...")
|
||||
"""
|
||||
self.chain.append(Comp.Image(file=base64_str))
|
||||
return self
|
||||
|
||||
def use_t2i(self, use_t2i: bool):
|
||||
"""设置是否使用文本转图片服务。
|
||||
|
||||
Args:
|
||||
use_t2i (bool): 是否使用文本转图片服务。默认为 None,即跟随用户的设置。当设置为 True 时,将会使用文本转图片服务。
|
||||
|
||||
"""
|
||||
self.use_t2i_ = use_t2i
|
||||
return self
|
||||
|
||||
def get_plain_text(self) -> str:
|
||||
"""获取纯文本消息。这个方法将获取 chain 中所有 Plain 组件的文本并拼接成一条消息。空格分隔。"""
|
||||
return " ".join(
|
||||
[comp.text for comp in self.chain if isinstance(comp, Comp.Plain)]
|
||||
)
|
||||
|
||||
def squash_plain(self):
|
||||
"""将消息链中的所有 Plain 消息段聚合到第一个 Plain 消息段中。"""
|
||||
if not self.chain:
|
||||
return None
|
||||
|
||||
new_chain = []
|
||||
first_plain = None
|
||||
plain_texts = []
|
||||
|
||||
for comp in self.chain:
|
||||
if isinstance(comp, Comp.Plain):
|
||||
if first_plain is None:
|
||||
first_plain = comp
|
||||
new_chain.append(comp)
|
||||
plain_texts.append(comp.text)
|
||||
else:
|
||||
new_chain.append(comp)
|
||||
|
||||
if first_plain is not None:
|
||||
first_plain.text = "".join(plain_texts)
|
||||
|
||||
self.chain = new_chain
|
||||
return self
|
||||
@@ -0,0 +1,225 @@
|
||||
from __future__ import annotations
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class ComponentType(str, Enum):
|
||||
# Basic Segment Types
|
||||
Plain = "Plain" # plain text message
|
||||
Image = "Image" # image
|
||||
Record = "Record" # audio
|
||||
Video = "Video" # video
|
||||
File = "File" # file attachment
|
||||
|
||||
# IM-specific Segment Types
|
||||
Face = "Face" # Emoji segment for Tencent QQ platform
|
||||
At = "At" # mention a user in IM apps
|
||||
Node = "Node" # a node in a forwarded message
|
||||
Nodes = "Nodes" # a forwarded message consisting of multiple nodes
|
||||
Poke = "Poke" # a poke message for Tencent QQ platform
|
||||
Reply = "Reply" # a reply message segment
|
||||
Forward = "Forward" # a forwarded message segment
|
||||
RPS = "RPS"
|
||||
Dice = "Dice"
|
||||
Shake = "Shake"
|
||||
Share = "Share"
|
||||
Contact = "Contact"
|
||||
Location = "Location"
|
||||
Music = "Music"
|
||||
Json = "Json"
|
||||
Unknown = "Unknown"
|
||||
WechatEmoji = "WechatEmoji"
|
||||
|
||||
|
||||
CompT = ComponentType
|
||||
|
||||
|
||||
class BaseMessageComponent(BaseModel):
|
||||
type: CompT
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Unified dict format"""
|
||||
return self.model_dump()
|
||||
|
||||
|
||||
class Plain(BaseMessageComponent):
|
||||
"""Represents a plain text message segment."""
|
||||
|
||||
type: Literal[CompT.Plain] = CompT.Plain
|
||||
text: str
|
||||
|
||||
|
||||
class Image(BaseMessageComponent):
|
||||
type: Literal[CompT.Image] = CompT.Image
|
||||
file: str
|
||||
"""base64-encoded image data, or file path, or HTTP URL"""
|
||||
|
||||
|
||||
class Record(BaseMessageComponent):
|
||||
type: Literal[CompT.Record] = CompT.Record
|
||||
file: str
|
||||
"""base64-encoded audio data, or file path, or HTTP URL"""
|
||||
|
||||
|
||||
class Video(BaseMessageComponent):
|
||||
type: Literal[CompT.Video] = CompT.Video
|
||||
file: str
|
||||
"""The video file URL."""
|
||||
|
||||
|
||||
class File(BaseMessageComponent):
|
||||
type: Literal[CompT.File] = CompT.File
|
||||
file_name: str
|
||||
mime_type: str | None = None
|
||||
file: str
|
||||
"""The file URL."""
|
||||
|
||||
|
||||
class At(BaseMessageComponent):
|
||||
type: Literal[CompT.At] = CompT.At
|
||||
user_id: str | None = None
|
||||
user_name: str | None = None
|
||||
|
||||
|
||||
class AtAll(At):
|
||||
user_id: str = "all"
|
||||
|
||||
|
||||
class Reply(BaseMessageComponent):
|
||||
type: Literal[CompT.Reply] = CompT.Reply
|
||||
id: str | int
|
||||
"""所引用的消息 ID"""
|
||||
chain: list[BaseMessageComponent] | None = []
|
||||
"""被引用的消息段列表"""
|
||||
sender_id: int | None | str = 0
|
||||
"""被引用的消息对应的发送者的 ID"""
|
||||
sender_nickname: str | None = ""
|
||||
"""被引用的消息对应的发送者的昵称"""
|
||||
time: int | None = 0
|
||||
"""被引用的消息发送时间"""
|
||||
message_str: str | None = ""
|
||||
"""被引用的消息解析后的纯文本消息字符串"""
|
||||
|
||||
|
||||
class Node(BaseMessageComponent):
|
||||
type: Literal[CompT.Node] = CompT.Node
|
||||
sender_id: str
|
||||
nickname: str | None = None
|
||||
content: list[BaseMessageComponent] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Nodes(BaseMessageComponent):
|
||||
type: Literal[CompT.Nodes] = CompT.Nodes
|
||||
nodes: list[Node] = Field(default_factory=list)
|
||||
|
||||
|
||||
class Face(BaseMessageComponent):
|
||||
type: Literal[CompT.Face] = CompT.Face
|
||||
id: int
|
||||
|
||||
|
||||
class RPS(BaseMessageComponent):
|
||||
type: Literal[CompT.RPS] = CompT.RPS
|
||||
|
||||
|
||||
class Dice(BaseMessageComponent):
|
||||
type: Literal[CompT.Dice] = CompT.Dice
|
||||
|
||||
|
||||
class Shake(BaseMessageComponent):
|
||||
type: Literal[CompT.Shake] = CompT.Shake
|
||||
|
||||
|
||||
class Share(BaseMessageComponent):
|
||||
type: Literal[CompT.Share] = CompT.Share
|
||||
url: str
|
||||
title: str
|
||||
content: str | None = ""
|
||||
image: str | None = ""
|
||||
|
||||
|
||||
class Contact(BaseMessageComponent):
|
||||
type: Literal[CompT.Contact] = CompT.Contact
|
||||
_type: str # type 字段冲突
|
||||
id: int | None = 0
|
||||
|
||||
|
||||
class Location(BaseMessageComponent):
|
||||
type: Literal[CompT.Location] = CompT.Location
|
||||
lat: float
|
||||
lon: float
|
||||
title: str | None = ""
|
||||
content: str | None = ""
|
||||
|
||||
|
||||
class Music(BaseMessageComponent):
|
||||
type: Literal[CompT.Music] = CompT.Music
|
||||
_type: str
|
||||
id: int | None = 0
|
||||
url: str | None = ""
|
||||
audio: str | None = ""
|
||||
title: str | None = ""
|
||||
content: str | None = ""
|
||||
image: str | None = ""
|
||||
|
||||
|
||||
class Poke(BaseMessageComponent):
|
||||
type: Literal[CompT.Poke] = CompT.Poke
|
||||
id: int | None = 0
|
||||
qq: int | None = 0
|
||||
|
||||
|
||||
class Forward(BaseMessageComponent):
|
||||
type: Literal[CompT.Forward] = CompT.Forward
|
||||
id: str
|
||||
|
||||
|
||||
class Json(BaseMessageComponent):
|
||||
type: Literal[CompT.Json] = CompT.Json
|
||||
data: dict
|
||||
|
||||
|
||||
class Unknown(BaseMessageComponent):
|
||||
type: Literal[CompT.Unknown] = CompT.Unknown
|
||||
text: str
|
||||
|
||||
|
||||
class WechatEmoji(BaseMessageComponent):
|
||||
type: Literal[CompT.WechatEmoji] = CompT.WechatEmoji
|
||||
md5: str | None = ""
|
||||
md5_len: int | None = 0
|
||||
cdnurl: str | None = ""
|
||||
|
||||
def __init__(self, **_):
|
||||
super().__init__(**_)
|
||||
|
||||
|
||||
ComponentTypes = {
|
||||
# Basic Message Segments
|
||||
"plain": Plain,
|
||||
"text": Plain,
|
||||
"image": Image,
|
||||
"record": Record,
|
||||
"video": Video,
|
||||
"file": File,
|
||||
# IM-specific Message Segments
|
||||
"face": Face,
|
||||
"at": At,
|
||||
"rps": RPS,
|
||||
"dice": Dice,
|
||||
"shake": Shake,
|
||||
"share": Share,
|
||||
"contact": Contact,
|
||||
"location": Location,
|
||||
"music": Music,
|
||||
"reply": Reply,
|
||||
"poke": Poke,
|
||||
"forward": Forward,
|
||||
"node": Node,
|
||||
"nodes": Nodes,
|
||||
"json": Json,
|
||||
"unknown": Unknown,
|
||||
"WechatEmoji": WechatEmoji,
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformMetadata:
|
||||
name: str
|
||||
"""平台的名称,即平台的类型,如 aiocqhttp, discord, slack"""
|
||||
description: str
|
||||
"""平台的描述"""
|
||||
id: str
|
||||
"""平台的唯一标识符,用于配置中识别特定平台"""
|
||||
|
||||
default_config_tmpl: dict | None = None
|
||||
"""平台的默认配置模板"""
|
||||
adapter_display_name: str | None = None
|
||||
"""显示在 WebUI 配置页中的平台名称,如空则是 name"""
|
||||
logo_path: str | None = None
|
||||
"""平台适配器的 logo 文件路径(相对于插件目录)"""
|
||||
@@ -0,0 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from ..basic.conversation_mgr import BaseConversationManager
|
||||
|
||||
|
||||
class Context(ABC):
|
||||
conversation_manager: BaseConversationManager
|
||||
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from ..basic.astrbot_config import AstrBotConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarMetadata:
|
||||
"""
|
||||
插件的元数据。
|
||||
当 activated 为 False 时,star_cls 可能为 None,请不要在插件未激活时调用 star_cls 的方法。
|
||||
"""
|
||||
|
||||
name: str | None = None
|
||||
"""插件名"""
|
||||
author: str | None = None
|
||||
"""插件作者"""
|
||||
desc: str | None = None
|
||||
"""插件简介"""
|
||||
version: str | None = None
|
||||
"""插件版本"""
|
||||
repo: str | None = None
|
||||
"""插件仓库地址"""
|
||||
|
||||
# star_cls_type: type[Star] | None = None
|
||||
# """插件的类对象的类型"""
|
||||
|
||||
module_path: str | None = None
|
||||
"""插件的模块路径"""
|
||||
|
||||
# star_cls: Star | None = None
|
||||
# """插件的类对象"""
|
||||
# module: ModuleType | None = None
|
||||
# """插件的模块对象"""
|
||||
|
||||
root_dir_name: str | None = None
|
||||
"""插件的目录名称"""
|
||||
reserved: bool = False
|
||||
"""是否是 AstrBot 的保留插件"""
|
||||
|
||||
activated: bool = True
|
||||
"""是否被激活"""
|
||||
|
||||
config: AstrBotConfig | None = None
|
||||
"""插件配置"""
|
||||
|
||||
star_handler_full_names: list[str] = field(default_factory=list)
|
||||
"""注册的 Handler 的全名列表"""
|
||||
|
||||
display_name: str | None = None
|
||||
"""用于展示的插件名称"""
|
||||
|
||||
logo_path: str | None = None
|
||||
"""插件 Logo 的路径"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Plugin {self.name} ({self.version}) by {self.author}: {self.desc}"
|
||||
@@ -0,0 +1,10 @@
|
||||
from ...api.star.context import Context as BaseContext
|
||||
from .conversation_mgr import ConversationManager
|
||||
|
||||
|
||||
class Context(BaseContext):
|
||||
def __init__(self, conversation_manager: ConversationManager):
|
||||
self.conversation_manager = conversation_manager
|
||||
|
||||
def _inject_rpc_handlers(self, runner):
|
||||
setattr(self.conversation_manager, "runner", runner)
|
||||
@@ -0,0 +1,14 @@
|
||||
from ...api.basic.conversation_mgr import BaseConversationManager
|
||||
from .util import rpc_method
|
||||
|
||||
|
||||
class ConversationManager(BaseConversationManager):
|
||||
@rpc_method
|
||||
async def new_conversation(
|
||||
self,
|
||||
unified_msg_origin: str,
|
||||
platform_id: str | None = None,
|
||||
content: list[dict] | None = None,
|
||||
title: str | None = None,
|
||||
persona_id: str | None = None,
|
||||
) -> str: ...
|
||||
@@ -0,0 +1,38 @@
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from typing import Callable
|
||||
from ..star_runner import StarRunner
|
||||
from ..types import CallContextFunctionRequest
|
||||
|
||||
|
||||
def rpc_method(func: Callable) -> Callable:
|
||||
"""sign as an RPC method."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
if not hasattr(self, "runner") or not isinstance(self.runner, StarRunner):
|
||||
raise RuntimeError(
|
||||
f"Class {self.__class__.__name__} is not configured for RPC calls."
|
||||
)
|
||||
method_name = f"{self.__class__.__name__}.{func.__name__}"
|
||||
sig = inspect.signature(func)
|
||||
bound_args = sig.bind(self, *args, **kwargs)
|
||||
bound_args.apply_defaults()
|
||||
params = dict(bound_args.arguments)
|
||||
params.pop("self")
|
||||
|
||||
runner: StarRunner = getattr(self, "runner")
|
||||
|
||||
return await runner._call_rpc(
|
||||
CallContextFunctionRequest(
|
||||
jsonrpc="2.0",
|
||||
id=runner._generate_request_id(),
|
||||
method="call_context_function",
|
||||
params=CallContextFunctionRequest.Params(
|
||||
name=method_name,
|
||||
args=params,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return wrapper
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
VPL means Virtual Star Layer.
|
||||
In the AstrBot 5.0 architecture, VPL is a layer that allows different types of stars to interact with the core system in a standardized way.
|
||||
Currently, AstrBot has two types of stars:
|
||||
1. Legacy Stars: These are the traditional stars that still running in the same runtime as AstrBot core.
|
||||
2. New Stars: These are the modern stars that run in isolated runtime, they communicate with AstrBot core through stdio streams or websocket.
|
||||
|
||||
The VPL module provides the necessary abstractions and interfaces to manage these stars seamlessly,
|
||||
let AstrBot core interact with both types of stars without needing to know the underlying implementation details.
|
||||
"""
|
||||
|
||||
from .stars.virtual import VirtualStar
|
||||
from .stars.new import NewStdioStar, NewWebSocketStar
|
||||
# from .types import StarURI, StarType
|
||||
|
||||
|
||||
class Galaxy:
|
||||
"""Manages the lifecycle and interactions of Virtual Stars (plugins) within AstrBot."""
|
||||
|
||||
vs_map: dict[str, VirtualStar] = {}
|
||||
|
||||
async def connect_to_stdio_star(self, star_name: str, config: dict) -> NewStdioStar:
|
||||
"""Connect to a new-style stdio star given its name."""
|
||||
star = NewStdioStar(**config)
|
||||
await star.initialize()
|
||||
self.vs_map[star_name] = star
|
||||
return star
|
||||
|
||||
async def connect_to_websocket_star(
|
||||
self, star_name: str, config: dict
|
||||
) -> NewWebSocketStar:
|
||||
"""Connect to a new-style websocket star given its name."""
|
||||
star = NewWebSocketStar(**config)
|
||||
await star.initialize()
|
||||
self.vs_map[star_name] = star
|
||||
return star
|
||||
@@ -0,0 +1,208 @@
|
||||
# JSON-RPC Server Implementation
|
||||
|
||||
This directory contains industry-standard implementations of JSON-RPC 2.0 servers for inter-process communication.
|
||||
|
||||
## Overview
|
||||
|
||||
The implementation follows best practices:
|
||||
|
||||
- **Clean separation of concerns**: Servers handle only communication, not business logic
|
||||
- **Async/await**: Non-blocking I/O for better performance
|
||||
- **Type safety**: Full type hints with Pydantic models
|
||||
- **Error handling**: Proper logging and error propagation
|
||||
- **Resource management**: Clean startup/shutdown lifecycle
|
||||
|
||||
## Architecture
|
||||
|
||||
### Base Class: `JSONRPCServer`
|
||||
|
||||
Abstract base class defining the server interface:
|
||||
|
||||
- `set_message_handler(handler)`: Register a callback for incoming messages
|
||||
- `start()`: Start the server
|
||||
- `stop()`: Stop the server and cleanup
|
||||
- `send_message(message)`: Send a JSON-RPC message
|
||||
|
||||
### STDIO Server: `StdioServer`
|
||||
|
||||
Communicates via standard input/output using line-delimited JSON.
|
||||
|
||||
**Features:**
|
||||
|
||||
- One JSON-RPC message per line
|
||||
- Non-blocking async I/O using executors
|
||||
- Thread-safe write operations with asyncio locks
|
||||
- Graceful EOF handling
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Plugin subprocess communication
|
||||
- Command-line tools
|
||||
- Pipeline-based architectures
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from astrbot_sdk.runtime.server import StdioServer
|
||||
from astrbot_sdk.runtime.rpc.jsonrpc import JSONRPCMessage
|
||||
|
||||
server = StdioServer()
|
||||
|
||||
def handle_message(message: JSONRPCMessage):
|
||||
# Process the message
|
||||
pass
|
||||
|
||||
server.set_message_handler(handle_message)
|
||||
await server.start()
|
||||
```
|
||||
|
||||
### WebSocket Server: `WebSocketServer`
|
||||
|
||||
Communicates via WebSocket connections.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Single active connection (typical for IPC)
|
||||
- Heartbeat/ping-pong for connection health
|
||||
- Support for text and binary messages
|
||||
- Graceful connection lifecycle management
|
||||
- Built on aiohttp for production readiness
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```python
|
||||
from astrbot_sdk.runtime.server import WebSocketServer
|
||||
|
||||
server = WebSocketServer(
|
||||
host="127.0.0.1",
|
||||
port=8765,
|
||||
path="/rpc",
|
||||
heartbeat=30.0 # seconds, 0 to disable
|
||||
)
|
||||
```
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Network-based plugin communication
|
||||
- Development/debugging (easier to inspect)
|
||||
- Multiple plugin instances
|
||||
|
||||
## Message Format
|
||||
|
||||
All servers use JSON-RPC 2.0 format:
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "unique-id",
|
||||
"method": "method_name",
|
||||
"params": {"key": "value"}
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "unique-id",
|
||||
"result": {"data": "response"}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "unique-id",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid Request",
|
||||
"data": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
See the `examples/` directory:
|
||||
|
||||
- `server_stdio_example.py`: STDIO server with echo handler
|
||||
- `server_websocket_example.py`: WebSocket server with echo handler
|
||||
- `client_stdio_test.py`: Test client for STDIO
|
||||
- `client_websocket_test.py`: Test client for WebSocket
|
||||
|
||||
### Running STDIO Example
|
||||
|
||||
Terminal 1 (server):
|
||||
|
||||
```bash
|
||||
python examples/server_stdio_example.py
|
||||
```
|
||||
|
||||
Then type JSON-RPC requests:
|
||||
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":"1","method":"test","params":{"hello":"world"}}
|
||||
```
|
||||
|
||||
Or use the test client:
|
||||
|
||||
```bash
|
||||
python examples/client_stdio_test.py | python examples/server_stdio_example.py
|
||||
```
|
||||
|
||||
### Running WebSocket Example
|
||||
|
||||
Terminal 1 (server):
|
||||
|
||||
```bash
|
||||
python examples/server_websocket_example.py
|
||||
```
|
||||
|
||||
Terminal 2 (client):
|
||||
|
||||
```bash
|
||||
python examples/client_websocket_test.py
|
||||
```
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **No business logic**: Servers only handle transport and serialization
|
||||
2. **Callback-based**: Use `set_message_handler()` for loose coupling
|
||||
3. **Async-first**: All I/O operations are non-blocking
|
||||
4. **Production-ready**: Proper error handling, logging, and resource cleanup
|
||||
5. **Testable**: Easy to mock and test with custom stdin/stdout
|
||||
|
||||
## Integration with AstrBot SDK
|
||||
|
||||
These servers are designed to be used by the Virtual Plugin Layer (VPL):
|
||||
|
||||
```python
|
||||
# In plugin runtime (subprocess)
|
||||
from astrbot_sdk.runtime.server import StdioServer
|
||||
|
||||
server = StdioServer()
|
||||
server.set_message_handler(handle_core_requests)
|
||||
await server.start()
|
||||
|
||||
# In AstrBot Core
|
||||
# Spawn plugin subprocess with stdio transport
|
||||
# Send JSON-RPC requests to plugin stdin
|
||||
# Receive JSON-RPC responses from plugin stdout
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
- Both servers use `asyncio.Lock` for write operations
|
||||
- Message handlers are called synchronously but can schedule async tasks
|
||||
- Servers must run in an asyncio event loop
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Parse errors are logged but don't crash the server
|
||||
- Connection errors trigger cleanup and can be recovered
|
||||
- User code exceptions in message handlers are contained
|
||||
@@ -0,0 +1,5 @@
|
||||
from .base import JSONRPCClient
|
||||
from .stdio import StdioClient
|
||||
from .websocket import WebSocketClient
|
||||
|
||||
__all__ = ["JSONRPCClient", "StdioClient", "WebSocketClient"]
|
||||
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from ..transport import JSONRPCTransport
|
||||
|
||||
|
||||
class JSONRPCClient(JSONRPCTransport, ABC):
|
||||
"""Base class for JSON-RPC clients.
|
||||
|
||||
Handles pure communication (reading/writing JSON-RPC messages).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import subprocess
|
||||
from typing import IO, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..jsonrpc import (
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCMessage,
|
||||
JSONRPCRequest,
|
||||
JSONRPCSuccessResponse,
|
||||
)
|
||||
from .base import JSONRPCClient
|
||||
|
||||
|
||||
class StdioClient(JSONRPCClient):
|
||||
"""JSON-RPC client using standard input/output for communication."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command: list[str],
|
||||
cwd: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the STDIO client.
|
||||
|
||||
Args:
|
||||
command: Command to start subprocess (e.g., ['python', 'plugin.py'])
|
||||
cwd: Working directory for subprocess
|
||||
"""
|
||||
super().__init__()
|
||||
self._command = command
|
||||
self._cwd = cwd
|
||||
self._process: subprocess.Popen | None = None
|
||||
self._stdin: IO[Any] | None = None
|
||||
self._stdout: IO[Any] | None = None
|
||||
self._read_task: asyncio.Task | None = None
|
||||
self._write_lock = asyncio.Lock()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the client and launch subprocess."""
|
||||
if self._running:
|
||||
logger.warning("StdioClient is already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
|
||||
# Start subprocess
|
||||
await self._start_subprocess()
|
||||
|
||||
self._read_task = asyncio.create_task(self._read_loop())
|
||||
logger.info("StdioClient started")
|
||||
|
||||
async def _start_subprocess(self) -> None:
|
||||
"""Start the subprocess and connect to its stdio."""
|
||||
logger.info(f"Starting subprocess: {' '.join(self._command)}")
|
||||
|
||||
try:
|
||||
self._process = subprocess.Popen(
|
||||
self._command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=self._cwd,
|
||||
text=True,
|
||||
bufsize=1, # Line buffered
|
||||
)
|
||||
|
||||
# Use subprocess's stdio
|
||||
self._stdin = self._process.stdout # Read from subprocess stdout
|
||||
self._stdout = self._process.stdin # Write to subprocess stdin
|
||||
|
||||
logger.info(f"Subprocess started with PID {self._process.pid}")
|
||||
|
||||
# Start monitoring stderr
|
||||
asyncio.create_task(self._monitor_stderr())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start subprocess: {e}")
|
||||
raise
|
||||
|
||||
async def _monitor_stderr(self) -> None:
|
||||
"""Monitor subprocess stderr and log output."""
|
||||
if not self._process or not self._process.stderr:
|
||||
return
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
while self._running and self._process.poll() is None:
|
||||
line = await loop.run_in_executor(None, self._process.stderr.readline)
|
||||
if line:
|
||||
logger.debug(f"[Subprocess stderr] {line.strip()}")
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring stderr: {e}")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the client and terminate subprocess if running."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
|
||||
# Cancel read task
|
||||
if self._read_task:
|
||||
self._read_task.cancel()
|
||||
try:
|
||||
await self._read_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._read_task = None
|
||||
|
||||
# Terminate subprocess if running
|
||||
if self._process:
|
||||
logger.info("Terminating subprocess...")
|
||||
self._process.terminate()
|
||||
try:
|
||||
self._process.wait(timeout=5.0)
|
||||
logger.info("Subprocess terminated gracefully")
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Subprocess did not terminate, killing...")
|
||||
self._process.kill()
|
||||
self._process.wait()
|
||||
logger.info("Subprocess killed")
|
||||
|
||||
self._process = None
|
||||
|
||||
logger.info("StdioClient stopped")
|
||||
|
||||
async def send_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Send a JSON-RPC message to stdout.
|
||||
|
||||
Args:
|
||||
message: The JSON-RPC message to send
|
||||
"""
|
||||
async with self._write_lock:
|
||||
try:
|
||||
json_str = message.model_dump_json(exclude_none=True)
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None, self._write_line, json_str
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send message: {e}")
|
||||
raise
|
||||
|
||||
def _write_line(self, line: str) -> None:
|
||||
"""Write a line to stdout (synchronous helper)."""
|
||||
if self._stdout:
|
||||
self._stdout.write(line + "\n")
|
||||
self._stdout.flush()
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
"""Main loop to read messages from stdin."""
|
||||
if not self._stdin:
|
||||
logger.error("No stdin available for reading")
|
||||
return
|
||||
|
||||
logger.debug("Started reading from stdin")
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
while self._running:
|
||||
# Read line from stdin in executor to avoid blocking
|
||||
line = await loop.run_in_executor(None, self._stdin.readline)
|
||||
|
||||
if not line:
|
||||
# EOF reached
|
||||
logger.info("EOF reached on stdin")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Parse JSON-RPC message
|
||||
message = self._parse_message(line)
|
||||
await self._handle_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse message: {e}, raw line: {line}")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Read loop cancelled")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in read loop: {e}")
|
||||
finally:
|
||||
logger.debug("Stopped reading from stdin")
|
||||
|
||||
def _parse_message(self, line: str) -> JSONRPCMessage:
|
||||
"""Parse a JSON-RPC message from a string.
|
||||
|
||||
Args:
|
||||
line: JSON string to parse
|
||||
|
||||
Returns:
|
||||
Parsed JSONRPCMessage (Request, SuccessResponse, or ErrorResponse)
|
||||
"""
|
||||
data = json.loads(line)
|
||||
|
||||
# Determine message type based on presence of fields
|
||||
if "method" in data:
|
||||
return JSONRPCRequest.model_validate(data)
|
||||
elif "error" in data:
|
||||
return JSONRPCErrorResponse.model_validate(data)
|
||||
elif "result" in data:
|
||||
return JSONRPCSuccessResponse.model_validate(data)
|
||||
else:
|
||||
raise ValueError(f"Invalid JSON-RPC message: {data}")
|
||||
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import aiohttp
|
||||
from loguru import logger
|
||||
|
||||
from ..jsonrpc import (
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCMessage,
|
||||
JSONRPCRequest,
|
||||
JSONRPCSuccessResponse,
|
||||
)
|
||||
from .base import JSONRPCClient
|
||||
|
||||
|
||||
class WebSocketClient(JSONRPCClient):
|
||||
"""JSON-RPC client using WebSocket for communication."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
heartbeat: float = 30.0,
|
||||
auto_reconnect: bool = True,
|
||||
reconnect_interval: float = 5.0,
|
||||
) -> None:
|
||||
"""Initialize the WebSocket client.
|
||||
|
||||
Args:
|
||||
url: WebSocket server URL (e.g., ws://127.0.0.1:8765/rpc)
|
||||
heartbeat: Heartbeat interval in seconds (0 to disable)
|
||||
auto_reconnect: Whether to automatically reconnect on disconnection
|
||||
reconnect_interval: Interval between reconnection attempts in seconds
|
||||
"""
|
||||
super().__init__()
|
||||
self._url = url
|
||||
self._heartbeat = heartbeat
|
||||
self._auto_reconnect = auto_reconnect
|
||||
self._reconnect_interval = reconnect_interval
|
||||
self._session: aiohttp.ClientSession | None = None
|
||||
self._ws: aiohttp.ClientWebSocketResponse | None = None
|
||||
self._write_lock = asyncio.Lock()
|
||||
self._read_task: asyncio.Task | None = None
|
||||
self._reconnect_task: asyncio.Task | None = None
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Connect to the WebSocket server."""
|
||||
if self._running:
|
||||
logger.warning("WebSocketClient is already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._session = aiohttp.ClientSession()
|
||||
|
||||
await self._connect()
|
||||
logger.info(f"WebSocketClient started and connected to {self._url}")
|
||||
|
||||
async def _connect(self) -> None:
|
||||
"""Establish WebSocket connection to the server."""
|
||||
try:
|
||||
if not self._session:
|
||||
raise RuntimeError("Session not initialized")
|
||||
|
||||
self._ws = await self._session.ws_connect(
|
||||
self._url,
|
||||
heartbeat=self._heartbeat if self._heartbeat > 0 else None,
|
||||
)
|
||||
logger.info(f"Connected to WebSocket server: {self._url}")
|
||||
|
||||
# Start reading messages
|
||||
self._read_task = asyncio.create_task(self._read_loop())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to WebSocket server: {e}")
|
||||
if self._auto_reconnect and self._running:
|
||||
logger.info(
|
||||
f"Will retry connection in {self._reconnect_interval} seconds..."
|
||||
)
|
||||
await asyncio.sleep(self._reconnect_interval)
|
||||
if self._running:
|
||||
await self._connect()
|
||||
else:
|
||||
raise
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Disconnect from the WebSocket server and cleanup resources."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
|
||||
# Cancel reconnection task if running
|
||||
if self._reconnect_task and not self._reconnect_task.done():
|
||||
self._reconnect_task.cancel()
|
||||
try:
|
||||
await self._reconnect_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._reconnect_task = None
|
||||
|
||||
# Cancel read task
|
||||
if self._read_task and not self._read_task.done():
|
||||
self._read_task.cancel()
|
||||
try:
|
||||
await self._read_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._read_task = None
|
||||
|
||||
# Close WebSocket connection
|
||||
if self._ws and not self._ws.closed:
|
||||
await self._ws.close()
|
||||
self._ws = None
|
||||
|
||||
# Close session
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
self._session = None
|
||||
|
||||
logger.info("WebSocketClient stopped")
|
||||
|
||||
async def send_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Send a JSON-RPC message through the WebSocket.
|
||||
|
||||
Args:
|
||||
message: The JSON-RPC message to send
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no WebSocket connection is active
|
||||
"""
|
||||
if not self._ws or self._ws.closed:
|
||||
raise RuntimeError("No active WebSocket connection")
|
||||
|
||||
async with self._write_lock:
|
||||
try:
|
||||
json_str = message.model_dump_json(exclude_none=True)
|
||||
await self._ws.send_str(json_str)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send message: {e}")
|
||||
raise
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
"""Main loop to read messages from WebSocket."""
|
||||
if not self._ws:
|
||||
logger.error("WebSocket connection not established")
|
||||
return
|
||||
|
||||
logger.debug("Started reading from WebSocket")
|
||||
|
||||
try:
|
||||
async for msg in self._ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
try:
|
||||
message = self._parse_message(msg.data)
|
||||
await self._handle_message(message)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to parse message: {e}, raw data: {msg.data}"
|
||||
)
|
||||
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
try:
|
||||
text = msg.data.decode("utf-8")
|
||||
message = self._parse_message(text)
|
||||
await self._handle_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse binary message: {e}")
|
||||
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
if self._ws:
|
||||
logger.error(f"WebSocket error: {self._ws.exception()}")
|
||||
break
|
||||
|
||||
elif msg.type in (
|
||||
aiohttp.WSMsgType.CLOSE,
|
||||
aiohttp.WSMsgType.CLOSING,
|
||||
aiohttp.WSMsgType.CLOSED,
|
||||
):
|
||||
logger.debug("WebSocket closing")
|
||||
break
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Read loop cancelled")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in read loop: {e}")
|
||||
finally:
|
||||
logger.debug("Stopped reading from WebSocket")
|
||||
|
||||
# Handle reconnection
|
||||
if self._running and self._auto_reconnect:
|
||||
logger.info("Connection lost, attempting to reconnect...")
|
||||
self._reconnect_task = asyncio.create_task(self._reconnect())
|
||||
|
||||
async def _reconnect(self) -> None:
|
||||
"""Attempt to reconnect to the WebSocket server."""
|
||||
while self._running and self._auto_reconnect:
|
||||
try:
|
||||
logger.info(
|
||||
f"Reconnecting to {self._url} in {self._reconnect_interval} seconds..."
|
||||
)
|
||||
await asyncio.sleep(self._reconnect_interval)
|
||||
|
||||
if not self._running:
|
||||
break
|
||||
|
||||
await self._connect()
|
||||
logger.info("Reconnected successfully")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Reconnection failed: {e}")
|
||||
# Continue loop to retry
|
||||
|
||||
def _parse_message(self, data: str) -> JSONRPCMessage:
|
||||
"""Parse a JSON-RPC message from a string.
|
||||
|
||||
Args:
|
||||
data: JSON string to parse
|
||||
|
||||
Returns:
|
||||
Parsed JSONRPCMessage (Request, SuccessResponse, or ErrorResponse)
|
||||
"""
|
||||
obj = json.loads(data)
|
||||
|
||||
# Determine message type based on presence of fields
|
||||
if "method" in obj:
|
||||
return JSONRPCRequest.model_validate(obj)
|
||||
elif "error" in obj:
|
||||
return JSONRPCErrorResponse.model_validate(obj)
|
||||
elif "result" in obj:
|
||||
return JSONRPCSuccessResponse.model_validate(obj)
|
||||
else:
|
||||
raise ValueError(f"Invalid JSON-RPC message: {obj}")
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class _JSONRPCBaseMessage(BaseModel):
|
||||
jsonrpc: Literal["2.0"]
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class JSONRPCRequest(_JSONRPCBaseMessage):
|
||||
id: str | None = None
|
||||
method: str
|
||||
params: dict[str, Any] = Field(default_factory=dict)
|
||||
"""A request that expects a response."""
|
||||
|
||||
|
||||
class _Result(_JSONRPCBaseMessage):
|
||||
id: str | None
|
||||
|
||||
|
||||
class JSONRPCSuccessResponse(_Result):
|
||||
result: dict[str, Any] = Field(default_factory=dict)
|
||||
"""A successful response to a request."""
|
||||
|
||||
|
||||
class JSONRPCErrorData(BaseModel):
|
||||
code: int
|
||||
message: str
|
||||
data: Any | None = None
|
||||
|
||||
|
||||
class JSONRPCErrorResponse(_Result):
|
||||
error: JSONRPCErrorData
|
||||
"""An error response to a request."""
|
||||
|
||||
|
||||
JSONRPCMessage = JSONRPCRequest | JSONRPCSuccessResponse | JSONRPCErrorResponse
|
||||
@@ -0,0 +1,9 @@
|
||||
from .base import JSONRPCServer
|
||||
from .stdio import StdioServer
|
||||
from .websockets import WebSocketServer
|
||||
|
||||
__all__ = [
|
||||
"JSONRPCServer",
|
||||
"StdioServer",
|
||||
"WebSocketServer",
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from ..transport import JSONRPCTransport
|
||||
|
||||
|
||||
class JSONRPCServer(JSONRPCTransport, ABC):
|
||||
"""Base class for JSON-RPC servers.
|
||||
|
||||
Handles pure communication (reading/writing JSON-RPC messages).
|
||||
Server runs in plugin process and receives messages from AstrBot.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from typing import IO, Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ..jsonrpc import (
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCMessage,
|
||||
JSONRPCRequest,
|
||||
JSONRPCSuccessResponse,
|
||||
)
|
||||
from .base import JSONRPCServer
|
||||
|
||||
|
||||
class StdioServer(JSONRPCServer):
|
||||
"""JSON-RPC server using standard input/output for communication.
|
||||
|
||||
This runs in the plugin process and communicates with AstrBot via stdio.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stdin: IO[Any] | None = None,
|
||||
stdout: IO[Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the STDIO server.
|
||||
|
||||
Args:
|
||||
stdin: Input stream to read from (defaults to sys.stdin)
|
||||
stdout: Output stream to write to (defaults to sys.stdout)
|
||||
"""
|
||||
super().__init__()
|
||||
self._stdin = stdin or sys.stdin
|
||||
self._stdout = stdout or sys.stdout
|
||||
self._read_task: asyncio.Task | None = None
|
||||
self._write_lock = asyncio.Lock()
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the server and begin reading from stdin."""
|
||||
if self._running:
|
||||
logger.warning("StdioServer is already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._read_task = asyncio.create_task(self._read_loop())
|
||||
logger.info("StdioServer started")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the server and cleanup resources."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
|
||||
# Cancel read task
|
||||
if self._read_task:
|
||||
self._read_task.cancel()
|
||||
try:
|
||||
await self._read_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._read_task = None
|
||||
|
||||
logger.info("StdioServer stopped")
|
||||
|
||||
async def send_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Send a JSON-RPC message to stdout.
|
||||
|
||||
Args:
|
||||
message: The JSON-RPC message to send
|
||||
"""
|
||||
async with self._write_lock:
|
||||
try:
|
||||
json_str = message.model_dump_json(exclude_none=True)
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None, self._write_line, json_str
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send message: {e}")
|
||||
raise
|
||||
|
||||
def _write_line(self, line: str) -> None:
|
||||
"""Write a line to stdout (synchronous helper)."""
|
||||
self._stdout.write(line + "\n")
|
||||
self._stdout.flush()
|
||||
|
||||
async def _read_loop(self) -> None:
|
||||
"""Main loop to read messages from stdin."""
|
||||
logger.debug("Started reading from stdin")
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
while self._running:
|
||||
# Read line from stdin in executor to avoid blocking
|
||||
line = await loop.run_in_executor(None, self._stdin.readline)
|
||||
|
||||
if not line:
|
||||
# EOF reached
|
||||
logger.info("EOF reached on stdin")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
try:
|
||||
# Parse JSON-RPC message
|
||||
message = self._parse_message(line)
|
||||
await self._handle_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse message: {e}, raw line: {line}")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Read loop cancelled")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in read loop: {e}")
|
||||
finally:
|
||||
logger.debug("Stopped reading from stdin")
|
||||
|
||||
def _parse_message(self, line: str) -> JSONRPCMessage:
|
||||
"""Parse a JSON-RPC message from a string.
|
||||
|
||||
Args:
|
||||
line: JSON string to parse
|
||||
|
||||
Returns:
|
||||
Parsed JSONRPCMessage (Request, SuccessResponse, or ErrorResponse)
|
||||
"""
|
||||
data = json.loads(line)
|
||||
|
||||
# Determine message type based on presence of fields
|
||||
if "method" in data:
|
||||
return JSONRPCRequest.model_validate(data)
|
||||
elif "error" in data:
|
||||
return JSONRPCErrorResponse.model_validate(data)
|
||||
elif "result" in data:
|
||||
return JSONRPCSuccessResponse.model_validate(data)
|
||||
else:
|
||||
raise ValueError(f"Invalid JSON-RPC message: {data}")
|
||||
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
from loguru import logger
|
||||
|
||||
from ..jsonrpc import (
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCMessage,
|
||||
JSONRPCRequest,
|
||||
JSONRPCSuccessResponse,
|
||||
)
|
||||
from .base import JSONRPCServer
|
||||
|
||||
|
||||
class WebSocketServer(JSONRPCServer):
|
||||
"""JSON-RPC server using WebSocket for communication.
|
||||
|
||||
This runs in the plugin process and accepts connections from AstrBot via WebSocket.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host: str = "127.0.0.1",
|
||||
port: int = 0, # 0 means auto-assign
|
||||
path: str = "/",
|
||||
heartbeat: float = 30.0,
|
||||
) -> None:
|
||||
"""Initialize the WebSocket server.
|
||||
|
||||
Args:
|
||||
host: Host to bind to
|
||||
port: Port to bind to (0 for auto-assign)
|
||||
path: WebSocket endpoint path
|
||||
heartbeat: Heartbeat interval in seconds (0 to disable)
|
||||
"""
|
||||
super().__init__()
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._path = path
|
||||
self._heartbeat = heartbeat
|
||||
self._app: web.Application | None = None
|
||||
self._runner: web.AppRunner | None = None
|
||||
self._site: web.TCPSite | None = None
|
||||
self._ws: web.WebSocketResponse | None = None
|
||||
self._write_lock = asyncio.Lock()
|
||||
self._actual_port: int | None = None
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the WebSocket server and begin listening for connections."""
|
||||
if self._running:
|
||||
logger.warning("WebSocketServer is already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._app = web.Application()
|
||||
self._app.router.add_get(self._path, self._handle_websocket)
|
||||
|
||||
self._runner = web.AppRunner(self._app)
|
||||
await self._runner.setup()
|
||||
|
||||
self._site = web.TCPSite(self._runner, self._host, self._port)
|
||||
await self._site.start()
|
||||
|
||||
# Get the actual port (useful when port=0)
|
||||
if self._site._server and hasattr(self._site._server, "sockets"):
|
||||
sockets = getattr(self._site._server, "sockets", None)
|
||||
if sockets:
|
||||
for socket in sockets:
|
||||
self._actual_port = socket.getsockname()[1]
|
||||
break
|
||||
|
||||
logger.info(
|
||||
f"WebSocketServer started on ws://{self._host}:{self._actual_port or self._port}{self._path}"
|
||||
)
|
||||
|
||||
async def _handle_websocket(self, request: web.Request) -> web.WebSocketResponse:
|
||||
"""Handle incoming WebSocket connections.
|
||||
|
||||
Args:
|
||||
request: The aiohttp request object
|
||||
|
||||
Returns:
|
||||
WebSocket response
|
||||
"""
|
||||
ws = web.WebSocketResponse(
|
||||
heartbeat=self._heartbeat if self._heartbeat > 0 else None
|
||||
)
|
||||
await ws.prepare(request)
|
||||
|
||||
# Only allow one connection at a time (typical for plugin IPC)
|
||||
if self._ws and not self._ws.closed:
|
||||
logger.warning(
|
||||
"Rejecting new connection - already have an active connection"
|
||||
)
|
||||
await ws.close(
|
||||
code=1008, message=b"Server already has an active connection"
|
||||
)
|
||||
return ws
|
||||
|
||||
self._ws = ws
|
||||
logger.info(f"WebSocket connection established from {request.remote}")
|
||||
|
||||
try:
|
||||
await self._message_loop(ws)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in WebSocket message loop: {e}")
|
||||
finally:
|
||||
if self._ws == ws:
|
||||
self._ws = None
|
||||
logger.info("WebSocket connection closed")
|
||||
|
||||
return ws
|
||||
|
||||
async def _message_loop(self, ws: web.WebSocketResponse) -> None:
|
||||
"""Main loop to receive messages from WebSocket.
|
||||
|
||||
Args:
|
||||
ws: The WebSocket response object
|
||||
"""
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.TEXT:
|
||||
try:
|
||||
message = self._parse_message(msg.data)
|
||||
await self._handle_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse message: {e}, raw data: {msg.data}")
|
||||
|
||||
elif msg.type == aiohttp.WSMsgType.BINARY:
|
||||
try:
|
||||
text = msg.data.decode("utf-8")
|
||||
message = self._parse_message(text)
|
||||
await self._handle_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse binary message: {e}")
|
||||
|
||||
elif msg.type == aiohttp.WSMsgType.ERROR:
|
||||
logger.error(f"WebSocket error: {ws.exception()}")
|
||||
break
|
||||
|
||||
elif msg.type in (
|
||||
aiohttp.WSMsgType.CLOSE,
|
||||
aiohttp.WSMsgType.CLOSING,
|
||||
aiohttp.WSMsgType.CLOSED,
|
||||
):
|
||||
logger.debug("WebSocket closing")
|
||||
break
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the WebSocket server and cleanup resources."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
|
||||
# Close active WebSocket connection
|
||||
if self._ws and not self._ws.closed:
|
||||
await self._ws.close()
|
||||
self._ws = None
|
||||
|
||||
# Cleanup server
|
||||
if self._site:
|
||||
await self._site.stop()
|
||||
self._site = None
|
||||
|
||||
if self._runner:
|
||||
await self._runner.cleanup()
|
||||
self._runner = None
|
||||
|
||||
self._app = None
|
||||
logger.info("WebSocketServer stopped")
|
||||
|
||||
async def send_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Send a JSON-RPC message through the WebSocket.
|
||||
|
||||
Args:
|
||||
message: The JSON-RPC message to send
|
||||
|
||||
Raises:
|
||||
RuntimeError: If no WebSocket connection is active
|
||||
"""
|
||||
if not self._ws or self._ws.closed:
|
||||
raise RuntimeError("No active WebSocket connection")
|
||||
|
||||
async with self._write_lock:
|
||||
try:
|
||||
json_str = message.model_dump_json(exclude_none=True)
|
||||
await self._ws.send_str(json_str)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send message: {e}")
|
||||
raise
|
||||
|
||||
@property
|
||||
def port(self) -> int | None:
|
||||
"""Get the actual port the server is listening on.
|
||||
|
||||
Returns:
|
||||
Port number, or None if server is not started
|
||||
"""
|
||||
return self._actual_port or self._port
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
"""Get the WebSocket URL the server is listening on.
|
||||
|
||||
Returns:
|
||||
WebSocket URL, or None if server is not started
|
||||
"""
|
||||
if self._actual_port or self._port:
|
||||
port = self._actual_port or self._port
|
||||
return f"ws://{self._host}:{port}{self._path}"
|
||||
return None
|
||||
|
||||
def _parse_message(self, data: str) -> JSONRPCMessage:
|
||||
"""Parse a JSON-RPC message from a string.
|
||||
|
||||
Args:
|
||||
data: JSON string to parse
|
||||
|
||||
Returns:
|
||||
Parsed JSONRPCMessage (Request, SuccessResponse, or ErrorResponse)
|
||||
"""
|
||||
obj = json.loads(data)
|
||||
|
||||
# Determine message type based on presence of fields
|
||||
if "method" in obj:
|
||||
return JSONRPCRequest.model_validate(obj)
|
||||
elif "error" in obj:
|
||||
return JSONRPCErrorResponse.model_validate(obj)
|
||||
elif "result" in obj:
|
||||
return JSONRPCSuccessResponse.model_validate(obj)
|
||||
else:
|
||||
raise ValueError(f"Invalid JSON-RPC message: {obj}")
|
||||
@@ -0,0 +1,48 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Awaitable
|
||||
|
||||
from .jsonrpc import JSONRPCMessage
|
||||
|
||||
MessageHandler = Callable[[JSONRPCMessage], Awaitable[None]]
|
||||
|
||||
|
||||
class JSONRPCTransport(ABC):
|
||||
"""Base class for JSON-RPC transport layers."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._handler: MessageHandler | None = None
|
||||
self._running = False
|
||||
|
||||
def set_message_handler(self, handler: MessageHandler) -> None:
|
||||
"""Set the handler to be called when a message is received.
|
||||
|
||||
Args:
|
||||
handler: Callback function that receives a JSONRPCMessage
|
||||
"""
|
||||
self._message_handler = handler
|
||||
|
||||
@abstractmethod
|
||||
async def start(self) -> None:
|
||||
"""Start the transport layer."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def stop(self) -> None:
|
||||
"""Stop the transport layer and cleanup resources."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def send_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Send a JSON-RPC message.
|
||||
|
||||
Args:
|
||||
message: The JSON-RPC message to send
|
||||
"""
|
||||
pass
|
||||
|
||||
async def _handle_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Internal method to dispatch received messages to the handler."""
|
||||
if self._message_handler:
|
||||
await self._message_handler(message)
|
||||
@@ -0,0 +1,102 @@
|
||||
import yaml
|
||||
import importlib
|
||||
import functools
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
from .stars.registry import star_handlers_registry, star_map, star_registry
|
||||
from ..runtime.api.context import Context
|
||||
from ..api.star.star import StarMetadata
|
||||
|
||||
|
||||
class StarManager:
|
||||
def __init__(self, context: Context) -> None:
|
||||
self.context = context
|
||||
|
||||
def discover_star(self, root_dir: Path | None = None):
|
||||
"""
|
||||
Discover star via plugin.yaml.
|
||||
|
||||
Args:
|
||||
root_dir (Path | None): The root directory to search for plugin.yaml. Defaults to None, which means the current working directory.
|
||||
"""
|
||||
if root_dir is None:
|
||||
root_dir = Path.cwd().relative_to(Path.cwd())
|
||||
else:
|
||||
root_dir = Path.cwd().joinpath(root_dir).resolve()
|
||||
path = root_dir / "plugin.yaml"
|
||||
if not path.exists():
|
||||
logger.warning("No plugin.yaml found in the current directory.")
|
||||
return []
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
data = yaml.safe_load(f)
|
||||
|
||||
# Try to find logo.png
|
||||
logo_path = None
|
||||
if Path(root_dir / "logo.png").exists():
|
||||
logo_path = str(root_dir / "logo.png")
|
||||
|
||||
# Validate required fields
|
||||
star_name = data.get("name")
|
||||
if not star_name:
|
||||
logger.error("Plugin name is required in plugin.yaml.")
|
||||
return []
|
||||
|
||||
# Load components
|
||||
components = data.get("components", [])
|
||||
full_name_list = []
|
||||
for comp in components:
|
||||
class_ = comp.get("class", "")
|
||||
print(f"Loading component: {class_}")
|
||||
if not class_:
|
||||
logger.warning(f"Component without class found: {comp}")
|
||||
continue
|
||||
module_path, class_name = class_.rsplit(":", 1)
|
||||
if not module_path:
|
||||
logger.warning(f"Invalid component without module: {comp}")
|
||||
continue
|
||||
# dynamically register the component
|
||||
try:
|
||||
# we need edit the module path to be relative to the root_dir
|
||||
root_dir_dot = str(root_dir).replace("/", ".").lstrip(".")
|
||||
if root_dir_dot:
|
||||
module_path = f"{root_dir_dot}.{module_path}"
|
||||
module_type = importlib.import_module(module_path)
|
||||
logger.info(f"Successfully loaded component module: {module_path}")
|
||||
component_cls = getattr(module_type, class_name)
|
||||
# Instantiate the component with context
|
||||
ccls = component_cls(self.context)
|
||||
|
||||
# add to full name list
|
||||
for h in star_handlers_registry._handlers:
|
||||
if h.handler_full_name.startswith(f"{class_}."):
|
||||
# bind the instance
|
||||
h.handler = functools.partial(h.handler, ccls)
|
||||
full_name_list.append(h.handler_full_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load component {module_path}: {e}")
|
||||
continue
|
||||
|
||||
# Register the star metadata
|
||||
star_module_path = f"{star_name}.main"
|
||||
star_metadata = StarMetadata(
|
||||
name=data.get("name"),
|
||||
author=data.get("author"),
|
||||
desc=data.get("desc"),
|
||||
version=data.get("version"),
|
||||
repo=data.get("repo"),
|
||||
module_path=star_module_path,
|
||||
root_dir_name=root_dir.name,
|
||||
reserved=False,
|
||||
star_handler_full_names=full_name_list,
|
||||
display_name=data.get("display_name"),
|
||||
logo_path=logo_path,
|
||||
)
|
||||
star_map[star_module_path] = star_metadata
|
||||
star_registry.append(star_metadata)
|
||||
|
||||
logger.info(f"Discovered {len(star_handlers_registry)} star handlers:")
|
||||
for md in star_handlers_registry:
|
||||
logger.info(
|
||||
f" - {md.handler_full_name} with {len(md.event_filters)} filters"
|
||||
)
|
||||
@@ -0,0 +1,156 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
from loguru import logger
|
||||
from .rpc.server.base import JSONRPCServer
|
||||
from .stars.registry import star_map, star_handlers_registry
|
||||
from .rpc.jsonrpc import (
|
||||
JSONRPCMessage,
|
||||
JSONRPCRequest,
|
||||
JSONRPCSuccessResponse,
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCErrorData,
|
||||
)
|
||||
from .types import CallHandlerRequest, HandshakeRequest
|
||||
from ..api.event.astr_message_event import AstrMessageEvent
|
||||
|
||||
|
||||
class StarRunner:
|
||||
def __init__(self, server: JSONRPCServer):
|
||||
self.server = server
|
||||
self._request_id_counter = 0
|
||||
self.pending_requests: dict[str, asyncio.Future] = {}
|
||||
|
||||
def _generate_request_id(self) -> str:
|
||||
self._request_id_counter += 1
|
||||
return str(self._request_id_counter)
|
||||
|
||||
async def _call_rpc(self, message: JSONRPCMessage):
|
||||
if message.id is not None:
|
||||
self.pending_requests[message.id] = asyncio.get_event_loop().create_future()
|
||||
await self.server.send_message(message)
|
||||
if message.id is not None:
|
||||
return await self.pending_requests[message.id]
|
||||
|
||||
async def _handle_messages(self, message: JSONRPCMessage):
|
||||
if isinstance(message, JSONRPCRequest):
|
||||
logger.debug(f"Received RPC request: {message.method}")
|
||||
if message.method == "handshake":
|
||||
payload = {}
|
||||
for star_name, star in star_map.items():
|
||||
payload[star_name] = star.__dict__
|
||||
handlers = []
|
||||
for handler_full_name in star.star_handler_full_names:
|
||||
handler = star_handlers_registry.get_handler_by_full_name(
|
||||
handler_full_name
|
||||
)
|
||||
if handler is None:
|
||||
continue
|
||||
handlers.append(handler.dump_model())
|
||||
payload[star_name]["handlers"] = handlers
|
||||
response = JSONRPCSuccessResponse(
|
||||
jsonrpc="2.0",
|
||||
id=message.id,
|
||||
result=payload,
|
||||
)
|
||||
await self.server.send_message(response)
|
||||
elif message.method == "call_handler":
|
||||
params = CallHandlerRequest.Params.model_validate(message.params)
|
||||
handler_full_name = params.handler_full_name
|
||||
event_model = params.event
|
||||
args = params.args
|
||||
event = event_model.to_event()
|
||||
logger.debug(f"Parsed event: {event}")
|
||||
|
||||
handler = star_handlers_registry.get_handler_by_full_name(
|
||||
handler_full_name
|
||||
)
|
||||
logger.debug(f"Invoking handler: {handler_full_name} with args: {args}")
|
||||
if handler is None:
|
||||
response = JSONRPCErrorResponse(
|
||||
jsonrpc="2.0",
|
||||
id=message.id,
|
||||
error=JSONRPCErrorData(
|
||||
code=-32601,
|
||||
message=f"Handler not found: {handler_full_name}",
|
||||
),
|
||||
)
|
||||
await self.server.send_message(response)
|
||||
else:
|
||||
try:
|
||||
ready_to_call = handler.handler(event, **args)
|
||||
notification = JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
method="handler_stream_start",
|
||||
params={
|
||||
"id": message.id,
|
||||
"handler_full_name": handler_full_name,
|
||||
},
|
||||
)
|
||||
await self.server.send_message(notification)
|
||||
if inspect.iscoroutine(ready_to_call):
|
||||
result = await ready_to_call
|
||||
notification = JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
method="handler_stream_update",
|
||||
params={
|
||||
"id": message.id,
|
||||
"handler_full_name": handler_full_name,
|
||||
"data": result,
|
||||
},
|
||||
)
|
||||
await self.server.send_message(notification)
|
||||
elif inspect.isasyncgen(ready_to_call):
|
||||
try:
|
||||
async for ret in ready_to_call:
|
||||
# Send intermediate results as notifications
|
||||
notification = JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
method="handler_stream_update",
|
||||
params={
|
||||
"id": message.id,
|
||||
"handler_full_name": handler_full_name,
|
||||
"data": ret,
|
||||
},
|
||||
)
|
||||
await self.server.send_message(notification)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error during async generator of handler {handler_full_name}: {e}"
|
||||
)
|
||||
except Exception as e:
|
||||
response = JSONRPCErrorResponse(
|
||||
jsonrpc="2.0",
|
||||
id=message.id,
|
||||
error=JSONRPCErrorData(
|
||||
code=-32000,
|
||||
message=str(e),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
notification = JSONRPCRequest(
|
||||
jsonrpc="2.0",
|
||||
method="handler_stream_end",
|
||||
params={
|
||||
"id": message.id,
|
||||
"handler_full_name": handler_full_name,
|
||||
},
|
||||
)
|
||||
await self.server.send_message(notification)
|
||||
elif isinstance(message, (JSONRPCSuccessResponse, JSONRPCErrorResponse)):
|
||||
if message.id in self.pending_requests:
|
||||
future = self.pending_requests.pop(message.id)
|
||||
if not future.done():
|
||||
future.set_result(message)
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
await self.stop()
|
||||
|
||||
async def run(self):
|
||||
self.server.set_message_handler(handler=self._handle_messages)
|
||||
await self.server.start()
|
||||
|
||||
async def stop(self):
|
||||
await self.server.stop()
|
||||
@@ -0,0 +1,14 @@
|
||||
import abc
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
|
||||
|
||||
class HandlerFilter(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
"""是否应当被过滤"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
__all__ = ["AstrBotConfig", "AstrMessageEvent", "HandlerFilter"]
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import inspect
|
||||
import re
|
||||
import types
|
||||
import typing
|
||||
from typing import Any
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
from ...stars.registry import StarHandlerMetadata
|
||||
from . import HandlerFilter
|
||||
from .custom_filter import CustomFilter
|
||||
|
||||
|
||||
class GreedyStr(str):
|
||||
"""标记指令完成其他参数接收后的所有剩余文本。"""
|
||||
|
||||
|
||||
def unwrap_optional(annotation) -> tuple:
|
||||
"""去掉 Optional[T] / Union[T, None] / T|None,返回 T"""
|
||||
args = typing.get_args(annotation)
|
||||
non_none_args = [a for a in args if a is not type(None)]
|
||||
if len(non_none_args) == 1:
|
||||
return (non_none_args[0],)
|
||||
if len(non_none_args) > 1:
|
||||
return tuple(non_none_args)
|
||||
return ()
|
||||
|
||||
|
||||
# 标准指令受到 wake_prefix 的制约。
|
||||
class CommandFilter(HandlerFilter):
|
||||
"""标准指令过滤器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
command_name: str,
|
||||
alias: set | None = None,
|
||||
handler_md: StarHandlerMetadata | None = None,
|
||||
parent_command_names: list[str] | None = None,
|
||||
):
|
||||
self.command_name = command_name
|
||||
self.alias = alias if alias else set()
|
||||
self.parent_command_names = (
|
||||
parent_command_names if parent_command_names is not None else [""]
|
||||
)
|
||||
if handler_md:
|
||||
self.init_handler_md(handler_md)
|
||||
self.custom_filter_list: list[CustomFilter] = []
|
||||
|
||||
# Cache for complete command names list
|
||||
self._cmpl_cmd_names: list | None = None
|
||||
|
||||
def print_types(self):
|
||||
parts = []
|
||||
for k, v in self.handler_params.items():
|
||||
if isinstance(v, type):
|
||||
parts.append(f"{k}({v.__name__}),")
|
||||
elif isinstance(v, types.UnionType) or typing.get_origin(v) is typing.Union:
|
||||
parts.append(f"{k}({v}),")
|
||||
else:
|
||||
parts.append(f"{k}({type(v).__name__})={v},")
|
||||
result = "".join(parts).rstrip(",")
|
||||
return result
|
||||
|
||||
def init_handler_md(self, handle_md: StarHandlerMetadata):
|
||||
self.handler_md = handle_md
|
||||
signature = inspect.signature(self.handler_md.handler)
|
||||
self.handler_params = {} # 参数名 -> 参数类型,如果有默认值则为默认值
|
||||
idx = 0
|
||||
for k, v in signature.parameters.items():
|
||||
if idx < 2:
|
||||
# 忽略前两个参数,即 self 和 event
|
||||
idx += 1
|
||||
continue
|
||||
if v.default == inspect.Parameter.empty:
|
||||
self.handler_params[k] = v.annotation
|
||||
else:
|
||||
self.handler_params[k] = v.default
|
||||
|
||||
def get_handler_md(self) -> StarHandlerMetadata:
|
||||
return self.handler_md
|
||||
|
||||
def add_custom_filter(self, custom_filter: CustomFilter):
|
||||
self.custom_filter_list.append(custom_filter)
|
||||
|
||||
def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
for custom_filter in self.custom_filter_list:
|
||||
if not custom_filter.filter(event, cfg):
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_and_convert_params(
|
||||
self,
|
||||
params: list[Any],
|
||||
param_type: dict[str, type],
|
||||
) -> dict[str, Any]:
|
||||
"""将参数列表 params 根据 param_type 转换为参数字典。"""
|
||||
result = {}
|
||||
param_items = list(param_type.items())
|
||||
for i, (param_name, param_type_or_default_val) in enumerate(param_items):
|
||||
is_greedy = param_type_or_default_val is GreedyStr
|
||||
|
||||
if is_greedy:
|
||||
# GreedyStr 必须是最后一个参数
|
||||
if i != len(param_items) - 1:
|
||||
raise ValueError(
|
||||
f"参数 '{param_name}' (GreedyStr) 必须是最后一个参数。",
|
||||
)
|
||||
|
||||
# 将剩余的所有部分合并成一个字符串
|
||||
remaining_params = params[i:]
|
||||
result[param_name] = " ".join(remaining_params)
|
||||
break
|
||||
# 没有 GreedyStr 的情况
|
||||
if i >= len(params):
|
||||
if (
|
||||
isinstance(param_type_or_default_val, (type, types.UnionType))
|
||||
or typing.get_origin(param_type_or_default_val) is typing.Union
|
||||
or param_type_or_default_val is inspect.Parameter.empty
|
||||
):
|
||||
# 是类型
|
||||
raise ValueError(
|
||||
f"必要参数缺失。该指令完整参数: {self.print_types()}",
|
||||
)
|
||||
# 是默认值
|
||||
result[param_name] = param_type_or_default_val
|
||||
else:
|
||||
# 尝试强制转换
|
||||
try:
|
||||
if param_type_or_default_val is None:
|
||||
if params[i].isdigit():
|
||||
result[param_name] = int(params[i])
|
||||
else:
|
||||
result[param_name] = params[i]
|
||||
elif isinstance(param_type_or_default_val, str):
|
||||
# 如果 param_type_or_default_val 是字符串,直接赋值
|
||||
result[param_name] = params[i]
|
||||
elif isinstance(param_type_or_default_val, bool):
|
||||
# 处理布尔类型
|
||||
lower_param = str(params[i]).lower()
|
||||
if lower_param in ["true", "yes", "1"]:
|
||||
result[param_name] = True
|
||||
elif lower_param in ["false", "no", "0"]:
|
||||
result[param_name] = False
|
||||
else:
|
||||
raise ValueError(
|
||||
f"参数 {param_name} 必须是布尔值(true/false, yes/no, 1/0)。",
|
||||
)
|
||||
elif isinstance(param_type_or_default_val, int):
|
||||
result[param_name] = int(params[i])
|
||||
elif isinstance(param_type_or_default_val, float):
|
||||
result[param_name] = float(params[i])
|
||||
else:
|
||||
origin = typing.get_origin(param_type_or_default_val)
|
||||
if origin in (typing.Union, types.UnionType):
|
||||
# 注解是联合类型
|
||||
# NOTE: 目前没有处理联合类型嵌套相关的注解写法
|
||||
nn_types = unwrap_optional(param_type_or_default_val)
|
||||
if len(nn_types) == 1:
|
||||
# 只有一个非 NoneType 类型
|
||||
result[param_name] = nn_types[0](params[i])
|
||||
else:
|
||||
# 没有或者有多个非 NoneType 类型,这里我们暂时直接赋值为原始值。
|
||||
# NOTE: 目前还没有做类型校验
|
||||
result[param_name] = params[i]
|
||||
else:
|
||||
result[param_name] = param_type_or_default_val(params[i])
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
f"参数 {param_name} 类型错误。完整参数: {self.print_types()}",
|
||||
)
|
||||
return result
|
||||
|
||||
def get_complete_command_names(self):
|
||||
if self._cmpl_cmd_names is not None:
|
||||
return self._cmpl_cmd_names
|
||||
self._cmpl_cmd_names = [
|
||||
f"{parent} {cmd}" if parent else cmd
|
||||
for cmd in [self.command_name] + list(self.alias)
|
||||
for parent in self.parent_command_names or [""]
|
||||
]
|
||||
return self._cmpl_cmd_names
|
||||
|
||||
def equals(self, message_str: str) -> bool:
|
||||
for full_cmd in self.get_complete_command_names():
|
||||
if message_str == full_cmd:
|
||||
return True
|
||||
return False
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
if not event.is_at_or_wake_command:
|
||||
return False
|
||||
|
||||
if not self.custom_filter_ok(event, cfg):
|
||||
return False
|
||||
|
||||
# 检查是否以指令开头
|
||||
message_str = re.sub(r"\s+", " ", event.get_message_str().strip())
|
||||
ok = False
|
||||
for full_cmd in self.get_complete_command_names():
|
||||
if message_str.startswith(f"{full_cmd} ") or message_str == full_cmd:
|
||||
ok = True
|
||||
message_str = message_str[len(full_cmd) :].strip()
|
||||
if not ok:
|
||||
return False
|
||||
|
||||
# 分割为列表
|
||||
ls = message_str.split(" ")
|
||||
# 去除空字符串
|
||||
ls = [param for param in ls if param]
|
||||
params = {}
|
||||
try:
|
||||
params = self.validate_and_convert_params(ls, self.handler_params)
|
||||
except ValueError as e:
|
||||
raise e
|
||||
|
||||
event.set_extra("parsed_params", params)
|
||||
|
||||
return True
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
from . import HandlerFilter
|
||||
from .command import CommandFilter
|
||||
from .custom_filter import CustomFilter
|
||||
|
||||
|
||||
# 指令组受到 wake_prefix 的制约。
|
||||
class CommandGroupFilter(HandlerFilter):
|
||||
def __init__(
|
||||
self,
|
||||
group_name: str,
|
||||
alias: set | None = None,
|
||||
parent_group: CommandGroupFilter | None = None,
|
||||
):
|
||||
self.group_name = group_name
|
||||
self.alias = alias if alias else set()
|
||||
self.sub_command_filters: list[CommandFilter | CommandGroupFilter] = []
|
||||
self.custom_filter_list: list[CustomFilter] = []
|
||||
self.parent_group = parent_group
|
||||
|
||||
# Cache for complete command names list
|
||||
self._cmpl_cmd_names: list | None = None
|
||||
|
||||
def add_sub_command_filter(
|
||||
self,
|
||||
sub_command_filter: CommandFilter | CommandGroupFilter,
|
||||
):
|
||||
self.sub_command_filters.append(sub_command_filter)
|
||||
|
||||
def add_custom_filter(self, custom_filter: CustomFilter):
|
||||
self.custom_filter_list.append(custom_filter)
|
||||
|
||||
def get_complete_command_names(self) -> list[str]:
|
||||
"""遍历父节点获取完整的指令名。
|
||||
|
||||
新版本 v3.4.29 采用预编译指令,不再从指令组递归遍历子指令,因此这个方法是返回包括别名在内的整个指令名列表。
|
||||
"""
|
||||
if self._cmpl_cmd_names is not None:
|
||||
return self._cmpl_cmd_names
|
||||
|
||||
parent_cmd_names = (
|
||||
self.parent_group.get_complete_command_names() if self.parent_group else []
|
||||
)
|
||||
|
||||
if not parent_cmd_names:
|
||||
# 根节点
|
||||
return [self.group_name] + list(self.alias)
|
||||
|
||||
result = []
|
||||
candidates = [self.group_name] + list(self.alias)
|
||||
for parent_cmd_name in parent_cmd_names:
|
||||
for candidate in candidates:
|
||||
result.append(parent_cmd_name + " " + candidate)
|
||||
self._cmpl_cmd_names = result
|
||||
return result
|
||||
|
||||
# 以树的形式打印出来
|
||||
def print_cmd_tree(
|
||||
self,
|
||||
sub_command_filters: list[CommandFilter | CommandGroupFilter],
|
||||
prefix: str = "",
|
||||
event: AstrMessageEvent | None = None,
|
||||
cfg: AstrBotConfig | None = None,
|
||||
) -> str:
|
||||
parts = []
|
||||
for sub_filter in sub_command_filters:
|
||||
if isinstance(sub_filter, CommandFilter):
|
||||
custom_filter_pass = True
|
||||
if event and cfg:
|
||||
custom_filter_pass = sub_filter.custom_filter_ok(event, cfg)
|
||||
if custom_filter_pass:
|
||||
cmd_th = sub_filter.print_types()
|
||||
line = f"{prefix}├── {sub_filter.command_name}"
|
||||
if cmd_th:
|
||||
line += f" ({cmd_th})"
|
||||
else:
|
||||
line += " (无参数指令)"
|
||||
|
||||
if sub_filter.handler_md and sub_filter.handler_md.desc:
|
||||
line += f": {sub_filter.handler_md.desc}"
|
||||
|
||||
parts.append(line + "\n")
|
||||
elif isinstance(sub_filter, CommandGroupFilter):
|
||||
custom_filter_pass = True
|
||||
if event and cfg:
|
||||
custom_filter_pass = sub_filter.custom_filter_ok(event, cfg)
|
||||
if custom_filter_pass:
|
||||
parts.append(f"{prefix}├── {sub_filter.group_name}\n")
|
||||
parts.append(
|
||||
sub_filter.print_cmd_tree(
|
||||
sub_filter.sub_command_filters,
|
||||
prefix + "│ ",
|
||||
event=event,
|
||||
cfg=cfg,
|
||||
)
|
||||
)
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
for custom_filter in self.custom_filter_list:
|
||||
if not custom_filter.filter(event, cfg):
|
||||
return False
|
||||
return True
|
||||
|
||||
def startswith(self, message_str: str) -> bool:
|
||||
return message_str.startswith(tuple(self.get_complete_command_names()))
|
||||
|
||||
def equals(self, message_str: str) -> bool:
|
||||
return message_str in self.get_complete_command_names()
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
if not event.is_at_or_wake_command:
|
||||
return False
|
||||
|
||||
# 判断当前指令组的自定义过滤器
|
||||
if not self.custom_filter_ok(event, cfg):
|
||||
return False
|
||||
|
||||
if self.equals(event.message_str.strip()):
|
||||
tree = (
|
||||
self.group_name
|
||||
+ "\n"
|
||||
+ self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
|
||||
)
|
||||
raise ValueError(
|
||||
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree,
|
||||
)
|
||||
|
||||
return self.startswith(event.message_str)
|
||||
@@ -0,0 +1,61 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
from . import HandlerFilter
|
||||
|
||||
|
||||
class CustomFilterMeta(ABCMeta):
|
||||
def __and__(cls, other):
|
||||
if not issubclass(other, CustomFilter):
|
||||
raise TypeError("Operands must be subclasses of CustomFilter.")
|
||||
return CustomFilterAnd(cls(), other())
|
||||
|
||||
def __or__(cls, other):
|
||||
if not issubclass(other, CustomFilter):
|
||||
raise TypeError("Operands must be subclasses of CustomFilter.")
|
||||
return CustomFilterOr(cls(), other())
|
||||
|
||||
|
||||
class CustomFilter(HandlerFilter, metaclass=CustomFilterMeta):
|
||||
def __init__(self, raise_error: bool = True, **kwargs):
|
||||
self.raise_error = raise_error
|
||||
|
||||
@abstractmethod
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
"""一个用于重写的自定义Filter"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __or__(self, other):
|
||||
return CustomFilterOr(self, other)
|
||||
|
||||
def __and__(self, other):
|
||||
return CustomFilterAnd(self, other)
|
||||
|
||||
|
||||
class CustomFilterOr(CustomFilter):
|
||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||
super().__init__()
|
||||
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||
raise ValueError(
|
||||
"CustomFilter lass can only operate with other CustomFilter.",
|
||||
)
|
||||
self.filter1 = filter1
|
||||
self.filter2 = filter2
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
return self.filter1.filter(event, cfg) or self.filter2.filter(event, cfg)
|
||||
|
||||
|
||||
class CustomFilterAnd(CustomFilter):
|
||||
def __init__(self, filter1: CustomFilter, filter2: CustomFilter):
|
||||
super().__init__()
|
||||
if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)):
|
||||
raise ValueError(
|
||||
"CustomFilter lass can only operate with other CustomFilter.",
|
||||
)
|
||||
self.filter1 = filter1
|
||||
self.filter2 = filter2
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
return self.filter1.filter(event, cfg) and self.filter2.filter(event, cfg)
|
||||
@@ -0,0 +1,33 @@
|
||||
import enum
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
from ....api.event.message_type import MessageType
|
||||
|
||||
from . import HandlerFilter
|
||||
|
||||
|
||||
class EventMessageType(enum.Flag):
|
||||
GROUP_MESSAGE = enum.auto()
|
||||
PRIVATE_MESSAGE = enum.auto()
|
||||
OTHER_MESSAGE = enum.auto()
|
||||
ALL = GROUP_MESSAGE | PRIVATE_MESSAGE | OTHER_MESSAGE
|
||||
|
||||
|
||||
MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE = {
|
||||
MessageType.GROUP_MESSAGE: EventMessageType.GROUP_MESSAGE,
|
||||
MessageType.FRIEND_MESSAGE: EventMessageType.PRIVATE_MESSAGE,
|
||||
MessageType.OTHER_MESSAGE: EventMessageType.OTHER_MESSAGE,
|
||||
}
|
||||
|
||||
|
||||
class EventMessageTypeFilter(HandlerFilter):
|
||||
def __init__(self, event_message_type: EventMessageType):
|
||||
self.event_message_type = event_message_type
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
message_type = event.get_message_type()
|
||||
if message_type in MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE:
|
||||
event_message_type = MESSAGE_TYPE_2_EVENT_MESSAGE_TYPE[message_type]
|
||||
return bool(event_message_type & self.event_message_type)
|
||||
return False
|
||||
@@ -0,0 +1,29 @@
|
||||
import enum
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
|
||||
from . import HandlerFilter
|
||||
|
||||
|
||||
class PermissionType(enum.Flag):
|
||||
"""权限类型。当选择 MEMBER,ADMIN 也可以通过。"""
|
||||
|
||||
ADMIN = enum.auto()
|
||||
MEMBER = enum.auto()
|
||||
|
||||
|
||||
class PermissionTypeFilter(HandlerFilter):
|
||||
def __init__(self, permission_type: PermissionType, raise_error: bool = True):
|
||||
self.permission_type = permission_type
|
||||
self.raise_error = raise_error
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
"""过滤器"""
|
||||
if self.permission_type == PermissionType.ADMIN:
|
||||
if not event.is_admin():
|
||||
# event.stop_event()
|
||||
# raise ValueError(f"您 (ID: {event.get_sender_id()}) 没有权限操作管理员指令。")
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,71 @@
|
||||
import enum
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
|
||||
from . import HandlerFilter
|
||||
|
||||
|
||||
class PlatformAdapterType(enum.Flag):
|
||||
AIOCQHTTP = enum.auto()
|
||||
QQOFFICIAL = enum.auto()
|
||||
TELEGRAM = enum.auto()
|
||||
WECOM = enum.auto()
|
||||
LARK = enum.auto()
|
||||
WECHATPADPRO = enum.auto()
|
||||
DINGTALK = enum.auto()
|
||||
DISCORD = enum.auto()
|
||||
SLACK = enum.auto()
|
||||
KOOK = enum.auto()
|
||||
VOCECHAT = enum.auto()
|
||||
WEIXIN_OFFICIAL_ACCOUNT = enum.auto()
|
||||
SATORI = enum.auto()
|
||||
MISSKEY = enum.auto()
|
||||
ALL = (
|
||||
AIOCQHTTP
|
||||
| QQOFFICIAL
|
||||
| TELEGRAM
|
||||
| WECOM
|
||||
| LARK
|
||||
| WECHATPADPRO
|
||||
| DINGTALK
|
||||
| DISCORD
|
||||
| SLACK
|
||||
| KOOK
|
||||
| VOCECHAT
|
||||
| WEIXIN_OFFICIAL_ACCOUNT
|
||||
| SATORI
|
||||
| MISSKEY
|
||||
)
|
||||
|
||||
|
||||
ADAPTER_NAME_2_TYPE = {
|
||||
"aiocqhttp": PlatformAdapterType.AIOCQHTTP,
|
||||
"qq_official": PlatformAdapterType.QQOFFICIAL,
|
||||
"telegram": PlatformAdapterType.TELEGRAM,
|
||||
"wecom": PlatformAdapterType.WECOM,
|
||||
"lark": PlatformAdapterType.LARK,
|
||||
"dingtalk": PlatformAdapterType.DINGTALK,
|
||||
"discord": PlatformAdapterType.DISCORD,
|
||||
"slack": PlatformAdapterType.SLACK,
|
||||
"kook": PlatformAdapterType.KOOK,
|
||||
"wechatpadpro": PlatformAdapterType.WECHATPADPRO,
|
||||
"vocechat": PlatformAdapterType.VOCECHAT,
|
||||
"weixin_official_account": PlatformAdapterType.WEIXIN_OFFICIAL_ACCOUNT,
|
||||
"satori": PlatformAdapterType.SATORI,
|
||||
"misskey": PlatformAdapterType.MISSKEY,
|
||||
}
|
||||
|
||||
|
||||
class PlatformAdapterTypeFilter(HandlerFilter):
|
||||
def __init__(self, platform_adapter_type_or_str: PlatformAdapterType | str):
|
||||
if isinstance(platform_adapter_type_or_str, str):
|
||||
self.platform_type = ADAPTER_NAME_2_TYPE.get(platform_adapter_type_or_str)
|
||||
else:
|
||||
self.platform_type = platform_adapter_type_or_str
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
adapter_name = event.get_platform_name()
|
||||
if adapter_name in ADAPTER_NAME_2_TYPE and self.platform_type is not None:
|
||||
return bool(ADAPTER_NAME_2_TYPE[adapter_name] & self.platform_type)
|
||||
return False
|
||||
@@ -0,0 +1,18 @@
|
||||
import re
|
||||
|
||||
from ....api.basic.astrbot_config import AstrBotConfig
|
||||
from ....api.event import AstrMessageEvent
|
||||
|
||||
from . import HandlerFilter
|
||||
|
||||
|
||||
# 正则表达式过滤器不会受到 wake_prefix 的制约。
|
||||
class RegexFilter(HandlerFilter):
|
||||
"""正则表达式过滤器"""
|
||||
|
||||
def __init__(self, regex: str):
|
||||
self.regex_str = regex
|
||||
self.regex = re.compile(regex)
|
||||
|
||||
def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
|
||||
return bool(self.regex.match(event.get_message_str().strip()))
|
||||
@@ -0,0 +1,594 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from ...api.event.astr_message_event import AstrMessageEvent, AstrMessageEventModel
|
||||
from ...api.star.star import StarMetadata
|
||||
from ..stars.registry import EventType, StarHandlerMetadata
|
||||
from ..rpc.jsonrpc import (
|
||||
JSONRPCErrorData,
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCMessage,
|
||||
JSONRPCRequest,
|
||||
JSONRPCSuccessResponse,
|
||||
)
|
||||
from ..types import CallHandlerRequest, HandshakeRequest
|
||||
from ..rpc.client import JSONRPCClient
|
||||
from ..rpc.client.stdio import StdioClient
|
||||
from ..rpc.client.websocket import WebSocketClient
|
||||
from .virtual import VirtualStar
|
||||
|
||||
|
||||
class NewStar(VirtualStar):
|
||||
"""NewStar implementation for isolated plugin runtime.
|
||||
|
||||
NewStar runs plugins in separate processes and communicates via JSON-RPC.
|
||||
This provides better isolation, security, and compatibility.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: JSONRPCClient,
|
||||
) -> None:
|
||||
"""Initialize a NewStar instance.
|
||||
|
||||
Args:
|
||||
client: JSON-RPC client for communication
|
||||
"""
|
||||
self._client = client
|
||||
self._metadata: dict[str, StarMetadata] = {}
|
||||
self._handlers: list[StarHandlerMetadata] = []
|
||||
self._request_id_counter = 0
|
||||
self._pending_requests: dict[
|
||||
str, asyncio.Future[dict] | asyncio.Queue[dict]
|
||||
] = {}
|
||||
self._active = False
|
||||
|
||||
# Set up message handler
|
||||
self._client.set_message_handler(self._handle_message)
|
||||
|
||||
def _generate_request_id(self) -> str:
|
||||
"""Generate a unique request ID."""
|
||||
self._request_id_counter += 1
|
||||
return f"req-{self._request_id_counter}"
|
||||
|
||||
async def _handle_message(self, message: JSONRPCMessage) -> None:
|
||||
"""Handle incoming JSON-RPC messages from the plugin.
|
||||
|
||||
Args:
|
||||
message: The received JSON-RPC message
|
||||
"""
|
||||
if isinstance(message, JSONRPCSuccessResponse) or isinstance(
|
||||
message,
|
||||
JSONRPCErrorResponse,
|
||||
):
|
||||
# This is a response to one of our requests
|
||||
request_id = message.id
|
||||
if request_id and request_id in self._pending_requests:
|
||||
pending = self._pending_requests[request_id]
|
||||
|
||||
# Check if it's a Future or Queue
|
||||
if isinstance(pending, asyncio.Future):
|
||||
self._pending_requests.pop(request_id)
|
||||
if isinstance(message, JSONRPCSuccessResponse):
|
||||
if not pending.done():
|
||||
pending.set_result(message.result)
|
||||
else:
|
||||
if not pending.done():
|
||||
pending.set_exception(
|
||||
RuntimeError(
|
||||
f"RPC Error {message.error.code}: {message.error.message}",
|
||||
),
|
||||
)
|
||||
elif isinstance(pending, asyncio.Queue):
|
||||
if isinstance(message, JSONRPCSuccessResponse):
|
||||
logger.debug(
|
||||
f"Streaming handler {request_id} completed successfully"
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Streaming handler {request_id} failed: {message.error.message}"
|
||||
)
|
||||
# Put error marker in queue
|
||||
await pending.put(
|
||||
{"_error": True, "message": message.error.message}
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Received response for unknown request ID: {request_id}"
|
||||
)
|
||||
|
||||
elif isinstance(message, JSONRPCRequest):
|
||||
# Handle notifications from plugin (streaming events or method calls)
|
||||
if message.method in [
|
||||
"handler_stream_start",
|
||||
"handler_stream_update",
|
||||
"handler_stream_end",
|
||||
]:
|
||||
await self._handle_stream_notification(message)
|
||||
else:
|
||||
# Plugin is calling a method on the core
|
||||
asyncio.create_task(self._handle_plugin_request(message))
|
||||
|
||||
async def _handle_plugin_request(self, request: JSONRPCRequest) -> None:
|
||||
"""Handle a JSON-RPC request from the plugin (plugin calling core methods).
|
||||
|
||||
Args:
|
||||
request: The JSON-RPC request from the plugin
|
||||
"""
|
||||
result: dict = {}
|
||||
try:
|
||||
# Handle core methods that plugins might call
|
||||
# For now, we'll implement basic methods
|
||||
method = request.method
|
||||
params = request.params
|
||||
|
||||
if method == "core.log":
|
||||
# Plugin wants to log something
|
||||
level = params.get("level", "info")
|
||||
message = params.get("message", "")
|
||||
getattr(logger, level.lower())(f"[Plugin] {message}")
|
||||
result = {"success": True}
|
||||
|
||||
elif method == "core.send_message":
|
||||
# Plugin wants to send a message
|
||||
# This would integrate with the platform adapter
|
||||
logger.info(f"Plugin requested to send message: {params}")
|
||||
result = {"success": True, "message_id": "mock-msg-id"}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown method: {method}")
|
||||
|
||||
# Send success response
|
||||
response = JSONRPCSuccessResponse(
|
||||
jsonrpc="2.0",
|
||||
id=request.id,
|
||||
result=result,
|
||||
)
|
||||
await self._client.send_message(response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling plugin request: {e}")
|
||||
# Send error response
|
||||
error_response = JSONRPCErrorResponse(
|
||||
jsonrpc="2.0",
|
||||
id=request.id,
|
||||
error=JSONRPCErrorData(
|
||||
code=-32603,
|
||||
message=str(e),
|
||||
),
|
||||
)
|
||||
await self._client.send_message(error_response)
|
||||
|
||||
async def _handle_stream_notification(self, notification: JSONRPCRequest) -> None:
|
||||
"""Handle streaming notifications from the plugin.
|
||||
|
||||
Args:
|
||||
notification: The streaming notification (handler_stream_start/update/end)
|
||||
"""
|
||||
params = notification.params
|
||||
request_id = params.get("id")
|
||||
|
||||
if not request_id or request_id not in self._pending_requests:
|
||||
logger.warning(
|
||||
f"Received stream notification for unknown request ID: {request_id}"
|
||||
)
|
||||
return
|
||||
|
||||
pending = self._pending_requests.get(request_id)
|
||||
if not isinstance(pending, asyncio.Queue):
|
||||
logger.warning(f"Request {request_id} is not a streaming request")
|
||||
return
|
||||
|
||||
if notification.method == "handler_stream_start":
|
||||
logger.debug(
|
||||
f"Stream started for handler {params.get('handler_full_name')}"
|
||||
)
|
||||
# Optionally put a start marker in the queue
|
||||
# await pending.put({"_stream_start": True})
|
||||
|
||||
elif notification.method == "handler_stream_update":
|
||||
# Put the streamed data into the queue
|
||||
data = params.get("data")
|
||||
logger.debug(f"Stream update for request {request_id}: {data}")
|
||||
if data is not None:
|
||||
await pending.put(data)
|
||||
|
||||
elif notification.method == "handler_stream_end":
|
||||
# Mark the end of the stream
|
||||
logger.debug(f"Stream ended for handler {params.get('handler_full_name')}")
|
||||
# Put a sentinel value to indicate stream end
|
||||
await pending.put({"_stream_end": True})
|
||||
# Clean up the pending request after a short delay to allow queue to be processed
|
||||
asyncio.create_task(self._cleanup_stream_request(request_id))
|
||||
|
||||
async def _cleanup_stream_request(
|
||||
self, request_id: str, delay: float = 1.0
|
||||
) -> None:
|
||||
"""Clean up a streaming request after a delay.
|
||||
|
||||
Args:
|
||||
request_id: The request ID to clean up
|
||||
delay: Delay before cleanup in seconds
|
||||
"""
|
||||
await asyncio.sleep(delay)
|
||||
if request_id in self._pending_requests:
|
||||
self._pending_requests.pop(request_id)
|
||||
logger.debug(f"Cleaned up streaming request {request_id}")
|
||||
|
||||
async def _call_rpc(self, request: JSONRPCRequest) -> dict:
|
||||
"""Call a JSON-RPC method on the plugin and wait for response.
|
||||
|
||||
Args:
|
||||
request: The JSON-RPC request to send
|
||||
|
||||
Returns:
|
||||
The result from the plugin
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the RPC call fails
|
||||
"""
|
||||
# Create a future to wait for the response
|
||||
future: asyncio.Future[dict] = asyncio.Future()
|
||||
|
||||
if request.id is not None:
|
||||
self._pending_requests[request.id] = future
|
||||
|
||||
try:
|
||||
await self._client.send_message(request)
|
||||
# Wait for response with timeout
|
||||
result = await asyncio.wait_for(future, timeout=30.0)
|
||||
return result
|
||||
except asyncio.TimeoutError:
|
||||
if request.id is not None:
|
||||
self._pending_requests.pop(request.id, None)
|
||||
raise RuntimeError(f"RPC call to {request.method} timed out")
|
||||
|
||||
async def _call_rpc_streaming(
|
||||
self,
|
||||
request: JSONRPCRequest,
|
||||
) -> asyncio.Queue[dict]:
|
||||
"""Call a JSON-RPC method on the plugin that returns a stream of results.
|
||||
|
||||
Args:
|
||||
request: The JSON-RPC request to send
|
||||
Returns:
|
||||
An asyncio.Queue that will receive streamed results
|
||||
"""
|
||||
# Create a queue to receive streamed results
|
||||
queue: asyncio.Queue[dict] = asyncio.Queue()
|
||||
|
||||
if request.id is not None:
|
||||
self._pending_requests[request.id] = queue
|
||||
|
||||
try:
|
||||
await self._client.send_message(request)
|
||||
return queue
|
||||
except Exception as e:
|
||||
if request.id is not None:
|
||||
self._pending_requests.pop(request.id, None)
|
||||
raise RuntimeError(f"RPC streaming call to {request.method} failed: {e}")
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Start the plugin process and establish connection."""
|
||||
# Start the client (which may start a subprocess for STDIO)
|
||||
await self._client.start()
|
||||
logger.info("Client started and ready for communication")
|
||||
|
||||
async def handshake(self) -> dict[str, StarMetadata]:
|
||||
"""Perform handshake to retrieve plugin metadata.
|
||||
|
||||
Returns:
|
||||
Plugin metadata including name, version, handlers, etc.
|
||||
"""
|
||||
logger.info("Performing handshake with plugin...")
|
||||
|
||||
result = await self._call_rpc(
|
||||
HandshakeRequest(
|
||||
jsonrpc="2.0", id=self._generate_request_id(), method="handshake"
|
||||
)
|
||||
)
|
||||
|
||||
print(result, result.__class__)
|
||||
|
||||
if isinstance(result, dict):
|
||||
# Parse metadata
|
||||
for star_name, star_info in result.items():
|
||||
handlers_data = star_info.pop("handlers", None)
|
||||
metadata = StarMetadata(**star_info)
|
||||
self._metadata[star_name] = metadata
|
||||
|
||||
# Get handlers
|
||||
self._handlers = []
|
||||
|
||||
for handler_data in handlers_data:
|
||||
handler_meta = StarHandlerMetadata(
|
||||
event_type=EventType(handler_data["event_type"]),
|
||||
handler_full_name=handler_data["handler_full_name"],
|
||||
handler_name=handler_data["handler_name"],
|
||||
handler_module_path=handler_data["handler_module_path"],
|
||||
handler=self._create_handler_proxy(
|
||||
handler_data["handler_full_name"]
|
||||
),
|
||||
event_filters=[],
|
||||
desc=handler_data.get("desc", ""),
|
||||
extras_configs=handler_data.get("extras_configs", {}),
|
||||
)
|
||||
self._handlers.append(handler_meta)
|
||||
|
||||
logger.info(
|
||||
f"Handshake complete: {len(self._metadata)} stars loaded, {self._metadata.keys()}, {len(self._handlers)} handlers registered."
|
||||
)
|
||||
logger.info(f"Registered {len(self._handlers)} handlers")
|
||||
|
||||
return self._metadata
|
||||
raise RuntimeError("Handshake failed: Invalid response from plugin")
|
||||
|
||||
def _create_handler_proxy(self, handler_full_name: str):
|
||||
"""Create a proxy function that calls the handler via RPC.
|
||||
|
||||
Args:
|
||||
handler_full_name: The full name of the handler
|
||||
|
||||
Returns:
|
||||
An async function that proxies calls to the remote handler.
|
||||
The function may return a direct result or an async generator for streaming.
|
||||
"""
|
||||
|
||||
async def handler_proxy(event: AstrMessageEvent, **kwargs):
|
||||
"""Proxy function for remote handler invocation.
|
||||
|
||||
Returns either a direct result or an async generator for streaming handlers.
|
||||
"""
|
||||
request_id = self._generate_request_id()
|
||||
request = CallHandlerRequest(
|
||||
jsonrpc="2.0",
|
||||
id=request_id,
|
||||
method="call_handler",
|
||||
params=CallHandlerRequest.Params(
|
||||
handler_full_name=handler_full_name,
|
||||
event=AstrMessageEventModel.from_event(event),
|
||||
args=kwargs,
|
||||
),
|
||||
)
|
||||
|
||||
# Create a queue for potential streaming response
|
||||
queue: asyncio.Queue[dict] = asyncio.Queue()
|
||||
self._pending_requests[request_id] = queue
|
||||
|
||||
try:
|
||||
# Send the request
|
||||
await self._client.send_message(request)
|
||||
|
||||
# Wait for the first response or stream notification
|
||||
try:
|
||||
# Set a timeout for the first response
|
||||
first_response = await asyncio.wait_for(queue.get(), timeout=30.0)
|
||||
|
||||
# Check what type of response we got
|
||||
if isinstance(first_response, dict):
|
||||
# Check for stream end (empty stream case)
|
||||
if first_response.get("_stream_end"):
|
||||
# Empty stream, return None
|
||||
self._pending_requests.pop(request_id, None)
|
||||
return None
|
||||
|
||||
# Check for error
|
||||
if first_response.get("_error"):
|
||||
self._pending_requests.pop(request_id, None)
|
||||
raise RuntimeError(
|
||||
first_response.get("message", "Unknown error")
|
||||
)
|
||||
|
||||
# Check if this is streaming data or a final result
|
||||
# We peek at the queue to see if more data is coming
|
||||
# If the queue is empty after a short wait, it's a final result
|
||||
try:
|
||||
# Try to get another item with a very short timeout
|
||||
second_response = await asyncio.wait_for(
|
||||
queue.get(), timeout=0.1
|
||||
)
|
||||
# We got a second item, so this is streaming
|
||||
# Create and return the generator
|
||||
return self._create_stream_generator(
|
||||
queue, first_response, second_response
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# No second item, this might be a final result
|
||||
# But we should check if stream_end arrives shortly
|
||||
try:
|
||||
stream_end = await asyncio.wait_for(
|
||||
queue.get(), timeout=0.5
|
||||
)
|
||||
if isinstance(stream_end, dict) and stream_end.get(
|
||||
"_stream_end"
|
||||
):
|
||||
# This was a single-item stream
|
||||
self._pending_requests.pop(request_id, None)
|
||||
return self._deserialize_result(first_response)
|
||||
else:
|
||||
# More data arrived, it's streaming
|
||||
return self._create_stream_generator(
|
||||
queue, first_response, stream_end
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
# Truly a final result (non-streaming)
|
||||
self._pending_requests.pop(request_id, None)
|
||||
return self._deserialize_result(first_response)
|
||||
else:
|
||||
# Unexpected response type
|
||||
self._pending_requests.pop(request_id, None)
|
||||
return self._deserialize_result(first_response)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# Timeout waiting for response
|
||||
self._pending_requests.pop(request_id, None)
|
||||
raise RuntimeError(f"RPC call to {handler_full_name} timed out")
|
||||
|
||||
except Exception:
|
||||
# Clean up on error
|
||||
self._pending_requests.pop(request_id, None)
|
||||
raise
|
||||
|
||||
return handler_proxy
|
||||
|
||||
async def _create_stream_generator(
|
||||
self, queue: asyncio.Queue[dict], *initial_items: dict
|
||||
):
|
||||
"""Create an async generator that yields items from the stream queue.
|
||||
|
||||
Args:
|
||||
queue: The queue containing stream items
|
||||
initial_items: Initial items that were already retrieved from the queue
|
||||
|
||||
Yields:
|
||||
Items from the stream
|
||||
"""
|
||||
# Yield any initial items
|
||||
for item in initial_items:
|
||||
if not (isinstance(item, dict) and item.get("_stream_end")):
|
||||
yield self._deserialize_result(item)
|
||||
|
||||
# Continue yielding items from the queue
|
||||
while True:
|
||||
try:
|
||||
item = await queue.get()
|
||||
|
||||
# Check for end marker
|
||||
if isinstance(item, dict) and item.get("_stream_end"):
|
||||
break
|
||||
|
||||
# Check for error marker
|
||||
if isinstance(item, dict) and item.get("_error"):
|
||||
raise RuntimeError(item.get("message", "Stream error"))
|
||||
|
||||
# Yield the item
|
||||
yield self._deserialize_result(item)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# Generator was cancelled, stop iteration
|
||||
logger.debug("Stream generator cancelled")
|
||||
break
|
||||
|
||||
def _deserialize_result(self, result: Any) -> Any:
|
||||
"""Deserialize result from JSON-RPC response.
|
||||
|
||||
Args:
|
||||
result: The result from the plugin
|
||||
|
||||
Returns:
|
||||
Deserialized result object
|
||||
"""
|
||||
# For now, return as-is
|
||||
# In practice, you might want to reconstruct MessageEventResult etc.
|
||||
return result
|
||||
|
||||
def get_triggered_handlers(
|
||||
self, event: AstrMessageEvent
|
||||
) -> list[StarHandlerMetadata]:
|
||||
"""Get the list of handlers that should be triggered for this event.
|
||||
|
||||
Args:
|
||||
event: The message event
|
||||
|
||||
Returns:
|
||||
List of handler metadata that should handle this event
|
||||
"""
|
||||
# For AdapterMessageEvent, return relevant handlers
|
||||
# This is cached locally, no RPC needed
|
||||
triggered = []
|
||||
|
||||
for handler in self._handlers:
|
||||
if handler.event_type == EventType.AdapterMessageEvent:
|
||||
# In practice, you'd check filters here
|
||||
triggered.append(handler)
|
||||
|
||||
return triggered
|
||||
|
||||
async def call_handler(
|
||||
self,
|
||||
handler: StarHandlerMetadata,
|
||||
event: AstrMessageEvent,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Call a specific handler in the plugin.
|
||||
|
||||
Args:
|
||||
handler: The handler metadata
|
||||
event: The message event
|
||||
*args: Additional positional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
Result from the handler
|
||||
"""
|
||||
logger.debug(f"Calling handler: {handler.handler_name}")
|
||||
|
||||
# Call the handler proxy
|
||||
result = await handler.handler(event, *args, **kwargs)
|
||||
return result
|
||||
|
||||
|
||||
class NewStdioStar(NewStar):
|
||||
"""NewStar implementation using STDIO communication.
|
||||
|
||||
This class automatically starts the plugin subprocess and manages its lifecycle.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plugin_dir: str,
|
||||
python_executable: str = "python",
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a STDIO-based NewStar.
|
||||
|
||||
Args:
|
||||
plugin_dir: Path to the plugin directory
|
||||
python_executable: Python executable to use (defaults to 'python')
|
||||
main_script: Main script filename (defaults to 'main.py')
|
||||
"""
|
||||
# Construct the command to start the plugin
|
||||
if not os.path.exists(plugin_dir):
|
||||
raise FileNotFoundError(f"Plugin directory not found: {plugin_dir}")
|
||||
|
||||
command = [python_executable, "-m", "astrbot_sdk", "run", "--stdio"]
|
||||
|
||||
# Create StdioClient with subprocess management
|
||||
client = StdioClient(command=command, cwd=plugin_dir)
|
||||
super().__init__(client)
|
||||
|
||||
|
||||
class NewWebSocketStar(NewStar):
|
||||
"""NewStar implementation using WebSocket communication.
|
||||
|
||||
Note: WebSocket-based stars do not start the plugin process.
|
||||
The plugin should be started externally and connect to the specified WebSocket URL.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
heartbeat: float = 30.0,
|
||||
reconnect_interval: float = 5.0,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a WebSocket-based NewStar.
|
||||
|
||||
Args:
|
||||
url: WebSocket server URL that the plugin will connect to
|
||||
heartbeat: Heartbeat interval in seconds
|
||||
reconnect_interval: Interval between reconnection attempts in seconds
|
||||
"""
|
||||
client = WebSocketClient(
|
||||
url=url, heartbeat=heartbeat, reconnect_interval=reconnect_interval
|
||||
)
|
||||
super().__init__(client)
|
||||
self._url = url
|
||||
self._heartbeat = heartbeat
|
||||
self._reconnect_interval = reconnect_interval
|
||||
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
import enum
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Generic, TypeVar
|
||||
from ..filter import HandlerFilter
|
||||
from ....api.star.star import StarMetadata
|
||||
from ....api.star.context import Context as BaseContext
|
||||
|
||||
T = TypeVar("T", bound="StarHandlerMetadata")
|
||||
|
||||
|
||||
class EventType(enum.Enum):
|
||||
"""表示一个 AstrBot 内部事件的类型。如适配器消息事件、LLM 请求事件、发送消息前的事件等
|
||||
|
||||
用于对 Handler 的职能分组。
|
||||
"""
|
||||
|
||||
OnAstrBotLoadedEvent = enum.auto()
|
||||
"""AstrBot 加载完成"""
|
||||
OnPlatformLoadedEvent = enum.auto()
|
||||
"""平台适配器加载完成"""
|
||||
AdapterMessageEvent = enum.auto()
|
||||
"""收到适配器消息事件"""
|
||||
OnLLMRequestEvent = enum.auto()
|
||||
"""LLM 请求前"""
|
||||
OnLLMResponseEvent = enum.auto()
|
||||
"""LLM 响应后"""
|
||||
OnDecoratingResultEvent = enum.auto()
|
||||
"""发送消息前"""
|
||||
OnCallingFuncToolEvent = enum.auto()
|
||||
"""调用函数工具前"""
|
||||
OnAfterMessageSentEvent = enum.auto()
|
||||
"""发送消息后"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarHandlerMetadata:
|
||||
"""描述一个 Star 所注册的某一个 Handler。"""
|
||||
|
||||
event_type: EventType
|
||||
"""Handler 的事件类型"""
|
||||
|
||||
handler_full_name: str
|
||||
'''格式为 f"{handler.__module__}_{handler.__name__}"'''
|
||||
|
||||
handler_name: str
|
||||
"""Handler 的名字,也就是方法名"""
|
||||
|
||||
handler_module_path: str
|
||||
"""Handler 所在的模块路径。"""
|
||||
|
||||
handler: Callable[..., Awaitable[Any]]
|
||||
"""Handler 的函数对象,应当是一个异步函数"""
|
||||
|
||||
event_filters: list[HandlerFilter]
|
||||
"""一个适配器消息事件过滤器,用于描述这个 Handler 能够处理、应该处理的适配器消息事件"""
|
||||
|
||||
desc: str = ""
|
||||
"""Handler 的描述信息"""
|
||||
|
||||
extras_configs: dict = field(default_factory=dict)
|
||||
"""插件注册的一些其他的信息, 如 priority 等"""
|
||||
|
||||
def __lt__(self, other: StarHandlerMetadata):
|
||||
"""定义小于运算符以支持优先队列"""
|
||||
return self.extras_configs.get("priority", 0) < other.extras_configs.get(
|
||||
"priority",
|
||||
0,
|
||||
)
|
||||
|
||||
def dump_model(self) -> dict[str, Any]:
|
||||
"""将 Handler 的元数据转换为字典形式,便于序列化。"""
|
||||
p = self.__dict__.copy()
|
||||
p.pop("handler")
|
||||
p.pop("event_filters")
|
||||
return p
|
||||
|
||||
class StarHandlerRegistry(Generic[T]):
|
||||
def __init__(self):
|
||||
self.star_handlers_map: dict[str, StarHandlerMetadata] = {}
|
||||
self._handlers: list[StarHandlerMetadata] = []
|
||||
|
||||
def append(self, handler: StarHandlerMetadata):
|
||||
"""添加一个 Handler,并保持按优先级有序"""
|
||||
if "priority" not in handler.extras_configs:
|
||||
handler.extras_configs["priority"] = 0
|
||||
|
||||
self.star_handlers_map[handler.handler_full_name] = handler
|
||||
self._handlers.append(handler)
|
||||
self._handlers.sort(key=lambda h: -h.extras_configs["priority"])
|
||||
|
||||
def _print_handlers(self):
|
||||
for handler in self._handlers:
|
||||
print(handler.handler_full_name)
|
||||
|
||||
def get_handlers_by_event_type(
|
||||
self,
|
||||
event_type: EventType,
|
||||
only_activated=True,
|
||||
plugins_name: list[str] | None = None,
|
||||
) -> list[StarHandlerMetadata]:
|
||||
handlers = []
|
||||
for handler in self._handlers:
|
||||
# 过滤事件类型
|
||||
if handler.event_type != event_type:
|
||||
continue
|
||||
# 过滤启用状态
|
||||
if only_activated:
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
if not (plugin and plugin.activated):
|
||||
continue
|
||||
# 过滤插件白名单
|
||||
if plugins_name is not None and plugins_name != ["*"]:
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
if not plugin:
|
||||
continue
|
||||
if (
|
||||
plugin.name not in plugins_name
|
||||
and event_type
|
||||
not in (
|
||||
EventType.OnAstrBotLoadedEvent,
|
||||
EventType.OnPlatformLoadedEvent,
|
||||
)
|
||||
and not plugin.reserved
|
||||
):
|
||||
continue
|
||||
handlers.append(handler)
|
||||
return handlers
|
||||
|
||||
def get_handler_by_full_name(self, full_name: str) -> StarHandlerMetadata | None:
|
||||
return self.star_handlers_map.get(full_name, None)
|
||||
|
||||
def get_handlers_by_module_name(
|
||||
self,
|
||||
module_name: str,
|
||||
) -> list[StarHandlerMetadata]:
|
||||
return [
|
||||
handler
|
||||
for handler in self._handlers
|
||||
if handler.handler_module_path == module_name
|
||||
]
|
||||
|
||||
def clear(self):
|
||||
self.star_handlers_map.clear()
|
||||
self._handlers.clear()
|
||||
|
||||
def remove(self, handler: StarHandlerMetadata):
|
||||
self.star_handlers_map.pop(handler.handler_full_name, None)
|
||||
self._handlers = [h for h in self._handlers if h != handler]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._handlers)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._handlers)
|
||||
|
||||
|
||||
class Star:
|
||||
"""所有插件的基类。每一个插件都应当继承自这个类,并实现相应的方法。"""
|
||||
|
||||
def __init__(self, context: BaseContext):
|
||||
self.context = context
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
if not star_map.get(cls.__module__):
|
||||
metadata = StarMetadata(
|
||||
# star_cls_type=cls,
|
||||
module_path=cls.__module__,
|
||||
)
|
||||
star_map[cls.__module__] = metadata
|
||||
star_registry.append(metadata)
|
||||
else:
|
||||
# star_map[cls.__module__].star_cls_type = cls
|
||||
star_map[cls.__module__].module_path = cls.__module__
|
||||
|
||||
|
||||
star_handlers_registry = StarHandlerRegistry() # type: ignore
|
||||
star_map: dict[str, StarMetadata] = {}
|
||||
star_registry: list[StarMetadata] = []
|
||||
@@ -0,0 +1,514 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
# import docstring_parser
|
||||
|
||||
from loguru import logger
|
||||
# from astrbot.core.agent.agent import Agent
|
||||
# from astrbot.core.agent.handoff import HandoffTool
|
||||
# from astrbot.core.agent.hooks import BaseAgentRunHooks
|
||||
# from astrbot.core.agent.tool import FunctionTool
|
||||
# from astrbot.core.astr_agent_context import AstrAgentContext
|
||||
# from astrbot.core.provider.register import llm_tools
|
||||
|
||||
from ..filter.command import CommandFilter
|
||||
from ..filter.command_group import CommandGroupFilter
|
||||
from ..filter.custom_filter import CustomFilterAnd, CustomFilterOr
|
||||
from ..filter.event_message_type import EventMessageType, EventMessageTypeFilter
|
||||
from ..filter.permission import PermissionType, PermissionTypeFilter
|
||||
from ..filter.platform_adapter_type import (
|
||||
PlatformAdapterType,
|
||||
PlatformAdapterTypeFilter,
|
||||
)
|
||||
from ..filter.regex import RegexFilter
|
||||
from ..registry import star_handlers_registry, StarHandlerMetadata, EventType
|
||||
|
||||
def get_handler_full_name(awaitable: Callable[..., Awaitable[Any]]) -> str:
|
||||
"""获取 Handler 的全名"""
|
||||
return f"{awaitable.__module__}:{awaitable.__qualname__}"
|
||||
|
||||
|
||||
def get_handler_or_create(
|
||||
handler: Callable[..., Awaitable[Any]],
|
||||
event_type: EventType,
|
||||
dont_add=False,
|
||||
**kwargs,
|
||||
) -> StarHandlerMetadata:
|
||||
"""获取 Handler 或者创建一个新的 Handler"""
|
||||
handler_full_name = get_handler_full_name(handler)
|
||||
md = star_handlers_registry.get_handler_by_full_name(handler_full_name)
|
||||
if md:
|
||||
return md
|
||||
md = StarHandlerMetadata(
|
||||
event_type=event_type,
|
||||
handler_full_name=handler_full_name,
|
||||
handler_name=handler.__name__,
|
||||
handler_module_path=handler.__module__,
|
||||
handler=handler,
|
||||
event_filters=[],
|
||||
)
|
||||
|
||||
# 插件handler的附加额外信息
|
||||
if handler.__doc__:
|
||||
md.desc = handler.__doc__.strip()
|
||||
if "desc" in kwargs:
|
||||
md.desc = kwargs["desc"]
|
||||
del kwargs["desc"]
|
||||
md.extras_configs = kwargs
|
||||
|
||||
if not dont_add:
|
||||
star_handlers_registry.append(md)
|
||||
return md
|
||||
|
||||
|
||||
def register_command(
|
||||
command_name: str | None = None,
|
||||
sub_command: str | None = None,
|
||||
alias: set | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""注册一个 Command."""
|
||||
new_command = None
|
||||
add_to_event_filters = False
|
||||
if isinstance(command_name, RegisteringCommandable):
|
||||
# 子指令
|
||||
if sub_command is not None:
|
||||
parent_command_names = (
|
||||
command_name.parent_group.get_complete_command_names()
|
||||
)
|
||||
new_command = CommandFilter(
|
||||
sub_command,
|
||||
alias,
|
||||
None,
|
||||
parent_command_names=parent_command_names,
|
||||
)
|
||||
command_name.parent_group.add_sub_command_filter(new_command)
|
||||
else:
|
||||
logger.warning(
|
||||
f"注册指令{command_name} 的子指令时未提供 sub_command 参数。",
|
||||
)
|
||||
# 裸指令
|
||||
elif command_name is None:
|
||||
logger.warning("注册裸指令时未提供 command_name 参数。")
|
||||
else:
|
||||
new_command = CommandFilter(command_name, alias, None)
|
||||
add_to_event_filters = True
|
||||
|
||||
def decorator(awaitable):
|
||||
if not add_to_event_filters:
|
||||
kwargs["sub_command"] = (
|
||||
True # 打一个标记,表示这是一个子指令,再 wakingstage 阶段这个 handler 将会直接被跳过(其父指令会接管)
|
||||
)
|
||||
handler_md = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.AdapterMessageEvent,
|
||||
**kwargs,
|
||||
)
|
||||
if new_command:
|
||||
new_command.init_handler_md(handler_md)
|
||||
handler_md.event_filters.append(new_command)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_custom_filter(custom_type_filter, *args, **kwargs):
|
||||
"""注册一个自定义的 CustomFilter
|
||||
|
||||
Args:
|
||||
custom_type_filter: 在裸指令时为CustomFilter对象
|
||||
在指令组时为父指令的RegisteringCommandable对象,即self或者command_group的返回
|
||||
raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True
|
||||
|
||||
"""
|
||||
add_to_event_filters = False
|
||||
raise_error = True
|
||||
|
||||
# 判断是否是指令组,指令组则添加到指令组的CommandGroupFilter对象中在waking_check的时候一起判断
|
||||
if isinstance(custom_type_filter, RegisteringCommandable):
|
||||
# 子指令, 此时函数为RegisteringCommandable对象的方法,首位参数为RegisteringCommandable对象的self。
|
||||
parent_register_commandable = custom_type_filter
|
||||
custom_filter = args[0]
|
||||
if len(args) > 1:
|
||||
raise_error = args[1]
|
||||
else:
|
||||
# 裸指令
|
||||
add_to_event_filters = True
|
||||
custom_filter = custom_type_filter
|
||||
if args:
|
||||
raise_error = args[0]
|
||||
|
||||
if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)):
|
||||
custom_filter = custom_filter(raise_error)
|
||||
|
||||
def decorator(awaitable):
|
||||
# 裸指令,子指令与指令组的区分,指令组会因为标记跳过wake。
|
||||
if (
|
||||
not add_to_event_filters and isinstance(awaitable, RegisteringCommandable)
|
||||
) or (add_to_event_filters and isinstance(awaitable, RegisteringCommandable)):
|
||||
# 指令组 与 根指令组,添加到本层的grouphandle中一起判断
|
||||
awaitable.parent_group.add_custom_filter(custom_filter)
|
||||
else:
|
||||
handler_md = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.AdapterMessageEvent,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if not add_to_event_filters and not isinstance(
|
||||
awaitable,
|
||||
RegisteringCommandable,
|
||||
):
|
||||
# 底层子指令
|
||||
handle_full_name = get_handler_full_name(awaitable)
|
||||
for (
|
||||
sub_handle
|
||||
) in parent_register_commandable.parent_group.sub_command_filters:
|
||||
# 所有符合fullname一致的子指令handle添加自定义过滤器。
|
||||
# 不确定是否会有多个子指令有一样的fullname,比如一个方法添加多个command装饰器?
|
||||
sub_handle_md = sub_handle.get_handler_md()
|
||||
if (
|
||||
sub_handle_md
|
||||
and sub_handle_md.handler_full_name == handle_full_name
|
||||
):
|
||||
sub_handle.add_custom_filter(custom_filter)
|
||||
|
||||
else:
|
||||
# 裸指令
|
||||
handler_md = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.AdapterMessageEvent,
|
||||
**kwargs,
|
||||
)
|
||||
handler_md.event_filters.append(custom_filter)
|
||||
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_command_group(
|
||||
command_group_name: str | None = None,
|
||||
sub_command: str | None = None,
|
||||
alias: set | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""注册一个 CommandGroup"""
|
||||
new_group = None
|
||||
if isinstance(command_group_name, RegisteringCommandable):
|
||||
# 子指令组
|
||||
if sub_command is None:
|
||||
logger.warning(f"{command_group_name} 指令组的子指令组 sub_command 未指定")
|
||||
else:
|
||||
new_group = CommandGroupFilter(
|
||||
sub_command,
|
||||
alias,
|
||||
parent_group=command_group_name.parent_group,
|
||||
)
|
||||
command_group_name.parent_group.add_sub_command_filter(new_group)
|
||||
# 根指令组
|
||||
elif command_group_name is None:
|
||||
logger.warning("根指令组的名称未指定")
|
||||
else:
|
||||
new_group = CommandGroupFilter(command_group_name, alias)
|
||||
|
||||
def decorator(obj):
|
||||
if new_group:
|
||||
handler_md = get_handler_or_create(
|
||||
obj,
|
||||
EventType.AdapterMessageEvent,
|
||||
**kwargs,
|
||||
)
|
||||
handler_md.event_filters.append(new_group)
|
||||
|
||||
return RegisteringCommandable(new_group)
|
||||
raise ValueError("注册指令组失败。")
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class RegisteringCommandable:
|
||||
"""用于指令组级联注册"""
|
||||
|
||||
group: Callable[..., Callable[..., RegisteringCommandable]] = register_command_group
|
||||
command: Callable[..., Callable[..., None]] = register_command
|
||||
custom_filter: Callable[..., Callable[..., None]] = register_custom_filter
|
||||
|
||||
def __init__(self, parent_group: CommandGroupFilter):
|
||||
self.parent_group = parent_group
|
||||
|
||||
|
||||
def register_event_message_type(event_message_type: EventMessageType, **kwargs):
|
||||
"""注册一个 EventMessageType"""
|
||||
|
||||
def decorator(awaitable):
|
||||
handler_md = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.AdapterMessageEvent,
|
||||
**kwargs,
|
||||
)
|
||||
handler_md.event_filters.append(EventMessageTypeFilter(event_message_type))
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_platform_adapter_type(
|
||||
platform_adapter_type: PlatformAdapterType,
|
||||
**kwargs,
|
||||
):
|
||||
"""注册一个 PlatformAdapterType"""
|
||||
|
||||
def decorator(awaitable):
|
||||
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent)
|
||||
handler_md.event_filters.append(
|
||||
PlatformAdapterTypeFilter(platform_adapter_type),
|
||||
)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_regex(regex: str, **kwargs):
|
||||
"""注册一个 Regex"""
|
||||
|
||||
def decorator(awaitable):
|
||||
handler_md = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.AdapterMessageEvent,
|
||||
**kwargs,
|
||||
)
|
||||
handler_md.event_filters.append(RegexFilter(regex))
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_permission_type(permission_type: PermissionType, raise_error: bool = True):
|
||||
"""注册一个 PermissionType
|
||||
|
||||
Args:
|
||||
permission_type: PermissionType
|
||||
raise_error: 如果没有权限,是否抛出错误到消息平台,并且停止事件传播。默认为 True
|
||||
|
||||
"""
|
||||
|
||||
def decorator(awaitable):
|
||||
handler_md = get_handler_or_create(awaitable, EventType.AdapterMessageEvent)
|
||||
handler_md.event_filters.append(
|
||||
PermissionTypeFilter(permission_type, raise_error),
|
||||
)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_on_astrbot_loaded(**kwargs):
|
||||
"""当 AstrBot 加载完成时"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(awaitable, EventType.OnAstrBotLoadedEvent, **kwargs)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_on_platform_loaded(**kwargs):
|
||||
"""当平台加载完成时"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(awaitable, EventType.OnPlatformLoadedEvent, **kwargs)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_on_llm_request(**kwargs):
|
||||
"""当有 LLM 请求时的事件
|
||||
|
||||
Examples:
|
||||
```py
|
||||
from astrbot.api.provider import ProviderRequest
|
||||
|
||||
@on_llm_request()
|
||||
async def test(self, event: AstrMessageEvent, request: ProviderRequest) -> None:
|
||||
request.system_prompt += "你是一个猫娘..."
|
||||
```
|
||||
|
||||
请务必接收两个参数:event, request
|
||||
|
||||
"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(awaitable, EventType.OnLLMRequestEvent, **kwargs)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_on_llm_response(**kwargs):
|
||||
"""当有 LLM 请求后的事件
|
||||
|
||||
Examples:
|
||||
```py
|
||||
from astrbot.api.provider import LLMResponse
|
||||
|
||||
@on_llm_response()
|
||||
async def test(self, event: AstrMessageEvent, response: LLMResponse) -> None:
|
||||
...
|
||||
```
|
||||
|
||||
请务必接收两个参数:event, request
|
||||
|
||||
"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(awaitable, EventType.OnLLMResponseEvent, **kwargs)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# def register_llm_tool(name: str | None = None, **kwargs):
|
||||
# """为函数调用(function-calling / tools-use)添加工具。
|
||||
|
||||
# 请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会尝试解析该函数注释)
|
||||
|
||||
# ```
|
||||
# @llm_tool(name="get_weather") # 如果 name 不填,将使用函数名
|
||||
# async def get_weather(event: AstrMessageEvent, location: str):
|
||||
# \'\'\'获取天气信息。
|
||||
|
||||
# Args:
|
||||
# location(string): 地点
|
||||
# \'\'\'
|
||||
# # 处理逻辑
|
||||
# ```
|
||||
|
||||
# 可接受的参数类型有:string, number, object, array, boolean。
|
||||
|
||||
# 返回值:
|
||||
# - 返回 str:结果会被加入下一次 LLM 请求的 prompt 中,用于让 LLM 总结工具返回的结果
|
||||
# - 返回 None:结果不会被加入下一次 LLM 请求的 prompt 中。
|
||||
|
||||
# 可以使用 yield 发送消息、终止事件。
|
||||
|
||||
# 发送消息:请参考文档。
|
||||
|
||||
# 终止事件:
|
||||
# ```
|
||||
# event.stop_event()
|
||||
# yield
|
||||
# ```
|
||||
|
||||
# """
|
||||
# name_ = name
|
||||
# registering_agent = None
|
||||
# if kwargs.get("registering_agent"):
|
||||
# registering_agent = kwargs["registering_agent"]
|
||||
|
||||
# def decorator(awaitable: Callable[..., Awaitable[Any]]):
|
||||
# llm_tool_name = name_ if name_ else awaitable.__name__
|
||||
# func_doc = awaitable.__doc__ or ""
|
||||
# docstring = docstring_parser.parse(func_doc)
|
||||
# args = []
|
||||
# for arg in docstring.params:
|
||||
# args.append(
|
||||
# {
|
||||
# "type": arg.type_name,
|
||||
# "name": arg.arg_name,
|
||||
# "description": arg.description,
|
||||
# },
|
||||
# )
|
||||
# # print(llm_tool_name, registering_agent)
|
||||
# if not registering_agent:
|
||||
# doc_desc = docstring.description.strip() if docstring.description else ""
|
||||
# md = get_handler_or_create(awaitable, EventType.OnCallingFuncToolEvent)
|
||||
# llm_tools.add_func(llm_tool_name, args, doc_desc, md.handler)
|
||||
# else:
|
||||
# assert isinstance(registering_agent, RegisteringAgent)
|
||||
# # print(f"Registering tool {llm_tool_name} for agent", registering_agent._agent.name)
|
||||
# if registering_agent._agent.tools is None:
|
||||
# registering_agent._agent.tools = []
|
||||
|
||||
# desc = docstring.description.strip() if docstring.description else ""
|
||||
# tool = llm_tools.spec_to_func(llm_tool_name, args, desc, awaitable)
|
||||
# registering_agent._agent.tools.append(tool)
|
||||
|
||||
# return awaitable
|
||||
|
||||
# return decorator
|
||||
|
||||
|
||||
# class RegisteringAgent:
|
||||
# """用于 Agent 注册"""
|
||||
|
||||
# def llm_tool(self, *args, **kwargs):
|
||||
# kwargs["registering_agent"] = self
|
||||
# return register_llm_tool(*args, **kwargs)
|
||||
|
||||
# def __init__(self, agent: Agent[AstrAgentContext]):
|
||||
# self._agent = agent
|
||||
|
||||
|
||||
# def register_agent(
|
||||
# name: str,
|
||||
# instruction: str,
|
||||
# tools: list[str | FunctionTool] | None = None,
|
||||
# run_hooks: BaseAgentRunHooks[AstrAgentContext] | None = None,
|
||||
# ):
|
||||
# """注册一个 Agent
|
||||
|
||||
# Args:
|
||||
# name: Agent 的名称
|
||||
# instruction: Agent 的指令
|
||||
# tools: Agent 使用的工具列表
|
||||
# run_hooks: Agent 运行时的钩子函数
|
||||
|
||||
# """
|
||||
# tools_ = tools or []
|
||||
|
||||
# def decorator(awaitable: Callable[..., Awaitable[Any]]):
|
||||
# AstrAgent = Agent[AstrAgentContext]
|
||||
# agent = AstrAgent(
|
||||
# name=name,
|
||||
# instructions=instruction,
|
||||
# tools=tools_,
|
||||
# run_hooks=run_hooks or BaseAgentRunHooks[AstrAgentContext](),
|
||||
# )
|
||||
# handoff_tool = HandoffTool(agent=agent)
|
||||
# handoff_tool.handler = awaitable
|
||||
# llm_tools.func_list.append(handoff_tool)
|
||||
# return RegisteringAgent(agent)
|
||||
|
||||
# return decorator
|
||||
|
||||
|
||||
def register_on_decorating_result(**kwargs):
|
||||
"""在发送消息前的事件"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.OnDecoratingResultEvent,
|
||||
**kwargs,
|
||||
)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_after_message_sent(**kwargs):
|
||||
"""在消息发送后的事件"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(
|
||||
awaitable,
|
||||
EventType.OnAfterMessageSentEvent,
|
||||
**kwargs,
|
||||
)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
@@ -0,0 +1,121 @@
|
||||
import typing as T
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from ...api.event.astr_message_event import AstrMessageEvent
|
||||
from ...api.star.star import StarMetadata
|
||||
from .registry import StarHandlerMetadata
|
||||
|
||||
|
||||
class VirtualStar(ABC):
|
||||
"""Abstract base class for virtual plugin implementations.
|
||||
|
||||
VirtualStar defines the interface for plugins that can run in isolated
|
||||
runtime environments (separate processes). It handles the complete lifecycle
|
||||
of a plugin from initialization to shutdown.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def initialize(self) -> None:
|
||||
"""Establish connection and initialize the plugin.
|
||||
|
||||
This method should:
|
||||
- Start the plugin process (if applicable)
|
||||
- Establish communication channels
|
||||
- Wait for the plugin to be ready
|
||||
|
||||
Raises:
|
||||
RuntimeError: If initialization fails
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def handshake(self) -> StarMetadata:
|
||||
"""Perform handshake to retrieve plugin metadata.
|
||||
|
||||
This method should:
|
||||
- Request plugin metadata from the plugin
|
||||
- Cache handler information locally
|
||||
- Validate the plugin's compatibility
|
||||
|
||||
Returns:
|
||||
StarMetadata: Complete plugin metadata including handlers
|
||||
|
||||
Raises:
|
||||
RuntimeError: If handshake fails or times out
|
||||
"""
|
||||
...
|
||||
|
||||
# @abstractmethod
|
||||
# async def turn_on(self) -> None:
|
||||
# """Attach and prepare resources. Only call when the plugin is not active.
|
||||
|
||||
# This method should:
|
||||
# - Activate the plugin
|
||||
# - Initialize any runtime resources
|
||||
# - Prepare the plugin to handle events
|
||||
|
||||
# Raises:
|
||||
# RuntimeError: If activation fails
|
||||
# """
|
||||
# ...
|
||||
|
||||
# @abstractmethod
|
||||
# async def turn_off(self) -> None:
|
||||
# """Detach and clean up resources. Make the plugin inactive.
|
||||
|
||||
# This method should:
|
||||
# - Deactivate the plugin
|
||||
# - Release runtime resources
|
||||
# - Keep the process running but idle
|
||||
|
||||
# Raises:
|
||||
# RuntimeError: If deactivation fails
|
||||
# """
|
||||
# ...
|
||||
|
||||
@abstractmethod
|
||||
def get_triggered_handlers(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
) -> list[StarHandlerMetadata]:
|
||||
"""Get the list of handlers that should be triggered for this event.
|
||||
|
||||
This method uses cached handler metadata to determine which handlers
|
||||
should handle the given event. No RPC calls should be made here.
|
||||
|
||||
Args:
|
||||
event: The message event to check
|
||||
|
||||
Returns:
|
||||
List of handler metadata that match the event
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def call_handler(
|
||||
self,
|
||||
handler: StarHandlerMetadata,
|
||||
event: AstrMessageEvent,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> T.Any:
|
||||
"""Call a registered handler in the plugin.
|
||||
|
||||
This method should:
|
||||
- Serialize the event and arguments
|
||||
- Call the handler via RPC
|
||||
- Wait for and return the result
|
||||
|
||||
Args:
|
||||
handler: The handler metadata
|
||||
event: The message event
|
||||
*args: Additional positional arguments
|
||||
**kwargs: Additional keyword arguments
|
||||
|
||||
Returns:
|
||||
The result from the handler
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the handler call fails or times out
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,37 @@
|
||||
from .galaxy import Galaxy
|
||||
from ..api.event import AstrMessageEvent
|
||||
from ..api.event.astrbot_message import AstrBotMessage, MessageMember
|
||||
from ..api.platform.platform_metadata import PlatformMetadata
|
||||
from ..api.event.message_type import MessageType
|
||||
|
||||
async def amain():
|
||||
galaxy = Galaxy()
|
||||
star = await galaxy.connect_to_websocket_star(
|
||||
"hello",
|
||||
{
|
||||
"url": "ws://127.0.0.1:8765",
|
||||
},
|
||||
)
|
||||
print("Connected to websocket star 'hello'")
|
||||
md = await star.handshake()
|
||||
print(f"Handshake metadata: {md}")
|
||||
|
||||
abm = AstrBotMessage()
|
||||
abm.type = MessageType.FRIEND_MESSAGE
|
||||
abm.self_id = "astrbot_123"
|
||||
abm.session_id = "test_session"
|
||||
abm.message_id = "msg_001"
|
||||
abm.message_str = "hello"
|
||||
abm.sender = MessageMember(user_id="user_123", nickname="User123") # Simplified for this example
|
||||
abm.group = None
|
||||
abm.message = []
|
||||
abm.raw_message = {}
|
||||
event = AstrMessageEvent(
|
||||
message_str=abm.message_str,
|
||||
message_obj=abm,
|
||||
platform_meta=PlatformMetadata(
|
||||
name="fake", description="Fake Platform", id="fake_1"
|
||||
),
|
||||
session_id="test_session",
|
||||
)
|
||||
await star.call_handler(star._handlers[0], event)
|
||||
@@ -0,0 +1,34 @@
|
||||
import asyncio
|
||||
import signal
|
||||
from .rpc.server import WebSocketServer
|
||||
from .star_runner import StarRunner
|
||||
from .star_manager import StarManager
|
||||
from ..runtime.api.context import Context
|
||||
from ..runtime.api.conversation_mgr import ConversationManager
|
||||
|
||||
|
||||
async def amain():
|
||||
server = WebSocketServer(port=8765)
|
||||
conversation_manager = ConversationManager()
|
||||
context = Context(conversation_manager=conversation_manager)
|
||||
runner = StarRunner(server)
|
||||
context._inject_rpc_handlers(runner=runner)
|
||||
star_manager = StarManager(context=context)
|
||||
star_manager.discover_star()
|
||||
await runner.run()
|
||||
|
||||
# 设置停止事件
|
||||
stop_event = asyncio.Event()
|
||||
|
||||
# 注册信号处理器
|
||||
loop = asyncio.get_running_loop()
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
loop.add_signal_handler(sig, stop_event.set)
|
||||
|
||||
print("Server is running. Press Ctrl+C to stop.")
|
||||
|
||||
try:
|
||||
await stop_event.wait()
|
||||
finally:
|
||||
print("Shutting down...")
|
||||
await server.stop()
|
||||
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
from .rpc.jsonrpc import JSONRPCRequest
|
||||
from typing import Any, Literal, Type
|
||||
from ..api.event.astr_message_event import AstrMessageEvent, AstrMessageEventModel
|
||||
|
||||
|
||||
# class StarType(enum.Enum):
|
||||
# LEGACY = "legacy"
|
||||
# STDIO = "stdio"
|
||||
# WEBSOCKET = "websocket"
|
||||
|
||||
|
||||
# class StarURI(BaseModel):
|
||||
# star_type: StarType
|
||||
# namespace: str
|
||||
# plugin_name: str
|
||||
|
||||
# def __str__(self):
|
||||
# return f"astrbot://{self.star_type.value}/{self.namespace}/{self.plugin_name}"
|
||||
|
||||
# @classmethod
|
||||
# def from_str(cls, uri_str: str) -> StarURI:
|
||||
# """Parse a StarURI from a string."""
|
||||
# try:
|
||||
# prefix, rest = uri_str.split("://", 1)
|
||||
# star_type_str, namespace, plugin_name = rest.split("/", 2)
|
||||
# star_type = StarType(star_type_str)
|
||||
# return cls(
|
||||
# star_type=star_type,
|
||||
# namespace=namespace,
|
||||
# plugin_name=plugin_name,
|
||||
# )
|
||||
# except Exception as e:
|
||||
# raise ValueError(f"Invalid StarURI format: {uri_str}") from e
|
||||
|
||||
# def is_new_star(self) -> bool:
|
||||
# """Determine if the Star is a new-style Star (stdio or websocket)."""
|
||||
# return self.star_type in {StarType.STDIO, StarType.WEBSOCKET}
|
||||
|
||||
|
||||
class HandshakeRequest(JSONRPCRequest):
|
||||
class Params(BaseModel):
|
||||
pass
|
||||
|
||||
method: Literal["handshake"]
|
||||
params: Params = Field(default_factory=Params)
|
||||
|
||||
|
||||
class CallHandlerRequest(JSONRPCRequest):
|
||||
class Params(BaseModel):
|
||||
handler_full_name: str
|
||||
event: AstrMessageEventModel
|
||||
args: dict[str, Any] = {}
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_event_data(cls: Type[CallHandlerRequest.Params], data: Any) -> Any:
|
||||
if isinstance(data, dict):
|
||||
event_data = data.get("event")
|
||||
if isinstance(event_data, dict):
|
||||
data["event"] = AstrMessageEventModel.model_validate(event_data)
|
||||
return data
|
||||
|
||||
method: Literal["call_handler"]
|
||||
params: Params | dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class CallContextFunctionRequest(JSONRPCRequest):
|
||||
class Params(BaseModel):
|
||||
name: str
|
||||
args: dict[str, Any] = {}
|
||||
|
||||
method: Literal["call_context_function"]
|
||||
params: Params | dict = Field(default_factory=dict)
|
||||
@@ -0,0 +1,81 @@
|
||||
import aiohttp
|
||||
import certifi
|
||||
import ssl
|
||||
import time
|
||||
from loguru import logger
|
||||
|
||||
|
||||
async def download_file(url: str, path: str, show_progress: bool = False):
|
||||
"""从指定 url 下载文件到指定路径 path"""
|
||||
try:
|
||||
ssl_context = ssl.create_default_context(
|
||||
cafile=certifi.where(),
|
||||
) # 使用 certifi 提供的 CA 证书
|
||||
connector = aiohttp.TCPConnector(ssl=ssl_context)
|
||||
async with aiohttp.ClientSession(
|
||||
trust_env=True,
|
||||
connector=connector,
|
||||
) as session:
|
||||
async with session.get(url, timeout=1800) as resp:
|
||||
if resp.status != 200:
|
||||
raise Exception(f"下载文件失败: {resp.status}")
|
||||
total_size = int(resp.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
start_time = time.time()
|
||||
if show_progress:
|
||||
print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}")
|
||||
with open(path, "wb") as f:
|
||||
while True:
|
||||
chunk = await resp.content.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
if show_progress:
|
||||
elapsed_time = (
|
||||
time.time() - start_time
|
||||
if time.time() - start_time > 0
|
||||
else 1
|
||||
)
|
||||
speed = downloaded_size / 1024 / elapsed_time # KB/s
|
||||
print(
|
||||
f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s",
|
||||
end="",
|
||||
)
|
||||
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
||||
# 关闭SSL验证(仅在证书验证失败时作为fallback)
|
||||
logger.warning(
|
||||
"SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。"
|
||||
)
|
||||
logger.warning(
|
||||
f"SSL certificate verification failed for {url}. "
|
||||
"Falling back to unverified connection (CERT_NONE). "
|
||||
"This is insecure and exposes the application to man-in-the-middle attacks. "
|
||||
"Please investigate certificate issues with the remote server."
|
||||
)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, ssl=ssl_context, timeout=120) as resp:
|
||||
total_size = int(resp.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
start_time = time.time()
|
||||
if show_progress:
|
||||
print(f"文件大小: {total_size / 1024:.2f} KB | 文件地址: {url}")
|
||||
with open(path, "wb") as f:
|
||||
while True:
|
||||
chunk = await resp.content.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
if show_progress:
|
||||
elapsed_time = time.time() - start_time
|
||||
speed = downloaded_size / 1024 / elapsed_time # KB/s
|
||||
print(
|
||||
f"\r下载进度: {downloaded_size / total_size:.2%} 速度: {speed:.2f} KB/s",
|
||||
end="",
|
||||
)
|
||||
if show_progress:
|
||||
print()
|
||||
@@ -0,0 +1,12 @@
|
||||
from astrbot_sdk.api.components.command import CommandComponent
|
||||
from astrbot_sdk.api.event import AstrMessageEvent, filter
|
||||
from astrbot_sdk.api.star.context import Context
|
||||
|
||||
|
||||
class HelloCommand(CommandComponent):
|
||||
def __init__(self, context: Context):
|
||||
self.context = context
|
||||
|
||||
@filter.command("hello")
|
||||
async def hello(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("Hello, Astrbot!")
|
||||
@@ -0,0 +1,12 @@
|
||||
from astrbot_sdk.api.components. import ListenerComponent
|
||||
from astrbot.api.v1.event import AstrMessageEvent, filter
|
||||
from astrbot.api.v1.context import Context
|
||||
|
||||
|
||||
class EchoListener(ListenerComponent):
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
@filter.platform_adapter_type(filter.PlatformAdapterType.ALL)
|
||||
async def on_message(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("Hello, Astrbot!")
|
||||
@@ -0,0 +1,31 @@
|
||||
from mcp.types import CallToolResult
|
||||
from pydantic import Field
|
||||
from pydantic.dataclasses import dataclass
|
||||
|
||||
from astrbot.api.v1.components.agent.tool import FunctionTool
|
||||
from astrbot.api.v1.components.agent import ContextWrapper, AstrAgentContext
|
||||
|
||||
|
||||
@dataclass
|
||||
class HelloWorldTool(FunctionTool):
|
||||
name: str = "hello_world" # 工具名称
|
||||
description: str = "Say hello to the world." # 工具描述
|
||||
parameters: dict = Field(
|
||||
default_factory=lambda: {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"greeting": {
|
||||
"type": "string",
|
||||
"description": "The greeting message.",
|
||||
},
|
||||
},
|
||||
"required": ["greeting"],
|
||||
}
|
||||
) # 工具参数定义,见 OpenAI 官网或 https://json-schema.org/understanding-json-schema/
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[AstrAgentContext], **kwargs
|
||||
) -> str | CallToolResult:
|
||||
# event 在 context.context.event 中可用
|
||||
greeting = kwargs.get("greeting", "Hello")
|
||||
return f"{greeting}, World!" # 也支持 mcp.types.CallToolResult 类型
|
||||
@@ -0,0 +1,22 @@
|
||||
_schema_version: 2
|
||||
name: astrbot_plugin_helloworld
|
||||
display_name: HelloWorld 插件
|
||||
desc: 一个简单的问候插件示例
|
||||
author: Soulter
|
||||
version: 0.1.0
|
||||
components: # 组件列表,将支持自动生成
|
||||
- class: handlers.commands.hello:HelloCommand
|
||||
type: command
|
||||
name: hello
|
||||
description: 发送问候消息
|
||||
subcommands:
|
||||
- name: wow
|
||||
description: 发送 "Hello, Astrbot!" 消息
|
||||
# - class: handlers.tools.echo:EchoTool
|
||||
# type: llm_tool
|
||||
# name: echo_tool
|
||||
# description: 回显输入的消息
|
||||
# - class: handlers.listeners.echo:EchoListener
|
||||
# type: listener
|
||||
# name: message_logger
|
||||
# description: 监听并记录所有消息
|
||||
@@ -0,0 +1,6 @@
|
||||
from astrbot_sdk.runtime.start_server import amain
|
||||
|
||||
import asyncio
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(amain())
|
||||
@@ -0,0 +1,6 @@
|
||||
from astrbot_sdk.runtime.start_client import amain
|
||||
|
||||
import asyncio
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(amain())
|
||||
@@ -0,0 +1,720 @@
|
||||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.13.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
{ name = "aiosignal" },
|
||||
{ name = "attrs" },
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "frozenlist" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "astrbot-sdk"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "certifi" },
|
||||
{ name = "docstring-parser" },
|
||||
{ name = "loguru" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiohttp", specifier = ">=3.13.2" },
|
||||
{ name = "certifi", specifier = ">=2025.10.5" },
|
||||
{ name = "docstring-parser", specifier = ">=0.17.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "pydantic", specifier = ">=2.12.3" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.22.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 },
|
||||
]
|
||||
Reference in New Issue
Block a user