aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
author简律纯 <i@jyunko.cn>2025-10-25 00:30:48 +0800
committer简律纯 <i@jyunko.cn>2025-10-25 00:30:48 +0800
commitcbc653ffd0ea9abf4360623dc7a7651e1a49cc61 (patch)
treeea3c396148158077bae3e77eaa9341f8c1990636
parent08299b37dfda86e56e4f2b442f68ccd2da7a82e3 (diff)
downloadconventional_role_play-cbc653ffd0ea9abf4360623dc7a7651e1a49cc61.tar.gz
conventional_role_play-cbc653ffd0ea9abf4360623dc7a7651e1a49cc61.zip
feat: Implement plugin system with combat tracker and dice analyzer
- Added `plugin_system_demo.py` to demonstrate basic plugin usage, processing, and analysis. - Created `CombatTrackerPlugin` for tracking combat statistics including damage and healing. - Developed `DiceAnalyzerPlugin` for analyzing dice rolls and calculating success rates. - Introduced `renderer_demo.py` for rendering output in HTML, Markdown, and JSON formats. - Implemented `rule_system_demo.py` to showcase rule engine capabilities with various examples. - Established core rule engine functionality in `rules.py` with support for conditions and actions. - Enhanced base plugin structure in `base.py` to support different plugin types (Processor, Renderer, Analyzer). - Added custom exception handling in `exceptions.py` for better error management. - Configured logging setup in `logging_config.py` for improved logging capabilities. - Created unit tests in `test_rust_core.py` to validate core functionalities and performance.
-rw-r--r--Cargo.toml6
-rw-r--r--docs/source/advanced_usage.md457
-rw-r--r--docs/source/index.rst8
-rw-r--r--docs/source/plugin_guide.md564
-rw-r--r--examples/README.md190
-rw-r--r--examples/basic_usage.py58
-rw-r--r--examples/custom_plugin.py70
-rw-r--r--examples/plugin_system_demo.py173
-rw-r--r--examples/plugins/combat_tracker_plugin.py96
-rw-r--r--examples/plugins/dice_analyzer_plugin.py90
-rw-r--r--examples/renderer_demo.py147
-rw-r--r--examples/rule_system_demo.py210
-rw-r--r--src/conventionalrp/__init__.py36
-rw-r--r--src/conventionalrp/__main__.py9
-rw-r--r--src/conventionalrp/_core.pyi238
-rw-r--r--src/conventionalrp/base.py0
-rw-r--r--src/conventionalrp/core/__init__.py9
-rw-r--r--src/conventionalrp/core/processor.py150
-rw-r--r--src/conventionalrp/core/rules.py238
-rw-r--r--src/conventionalrp/extractors/__init__.py3
-rw-r--r--src/conventionalrp/extractors/rule_extractor.py25
-rw-r--r--src/conventionalrp/html_renderer.py0
-rw-r--r--src/conventionalrp/json_renderer.py0
-rw-r--r--src/conventionalrp/markdown_renderer.py0
-rw-r--r--src/conventionalrp/plugins/__init__.py20
-rw-r--r--src/conventionalrp/plugins/base.py103
-rw-r--r--src/conventionalrp/plugins/plugin_manager.py209
-rw-r--r--src/conventionalrp/renderers/__init__.py3
-rw-r--r--src/conventionalrp/renderers/html_renderer.py433
-rw-r--r--src/conventionalrp/renderers/json_renderer.py77
-rw-r--r--src/conventionalrp/renderers/markdown_renderer.py176
-rw-r--r--src/conventionalrp/tokenizer.py0
-rw-r--r--src/conventionalrp/utils/__init__.py33
-rw-r--r--src/conventionalrp/utils/exceptions.py104
-rw-r--r--src/conventionalrp/utils/logging_config.py77
-rw-r--r--src/conventionalrp/utils/text_processing.py0
-rw-r--r--src/lib.rs252
-rw-r--r--tests/README.md63
-rw-r--r--tests/run_tests.py12
-rw-r--r--tests/test_parser.py20
-rw-r--r--tests/test_processor.py22
-rw-r--r--tests/test_pyo3.py4
-rw-r--r--tests/test_renderers.py25
-rw-r--r--tests/test_rule_extractor.py17
-rw-r--r--tests/test_rust_core.py99
-rw-r--r--uv.lock1027
46 files changed, 3885 insertions, 1668 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 5996a4e..859d3e7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "conventionalrp"
-version = "0.2.3"
+version = "1.1.3"
edition = "2021"
description = "HydroRoll Conventional Role Play SDK"
license = "AGPLv3.0"
@@ -9,7 +9,6 @@ homepage = "https://crp.hydroroll.team/"
repository = "https://github.com/HydroRoll-Team/conventional_role_play"
authors = ["HsiangNianian"]
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "_core"
crate-type = ["cdylib"]
@@ -17,3 +16,6 @@ crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.27.0", features = ["abi3-py39"] }
time = { version = "0.3.39", features = ["formatting", "macros", "local-offset"] }
+regex = "1.10"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
diff --git a/docs/source/advanced_usage.md b/docs/source/advanced_usage.md
new file mode 100644
index 0000000..7c0ca14
--- /dev/null
+++ b/docs/source/advanced_usage.md
@@ -0,0 +1,457 @@
+# 高级使用指南
+
+## 目录
+
+1. [插件系统](#插件系统)
+2. [规则引擎](#规则引擎)
+3. [自定义渲染器](#自定义渲染器)
+4. [性能优化](#性能优化)
+5. [错误处理](#错误处理)
+
+---
+
+## 插件系统
+
+ConventionalRP 提供了强大的插件系统,允许您扩展和定制框架的功能。
+
+### 插件类型
+
+#### 1. 解析器插件 (ParserPlugin)
+
+用于扩展文本解析功能:
+
+```python
+from conventionalrp.plugins import ParserPlugin
+
+class CustomParserPlugin(ParserPlugin):
+ def __init__(self):
+ super().__init__("CustomParser", "1.0.0")
+ self.priority = 60 # 优先级
+
+ def initialize(self, config=None):
+ self.config = config or {}
+
+ def can_parse(self, text: str) -> bool:
+ # 判断是否可以解析
+ return text.startswith("custom:")
+
+ def parse(self, text: str) -> List[Dict[str, Any]]:
+ # 解析文本
+ return [{"type": "custom", "content": text}]
+```
+
+#### 2. 处理器插件 (ProcessorPlugin)
+
+用于数据处理和转换:
+
+```python
+from conventionalrp.plugins import ProcessorPlugin
+
+class CombatTrackerPlugin(ProcessorPlugin):
+ def __init__(self):
+ super().__init__("CombatTracker", "1.0.0")
+ self.total_damage = 0
+
+ def initialize(self, config=None):
+ self.config = config or {}
+
+ def process_token(self, token: Dict[str, Any]) -> Dict[str, Any]:
+ # 处理单个 token
+ content = token.get("content", "")
+
+ # 检测伤害
+ import re
+ damage_match = re.search(r'(\d+)\s*点?伤害', content)
+ if damage_match:
+ damage = int(damage_match.group(1))
+ self.total_damage += damage
+ token["combat_data"] = {
+ "type": "damage",
+ "amount": damage,
+ "total": self.total_damage
+ }
+
+ return token
+```
+
+#### 3. 渲染器插件 (RendererPlugin)
+
+用于自定义输出格式:
+
+```python
+from conventionalrp.plugins import RendererPlugin
+
+class PlainTextRenderer(RendererPlugin):
+ def __init__(self):
+ super().__init__("PlainText", "1.0.0")
+
+ def initialize(self, config=None):
+ self.config = config or {}
+
+ def render(self, data: Any) -> str:
+ # 渲染为纯文本
+ lines = []
+ for item in data:
+ speaker = item.get("speaker", "")
+ content = item.get("content", "")
+ if speaker:
+ lines.append(f"{speaker}: {content}")
+ else:
+ lines.append(content)
+ return "\n".join(lines)
+```
+
+#### 4. 分析器插件 (AnalyzerPlugin)
+
+用于数据分析和统计:
+
+```python
+from conventionalrp.plugins import AnalyzerPlugin
+
+class SessionAnalyzerPlugin(AnalyzerPlugin):
+ def __init__(self):
+ super().__init__("SessionAnalyzer", "1.0.0")
+
+ def initialize(self, config=None):
+ self.config = config or {}
+
+ def analyze(self, data: Any) -> Dict[str, Any]:
+ # 分析游戏数据
+ stats = {
+ "total_dialogues": 0,
+ "total_dice_rolls": 0,
+ "active_characters": set(),
+ }
+
+ for token in data:
+ token_type = token.get("type", "")
+ if token_type == "dialogue":
+ stats["total_dialogues"] += 1
+ elif token_type == "dice":
+ stats["total_dice_rolls"] += 1
+
+ speaker = token.get("speaker", "")
+ if speaker:
+ stats["active_characters"].add(speaker)
+
+ stats["active_characters"] = list(stats["active_characters"])
+ return stats
+```
+
+### 使用插件管理器
+
+```python
+from conventionalrp.plugins import PluginManager
+
+# 创建管理器
+manager = PluginManager(plugin_dirs=["./my_plugins"])
+
+# 发现插件
+discovered = manager.discover_plugins()
+print(f"发现 {len(discovered)} 个插件")
+
+# 注册插件
+my_plugin = MyCustomPlugin()
+my_plugin.initialize()
+manager.register_plugin(my_plugin)
+
+# 执行插件
+result = manager.execute_plugins(data, plugin_type=ProcessorPlugin)
+
+# 启用/禁用插件
+manager.disable_plugin("MyPlugin")
+manager.enable_plugin("MyPlugin")
+
+# 获取统计信息
+stats = manager.get_statistics()
+print(stats)
+```
+
+---
+
+## 规则引擎
+
+规则引擎允许您定义灵活的数据处理规则。
+
+### 规则定义
+
+```python
+from conventionalrp.core.rules import Rule, RuleEngine
+
+# 创建规则
+rule = Rule(
+ name="detect_critical_hit",
+ conditions={
+ "type": {"equals": "dice"},
+ "content": {"matches": r"d20=20"}
+ },
+ actions=[
+ {"action": "add_tag", "tag": "critical_hit"},
+ {"action": "set_field", "field": "is_critical", "value": True}
+ ],
+ priority=100
+)
+```
+
+### 条件运算符
+
+- `equals`: 精确匹配
+- `not_equals`: 不等于
+- `contains`: 包含子字符串
+- `not_contains`: 不包含
+- `matches`: 正则表达式匹配
+- `in`: 值在列表中
+- `not_in`: 值不在列表中
+- `greater_than`: 大于
+- `less_than`: 小于
+
+### 动作类型
+
+- `set_field`: 设置字段值
+- `add_tag`: 添加标签
+- `remove_field`: 删除字段
+- `transform`: 自定义转换函数
+
+### 使用规则引擎
+
+```python
+# 创建引擎
+engine = RuleEngine()
+
+# 添加规则
+engine.add_rule(rule)
+
+# 应用规则
+tokens = [
+ {"type": "dice", "content": "d20=20"},
+ {"type": "dialogue", "content": "我投了大成功!"}
+]
+
+processed_tokens = engine.apply_rules(tokens)
+
+# 移除规则
+engine.remove_rule("detect_critical_hit")
+```
+
+### 高级示例:动态规则
+
+```python
+def create_damage_rule(min_damage: int):
+ """创建伤害检测规则"""
+ return Rule(
+ name=f"detect_damage_{min_damage}",
+ conditions={
+ "content": {"matches": rf"(\d+)点伤害"},
+ # 可以添加自定义验证
+ },
+ actions=[
+ {"action": "set_field", "field": "damage_detected", "value": True},
+ {"action": "transform", "function": lambda token: {
+ **token,
+ "damage_level": "high" if int(re.search(r'(\d+)', token["content"]).group(1)) > min_damage else "normal"
+ }}
+ ]
+ )
+
+# 使用
+engine.add_rule(create_damage_rule(20))
+```
+
+---
+
+## 自定义渲染器
+
+### 继承 BaseRenderer
+
+```python
+from conventionalrp.renderers.base import BaseRenderer
+
+class LaTeXRenderer(BaseRenderer):
+ def __init__(self):
+ super().__init__()
+
+ def render(self, data: List[Dict[str, Any]]) -> str:
+ latex = r"\documentclass{article}" + "\n"
+ latex += r"\begin{document}" + "\n"
+
+ for item in data:
+ token_type = item.get("type", "")
+ content = item.get("content", "")
+
+ if token_type == "dialogue":
+ speaker = item.get("speaker", "")
+ latex += rf"\textbf{{{speaker}:}} {content}\\" + "\n"
+ else:
+ latex += f"{content}\\\\\n"
+
+ latex += r"\end{document}" + "\n"
+ return latex
+```
+
+### 使用主题系统
+
+```python
+from conventionalrp.renderers.html_renderer import HTMLRenderer
+
+# 使用内置主题
+renderer = HTMLRenderer(theme="dark")
+
+# 使用自定义 CSS
+custom_css = """
+body {
+ background: linear-gradient(to bottom, #2c3e50, #3498db);
+}
+.token {
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
+}
+"""
+
+renderer = HTMLRenderer(theme="light", custom_css=custom_css)
+output = renderer.render(data)
+```
+
+---
+
+## 性能优化
+
+### 1. 使用 Rust 核心
+
+编译 Rust 模块获得更好的性能:
+
+```bash
+maturin develop --uv --release
+```
+
+### 2. 批量处理
+
+```python
+# 不推荐:逐个处理
+for token in tokens:
+ processor.process(token)
+
+# 推荐:批量处理
+processed_tokens = processor.process_batch(tokens)
+```
+
+### 3. 规则优化
+
+```python
+# 设置合适的优先级
+Rule(name="fast_rule", priority=100, ...) # 先执行
+Rule(name="slow_rule", priority=10, ...) # 后执行
+
+# 预编译正则表达式
+import re
+pattern = re.compile(r'd\d+') # 编译一次
+rule = Rule(
+ conditions={"content": {"matches": pattern}},
+ ...
+)
+```
+
+### 4. 缓存结果
+
+```python
+from functools import lru_cache
+
+@lru_cache(maxsize=128)
+def expensive_operation(data: str) -> str:
+ # 昂贵的计算
+ return result
+```
+
+### 性能基准测试
+
+```bash
+# 运行基准测试
+python benchmarks/benchmark_parser.py
+python benchmarks/benchmark_rules.py
+```
+
+---
+
+## 错误处理
+
+### 自定义异常
+
+```python
+from conventionalrp.utils.exceptions import (
+ ParsingError,
+ RuleError,
+ PluginError
+)
+
+try:
+ parser.parse_log("invalid.txt")
+except ParsingError as e:
+ print(f"解析错误: {e}")
+except FileNotFoundError as e:
+ print(f"文件未找到: {e}")
+```
+
+### 日志配置
+
+```python
+from conventionalrp.utils.logging_config import setup_logging
+import logging
+
+# 设置日志级别
+setup_logging(level=logging.DEBUG)
+
+# 在您的代码中使用
+logger = logging.getLogger(__name__)
+logger.debug("调试信息")
+logger.info("普通信息")
+logger.warning("警告")
+logger.error("错误")
+```
+
+### 最佳实践
+
+1. **始终验证输入**
+ ```python
+ def process_token(token: Dict[str, Any]) -> Dict[str, Any]:
+ if not isinstance(token, dict):
+ raise TypeError("Token must be a dictionary")
+
+ if "type" not in token:
+ raise ValueError("Token must have a 'type' field")
+
+ return processed_token
+ ```
+
+2. **使用类型提示**
+ ```python
+ from typing import List, Dict, Any, Optional
+
+ def parse_data(text: str) -> List[Dict[str, Any]]:
+ ...
+ ```
+
+3. **提供清晰的错误消息**
+ ```python
+ if not file_path.exists():
+ raise FileNotFoundError(
+ f"Log file not found: {file_path}\n"
+ f"Please check the file path and try again."
+ )
+ ```
+
+---
+
+## 完整示例
+
+查看 `examples/` 目录获取更多示例:
+
+- `basic_usage.py` - 基本使用
+- `rule_system_demo.py` - 规则系统演示
+- `plugin_system_demo.py` - 插件系统演示
+- `renderer_demo.py` - 渲染器演示
+
+---
+
+## 下一步
+
+- 阅读 [API 文档](api.md)
+- 查看 [插件开发指南](plugin_guide.md)
+- 参与 [GitHub 讨论](https://github.com/HydroRoll-Team/conventional_role_play/discussions)
diff --git a/docs/source/index.rst b/docs/source/index.rst
index e69de29..e3b529f 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -0,0 +1,8 @@
+.. toctree::
+ :maxdepth: 2
+ :caption: 内容目录
+
+ usage <usage.md>
+ advanced_usage <advanced_usage.md>
+ plugin_guide <plugin_guide.md>
+ api <api.md> \ No newline at end of file
diff --git a/docs/source/plugin_guide.md b/docs/source/plugin_guide.md
new file mode 100644
index 0000000..7c146fe
--- /dev/null
+++ b/docs/source/plugin_guide.md
@@ -0,0 +1,564 @@
+# 插件开发指南
+
+本指南将帮助您创建自己的 ConventionalRP 插件。
+
+## 目录
+
+1. [插件基础](#插件基础)
+2. [插件类型](#插件类型)
+3. [开发流程](#开发流程)
+4. [最佳实践](#最佳实践)
+5. [发布插件](#发布插件)
+
+---
+
+## 插件基础
+
+### 什么是插件?
+
+插件是扩展 ConventionalRP 功能的独立模块。每个插件继承自基础 `Plugin` 类,并实现特定的接口。
+
+### 插件生命周期
+
+1. **发现** - PluginManager 扫描插件目录
+2. **加载** - 导入插件模块
+3. **初始化** - 调用 `initialize()` 方法
+4. **启用** - 调用 `on_enable()` 钩子
+5. **执行** - 调用 `process()` 方法处理数据
+6. **禁用** - 调用 `on_disable()` 钩子
+
+### 基本结构
+
+```python
+from conventionalrp.plugins import Plugin
+from typing import Any, Dict, Optional
+
+class MyPlugin(Plugin):
+ def __init__(self):
+ super().__init__(name="MyPlugin", version="1.0.0")
+ # 初始化插件状态
+ self.data = {}
+
+ def initialize(self, config: Optional[Dict[str, Any]] = None):
+ """初始化插件,加载配置"""
+ self.config = config or {}
+ self.logger.info(f"{self.name} initialized with config: {self.config}")
+
+ def process(self, data: Any) -> Any:
+ """处理数据"""
+ self.logger.debug(f"Processing data: {data}")
+ # 实现您的处理逻辑
+ return data
+
+ def on_enable(self):
+ """插件启用时调用"""
+ super().on_enable()
+ # 执行启用逻辑(如打开文件、建立连接等)
+
+ def on_disable(self):
+ """插件禁用时调用"""
+ # 执行清理逻辑(如关闭文件、断开连接等)
+ super().on_disable()
+```
+
+---
+
+## 插件类型
+
+### 1. 解析器插件 (ParserPlugin)
+
+解析器插件用于识别和解析特定格式的文本。
+
+#### 接口
+
+```python
+from conventionalrp.plugins import ParserPlugin
+
+class MyParserPlugin(ParserPlugin):
+ def can_parse(self, text: str) -> bool:
+ """判断是否可以解析给定文本"""
+ raise NotImplementedError
+
+ def parse(self, text: str) -> List[Dict[str, Any]]:
+ """解析文本并返回结果"""
+ raise NotImplementedError
+```
+
+#### 示例:CoC 规则解析器
+
+```python
+import re
+from conventionalrp.plugins import ParserPlugin
+
+class CoCParserPlugin(ParserPlugin):
+ """Call of Cthulhu 规则解析器"""
+
+ def __init__(self):
+ super().__init__("CoCParser", "1.0.0")
+ self.priority = 60
+
+ # CoC 特有的模式
+ self.skill_check_pattern = re.compile(
+ r'\.(?:rc|san|sc)\s+(.+)',
+ re.IGNORECASE
+ )
+ self.sanity_pattern = re.compile(
+ r'(\d+)/(\d+d\d+)',
+ re.IGNORECASE
+ )
+
+ def initialize(self, config=None):
+ self.config = config or {}
+
+ def can_parse(self, text: str) -> bool:
+ """检查是否是 CoC 指令"""
+ return bool(self.skill_check_pattern.match(text))
+
+ def parse(self, text: str) -> List[Dict[str, Any]]:
+ """解析 CoC 指令"""
+ results = []
+
+ match = self.skill_check_pattern.match(text)
+ if match:
+ command_text = match.group(1)
+
+ # 解析 SAN 检定
+ san_match = self.sanity_pattern.search(command_text)
+ if san_match:
+ results.append({
+ "type": "sanity_check",
+ "success_loss": san_match.group(1),
+ "failure_loss": san_match.group(2),
+ "content": text,
+ })
+ else:
+ # 普通技能检定
+ results.append({
+ "type": "skill_check",
+ "skill": command_text.strip(),
+ "content": text,
+ })
+
+ return results
+```
+
+### 2. 处理器插件 (ProcessorPlugin)
+
+处理器插件用于转换和增强数据。
+
+#### 示例:经验值追踪器
+
+```python
+from conventionalrp.plugins import ProcessorPlugin
+import re
+
+class ExperienceTrackerPlugin(ProcessorPlugin):
+ """经验值追踪插件"""
+
+ def __init__(self):
+ super().__init__("ExperienceTracker", "1.0.0")
+ self.character_exp = {}
+ self.exp_pattern = re.compile(r'获得\s*(\d+)\s*点?经验', re.IGNORECASE)
+
+ def initialize(self, config=None):
+ self.config = config or {}
+ # 加载初始经验值
+ if "initial_exp" in self.config:
+ self.character_exp = self.config["initial_exp"].copy()
+
+ def process_token(self, token: Dict[str, Any]) -> Dict[str, Any]:
+ """处理 token 并追踪经验值"""
+ content = token.get("content", "")
+ speaker = token.get("speaker", "")
+
+ # 检测经验值获取
+ match = self.exp_pattern.search(content)
+ if match and speaker:
+ exp_gained = int(match.group(1))
+
+ if speaker not in self.character_exp:
+ self.character_exp[speaker] = 0
+
+ self.character_exp[speaker] += exp_gained
+
+ # 添加经验值信息到 token
+ token["exp_data"] = {
+ "gained": exp_gained,
+ "total": self.character_exp[speaker],
+ "character": speaker,
+ }
+
+ self.logger.info(f"{speaker} 获得 {exp_gained} 经验,总计 {self.character_exp[speaker]}")
+
+ return token
+
+ def get_leaderboard(self) -> List[Dict[str, Any]]:
+ """获取经验值排行榜"""
+ sorted_chars = sorted(
+ self.character_exp.items(),
+ key=lambda x: x[1],
+ reverse=True
+ )
+ return [
+ {"character": char, "exp": exp}
+ for char, exp in sorted_chars
+ ]
+```
+
+### 3. 渲染器插件 (RendererPlugin)
+
+渲染器插件用于生成特定格式的输出。
+
+#### 示例:Discord Markdown 渲染器
+
+```python
+from conventionalrp.plugins import RendererPlugin
+
+class DiscordRenderer(RendererPlugin):
+ """Discord Markdown 渲染器"""
+
+ def __init__(self):
+ super().__init__("DiscordRenderer", "1.0.0")
+
+ def initialize(self, config=None):
+ self.config = config or {}
+ self.use_embeds = self.config.get("use_embeds", False)
+
+ def render(self, data: Any) -> str:
+ """渲染为 Discord 格式"""
+ output = []
+
+ for token in data:
+ token_type = token.get("type", "unknown")
+ speaker = token.get("speaker", "")
+ content = token.get("content", "")
+
+ if token_type == "dialogue":
+ # 使用 Discord 的粗体和引用
+ output.append(f"**{speaker}:** {content}")
+
+ elif token_type == "dice":
+ # 使用代码块高亮骰子结果
+ result = token.get("result", "?")
+ output.append(f"`{content}` → **{result}**")
+
+ elif token_type == "narration":
+ # 使用斜体表示旁白
+ output.append(f"*{content}*")
+
+ elif token_type == "system":
+ # 使用代码块表示系统消息
+ output.append(f"```\n{content}\n```")
+
+ else:
+ output.append(content)
+
+ return "\n\n".join(output)
+```
+
+### 4. 分析器插件 (AnalyzerPlugin)
+
+分析器插件用于统计和分析数据。
+
+#### 示例:对话分析器
+
+```python
+from conventionalrp.plugins import AnalyzerPlugin
+from collections import Counter
+
+class DialogueAnalyzerPlugin(AnalyzerPlugin):
+ """对话分析插件"""
+
+ def __init__(self):
+ super().__init__("DialogueAnalyzer", "1.0.0")
+
+ def initialize(self, config=None):
+ self.config = config or {}
+
+ def analyze(self, data: Any) -> Dict[str, Any]:
+ """分析对话数据"""
+ dialogue_count = Counter()
+ word_count = Counter()
+ total_words = 0
+
+ for token in data:
+ if token.get("type") == "dialogue":
+ speaker = token.get("speaker", "Unknown")
+ content = token.get("content", "")
+
+ # 统计对话次数
+ dialogue_count[speaker] += 1
+
+ # 统计词数
+ words = len(content.split())
+ word_count[speaker] += words
+ total_words += words
+
+ # 计算参与度
+ most_active = dialogue_count.most_common(1)
+ most_talkative = word_count.most_common(1)
+
+ return {
+ "total_dialogues": sum(dialogue_count.values()),
+ "total_words": total_words,
+ "dialogue_count": dict(dialogue_count),
+ "word_count": dict(word_count),
+ "most_active_character": most_active[0][0] if most_active else None,
+ "most_talkative_character": most_talkative[0][0] if most_talkative else None,
+ "average_words_per_dialogue": total_words / sum(dialogue_count.values()) if dialogue_count else 0,
+ }
+```
+
+---
+
+## 开发流程
+
+### 1. 创建插件项目
+
+```bash
+mkdir my_plugin
+cd my_plugin
+touch my_plugin.py
+touch config.json
+touch README.md
+```
+
+### 2. 实现插件
+
+```python
+# my_plugin.py
+from conventionalrp.plugins import ProcessorPlugin
+
+class MyPlugin(ProcessorPlugin):
+ # ... 实现代码
+ pass
+```
+
+### 3. 添加配置
+
+```json
+{
+ "name": "MyPlugin",
+ "version": "1.0.0",
+ "description": "My custom plugin",
+ "author": "Your Name",
+ "settings": {
+ "enable_feature_x": true,
+ "threshold": 10
+ }
+}
+```
+
+### 4. 测试插件
+
+```python
+# test_my_plugin.py
+import unittest
+from my_plugin import MyPlugin
+
+class TestMyPlugin(unittest.TestCase):
+ def setUp(self):
+ self.plugin = MyPlugin()
+ self.plugin.initialize()
+
+ def test_process(self):
+ test_data = [{"type": "test", "content": "test"}]
+ result = self.plugin.process(test_data)
+ self.assertIsNotNone(result)
+
+ def test_enable_disable(self):
+ self.assertTrue(self.plugin.enabled)
+ self.plugin.on_disable()
+ self.assertFalse(self.plugin.enabled)
+
+if __name__ == "__main__":
+ unittest.main()
+```
+
+### 5. 集成测试
+
+```python
+# integration_test.py
+from conventionalrp.plugins import PluginManager
+from my_plugin import MyPlugin
+
+manager = PluginManager()
+plugin = MyPlugin()
+plugin.initialize({"test": True})
+manager.register_plugin(plugin)
+
+# 测试数据
+test_data = [...]
+
+# 执行插件
+result = manager.execute_plugins(test_data)
+print(result)
+```
+
+---
+
+## 最佳实践
+
+### 1. 命名约定
+
+- 插件类名使用 PascalCase,如 `MyCustomPlugin`
+- 文件名使用 snake_case,如 `my_custom_plugin.py`
+- 配置键使用 snake_case
+
+### 2. 日志记录
+
+```python
+def process_token(self, token):
+ self.logger.debug(f"Processing token: {token['type']}")
+
+ try:
+ result = self._do_processing(token)
+ self.logger.info("Processing successful")
+ return result
+ except Exception as e:
+ self.logger.error(f"Processing failed: {e}")
+ raise
+```
+
+### 3. 错误处理
+
+```python
+from conventionalrp.utils.exceptions import PluginError
+
+def initialize(self, config=None):
+ try:
+ self.config = config or {}
+ # 验证必需的配置
+ if "required_key" not in self.config:
+ raise PluginError(
+ f"{self.name}: Missing required configuration 'required_key'"
+ )
+ except Exception as e:
+ self.logger.error(f"Initialization failed: {e}")
+ raise
+```
+
+### 4. 性能优化
+
+```python
+# 使用缓存
+from functools import lru_cache
+
+@lru_cache(maxsize=128)
+def expensive_operation(self, key: str) -> Any:
+ # 昂贵的计算
+ return result
+
+# 批量处理
+def process(self, data: List[Dict]) -> List[Dict]:
+ # 一次性处理所有数据,而不是逐个处理
+ return [self.process_token(token) for token in data]
+```
+
+### 5. 文档注释
+
+```python
+class MyPlugin(Plugin):
+ """
+ 我的自定义插件
+
+ 这个插件用于...
+
+ Attributes:
+ setting_x: 设置 X 的说明
+ setting_y: 设置 Y 的说明
+
+ Example:
+ >>> plugin = MyPlugin()
+ >>> plugin.initialize({"setting_x": True})
+ >>> result = plugin.process(data)
+ """
+
+ def process_token(self, token: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ 处理单个 token
+
+ Args:
+ token: 输入 token
+
+ Returns:
+ 处理后的 token
+
+ Raises:
+ PluginError: 如果处理失败
+ """
+ pass
+```
+
+---
+
+## 发布插件
+
+### 1. 准备文件
+
+创建以下文件:
+- `README.md` - 插件说明
+- `LICENSE` - 开源许可证
+- `requirements.txt` - 依赖列表
+- `setup.py` 或 `pyproject.toml` - 打包配置
+
+### 2. 打包
+
+```bash
+# 使用 setuptools
+python setup.py sdist bdist_wheel
+
+# 或使用 poetry
+poetry build
+```
+
+### 3. 发布
+
+```bash
+# 发布到 PyPI
+twine upload dist/*
+
+# 或发布到 GitHub
+git tag v1.0.0
+git push origin v1.0.0
+```
+
+### 4. 文档
+
+在 README.md 中包含:
+- 插件描述
+- 安装方法
+- 使用示例
+- 配置选项
+- API 文档
+- 许可证信息
+
+---
+
+## 示例项目结构
+
+```
+my_plugin/
+├── README.md
+├── LICENSE
+├── requirements.txt
+├── setup.py
+├── my_plugin/
+│ ├── __init__.py
+│ ├── plugin.py
+│ └── utils.py
+├── tests/
+│ ├── test_plugin.py
+│ └── test_utils.py
+└── examples/
+ └── example_usage.py
+```
+
+---
+
+## 相关资源
+
+- [插件系统演示](../../examples/plugin_system_demo.py)
+- [示例插件](../../examples/plugins/)
+- [API 文档](api.md)
+- [高级使用指南](advanced_usage.md)
diff --git a/examples/README.md b/examples/README.md
deleted file mode 100644
index d823f5b..0000000
--- a/examples/README.md
+++ /dev/null
@@ -1,190 +0,0 @@
-# ConventionalRP 示例
-
-本目录包含 ConventionalRP SDK 的使用示例。
-
-## 目录结构
-
-```
-examples/
-├── basic_usage.py # 基础使用示例
-├── custom_plugin.py # 自定义插件示例
-├── rules/ # 规则文件
-│ └── dnd5e_rules.json5 # D&D 5E 解析规则
-├── logs/ # 示例日志文件
-│ ├── sample_session.txt # 完整会话日志
-│ └── combat_log.txt # 战斗日志
-└── output/ # 输出文件目录(自动生成)
-```
-
-## 快速开始
-
-### 1. 基础使用示例
-
-演示如何解析 TRPG 日志并以多种格式输出:
-
-```bash
-cd examples
-python basic_usage.py
-```
-
-**输出:**
-- `output/session_output.json` - JSON 格式
-- `output/session_output.html` - HTML 格式
-- `output/session_output.md` - Markdown 格式
-
-### 2. 自定义插件示例
-
-演示如何创建自定义插件进行数据分析:
-
-```bash
-python custom_plugin.py
-```
-
-**功能:**
-- 骰子统计分析
-- 对话提取
-- 角色行为分析
-
-## 规则文件格式
-
-规则文件使用 JSON5 格式(支持注释和尾随逗号):
-
-```json5
-{
- metadata: [{
- type: "metadata",
- patterns: ["正则表达式"],
- groups: ["字段名"],
- priority: 100
- }],
-
- content: [{
- type: "内容类型",
- match_type: "匹配模式", // enclosed, prefix, suffix
- patterns: ["正则表达式"],
- groups: ["提取字段"],
- priority: 90
- }]
-}
-```
-
-### 匹配模式说明
-
-- **enclosed**: 封闭匹配(如 `**动作**`、`「对话」`)
-- **prefix**: 前缀匹配(如 `[系统]消息`)
-- **suffix**: 后缀匹配(文本结尾)
-
-### 优先级
-
-- 数字越大优先级越高
-- 建议范围:1-100
-- 元数据通常设置为最高优先级(100)
-
-## 日志文件格式
-
-标准 TRPG 日志格式:
-
-```
-[时间戳] <角色名> 内容
-```
-
-**示例:**
-
-```
-[2025-10-24 14:30:01] <艾莉娅> 「我要检查这扇门」
-[2025-10-24 14:30:05] <DiceBot> 检定结果: [d20 = 18]
-[2025-10-24 14:30:10] <DM> 你发现了陷阱
-```
-
-## 自定义规则
-
-你可以为不同的游戏系统创建自定义规则:
-
-### D&D 5E
-
-已提供 `rules/dnd5e_rules.json5`
-
-### 其他系统
-
-创建新的规则文件,参考 D&D 5E 规则的结构:
-
-```bash
-cp rules/dnd5e_rules.json5 rules/my_system_rules.json5
-# 然后编辑 my_system_rules.json5
-```
-
-## 创建自定义插件
-
-插件是用于扩展功能的 Python 类:
-
-```python
-class MyPlugin:
- def __init__(self):
- self.name = "My Plugin"
-
- def process(self, parsed_data):
- # 你的处理逻辑
- return result
-```
-
-查看 `custom_plugin.py` 了解完整示例。
-
-## 常见模式
-
-### 1. 骰子投掷
-
-- `[d20 = 18]` - 简单投掷结果
-- `.r1d20+5` - 投掷命令
-- `(1d20+5 = 18)` - 完整投掷信息
-
-### 2. 角色动作
-
-- `*动作描述*` - 单星号
-- `**重要动作**` - 双星号
-
-### 3. 对话
-
-- `「对话内容」` - 中文引号
-- `"对话内容"` - 英文引号
-- `"对话内容"` - 弯引号
-
-### 4. OOC(脱戏)
-
-- `((OOC内容))` - 双括号
-- `//OOC注释` - 双斜杠
-
-### 5. 系统消息
-
-- `[系统]消息内容`
-- `[System]Message`
-
-## 疑难解答
-
-### 问题:规则文件加载失败
-
-**解决方案:**
-1. 确保文件是有效的 JSON5 格式
-2. 检查正则表达式是否转义正确(使用 `\\` 而不是 `\`)
-3. 验证文件编码为 UTF-8
-
-### 问题:解析结果不正确
-
-**解决方案:**
-1. 调整规则的优先级
-2. 测试正则表达式(使用 https://regex101.com/)
-3. 检查 match_type 是否正确
-
-### 问题:中文字符显示异常
-
-**解决方案:**
-- 确保所有文件使用 UTF-8 编码
-- 在打开文件时指定 `encoding='utf-8'`
-
-## 更多示例
-
-访问项目文档查看更多示例:
-https://crp.hydroroll.team/
-
-## 贡献
-
-欢迎提交新的示例和规则文件!请参考 CONTRIBUTING.md
diff --git a/examples/basic_usage.py b/examples/basic_usage.py
index c327cb0..d1ab724 100644
--- a/examples/basic_usage.py
+++ b/examples/basic_usage.py
@@ -1,13 +1,6 @@
-#!/usr/bin/env python3
-"""
-基础使用示例
-演示如何使用 ConventionalRP 解析和处理 TRPG 日志
-"""
-
import sys
from pathlib import Path
-# 添加 src 目录到 Python 路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root / "src"))
@@ -18,37 +11,22 @@ from conventionalrp.renderers.html_renderer import HTMLRenderer
from conventionalrp.renderers.json_renderer import JSONRenderer
from conventionalrp.renderers.markdown_renderer import MarkdownRenderer
-
-def main():
- # 获取示例文件路径
+def main():
example_dir = Path(__file__).parent
rules_file = example_dir / "rules" / "dnd5e_rules.json5"
log_file = example_dir / "logs" / "sample_session.txt"
-
- print("=" * 60)
- print("ConventionalRP 基础使用示例")
- print("=" * 60)
-
- # 步骤 1: 加载规则
- print("\n[步骤 1] 加载解析规则...")
+
parser = Parser()
parser.load_rules(str(rules_file))
- print(f"✓ 规则加载成功: {rules_file.name}")
+ print(f"Rule loaded: {rules_file.name}")
- # 步骤 2: 解析日志
- print("\n[步骤 2] 解析 TRPG 日志...")
parsed_data = parser.parse_log(str(log_file))
- print(f"✓ 日志解析完成,共 {len(parsed_data)} 条记录")
+ print(f"Log parsed successfully, {len(parsed_data)} entries found.")
- # 步骤 3: 处理解析结果
- print("\n[步骤 3] 处理解析后的数据...")
processor = Processor()
processed_data = processor.process_tokens(parsed_data)
- print(f"✓ 数据处理完成")
-
- # 步骤 4: 渲染输出
- print("\n[步骤 4] 渲染输出...")
-
+ print(f"Done processing data")
+
# JSON 格式
json_renderer = JSONRenderer()
json_output = json_renderer.render(processed_data)
@@ -56,7 +34,7 @@ def main():
json_file.parent.mkdir(exist_ok=True)
with open(json_file, "w", encoding="utf-8") as f:
f.write(json_output)
- print(f"✓ JSON 输出已保存: {json_file}")
+ print(f"Json: {json_file}")
# HTML 格式
html_renderer = HTMLRenderer()
@@ -64,32 +42,14 @@ def main():
html_file = example_dir / "output" / "session_output.html"
with open(html_file, "w", encoding="utf-8") as f:
f.write(html_output)
- print(f"✓ HTML 输出已保存: {html_file}")
+ print(f"HTML: {html_file}")
- # Markdown 格式
md_renderer = MarkdownRenderer()
md_output = md_renderer.render(processed_data)
md_file = example_dir / "output" / "session_output.md"
with open(md_file, "w", encoding="utf-8") as f:
f.write(md_output)
- print(f"✓ Markdown 输出已保存: {md_file}")
-
- # 预览前几条记录
- print("\n" + "=" * 60)
- print("解析结果预览(前3条):")
- print("=" * 60)
- for i, entry in enumerate(parsed_data[:3], 1):
- print(f"\n[记录 {i}]")
- print(f" 时间: {entry.get('timestamp', 'N/A')}")
- print(f" 发言者: {entry.get('speaker', 'N/A')}")
- print(f" 内容类型数: {len(entry.get('content', []))}")
- for content in entry.get('content', [])[:2]: # 只显示前2个内容
- print(f" - {content.get('type', 'unknown')}: {content.get('content', '')[:50]}...")
-
- print("\n" + "=" * 60)
- print("✓ 所有步骤完成!")
- print("=" * 60)
-
+ print(f"Markdown: {md_file}")
if __name__ == "__main__":
main()
diff --git a/examples/custom_plugin.py b/examples/custom_plugin.py
index dd96311..5f77c41 100644
--- a/examples/custom_plugin.py
+++ b/examples/custom_plugin.py
@@ -1,14 +1,7 @@
-#!/usr/bin/env python3
-"""
-自定义插件示例
-演示如何创建和使用自定义插件来扩展 ConventionalRP 的功能
-"""
-
import sys
from typing import List, Dict, Any
from pathlib import Path
-# 添加 src 目录到 Python 路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root / "src"))
@@ -17,21 +10,12 @@ from conventionalrp.core.processor import Processor
class DiceRollAnalyzer:
- """骰子统计分析插件"""
+ """骰点统计分析插件"""
def __init__(self):
self.name = "Dice Roll Analyzer"
def analyze(self, parsed_data: List[Dict[str, Any]]) -> Dict[str, Any]:
- """
- 分析日志中的所有骰子投掷
-
- Args:
- parsed_data: 解析后的日志数据
-
- Returns:
- 统计结果
- """
stats = {
"total_rolls": 0,
"by_character": {},
@@ -65,15 +49,6 @@ class DialogueExtractor:
self.name = "Dialogue Extractor"
def extract(self, parsed_data: List[Dict[str, Any]]) -> List[Dict[str, str]]:
- """
- 提取所有角色对话
-
- Args:
- parsed_data: 解析后的日志数据
-
- Returns:
- 对话列表
- """
dialogues = []
for entry in parsed_data:
@@ -92,59 +67,38 @@ class DialogueExtractor:
def main():
- print("=" * 60)
- print("ConventionalRP 自定义插件示例")
- print("=" * 60)
-
- # 准备数据
example_dir = Path(__file__).parent
rules_file = example_dir / "rules" / "dnd5e_rules.json5"
log_file = example_dir / "logs" / "combat_log.txt"
-
- print("\n[1] 解析日志...")
+
+ print("\nLoading log...")
parser = Parser()
parser.load_rules(str(rules_file))
parsed_data = parser.parse_log(str(log_file))
- print(f"✓ 解析完成,共 {len(parsed_data)} 条记录")
+ print(f"Done, {len(parsed_data)} in total")
- # 使用骰子分析插件
- print("\n[2] 运行骰子统计分析插件...")
dice_analyzer = DiceRollAnalyzer()
dice_stats = dice_analyzer.analyze(parsed_data)
-
- print(f"\n骰子统计结果:")
+
+ print(f"\nStatistics:")
print(f" 总投掷次数: {dice_stats['total_rolls']}")
print(f"\n 按角色统计:")
for character, count in dice_stats['by_character'].items():
print(f" {character}: {count} 次")
- print(f"\n 按骰子类型统计:")
+ print(f"\n Statistics in Dice Types:")
for dice_type, count in dice_stats['dice_types'].items():
- print(f" d{dice_type}: {count} 次")
+ print(f" d{dice_type}: {count} times")
- # 使用对话提取插件
- print("\n[3] 运行对话提取插件...")
dialogue_extractor = DialogueExtractor()
dialogues = dialogue_extractor.extract(parsed_data)
-
- print(f"\n提取到 {len(dialogues)} 条对话:")
- for i, dialogue in enumerate(dialogues[:5], 1): # 只显示前5条
+
+ print(f"\nExtracted {len(dialogues)} dialogues:")
+ for i, dialogue in enumerate(dialogues[:5], 1): # Only show the first 5
print(f"\n [{i}] {dialogue['speaker']} ({dialogue['timestamp']})")
print(f" {dialogue['dialogue']}")
if len(dialogues) > 5:
- print(f"\n ... 还有 {len(dialogues) - 5} 条对话")
-
- print("\n" + "=" * 60)
- print("✓ 插件演示完成!")
- print("=" * 60)
- print("\n提示: 你可以创建自己的插件来实现:")
- print(" - 战斗统计分析")
- print(" - 角色行为分析")
- print(" - 关键词提取")
- print(" - 情感分析")
- print(" - 自动摘要生成")
- print(" - ... 以及更多!")
-
+ print(f"\n ... and {len(dialogues) - 5} more dialogues")
if __name__ == "__main__":
main()
diff --git a/examples/plugin_system_demo.py b/examples/plugin_system_demo.py
new file mode 100644
index 0000000..1899b34
--- /dev/null
+++ b/examples/plugin_system_demo.py
@@ -0,0 +1,173 @@
+import sys
+import os
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+
+from conventionalrp.plugins import PluginManager
+from conventionalrp.core.parser import Parser
+
+sys.path.insert(0, str(Path(__file__).parent / "plugins"))
+from dice_analyzer_plugin import DiceAnalyzerPlugin
+from combat_tracker_plugin import CombatTrackerPlugin
+
+
+def demo_basic_plugin_usage():
+ manager = PluginManager()
+
+ dice_analyzer = DiceAnalyzerPlugin()
+ dice_analyzer.initialize()
+ manager.register_plugin(dice_analyzer)
+
+ combat_tracker = CombatTrackerPlugin()
+ combat_tracker.initialize()
+ manager.register_plugin(combat_tracker)
+
+ print("\nRegistered Plugins:")
+ for plugin_info in manager.list_plugins():
+ print(f" - {plugin_info['name']} v{plugin_info['version']} (type: {plugin_info['type']})")
+
+ stats = manager.get_statistics()
+ print(f"\nPlugin Statistics: {stats}")
+
+
+def demo_processor_plugin():
+ manager = PluginManager()
+
+ combat_tracker = CombatTrackerPlugin()
+ combat_tracker.initialize()
+ manager.register_plugin(combat_tracker)
+
+ combat_log = [
+ {"type": "dialogue", "speaker": "战士", "content": "我攻击兽人,造成12点伤害!"},
+ {"type": "dialogue", "speaker": "法师", "content": "火球术!造成28点火焰伤害"},
+ {"type": "dialogue", "speaker": "牧师", "content": "治疗之光,恢复15点生命值"},
+ {"type": "dialogue", "speaker": "战士", "content": "重击!造成18点伤害"},
+ {"type": "dialogue", "speaker": "牧师", "content": "群体治疗,恢复10点生命值"},
+ ]
+
+ print("\nProcessing Combat Log:")
+ from conventionalrp.plugins.base import ProcessorPlugin
+ processed_log = manager.execute_plugins(combat_log, plugin_type=ProcessorPlugin)
+
+ for token in processed_log:
+ if "combat_data" in token:
+ print(f" {token['speaker']}: {token['content']}")
+ print(f" -> {token['combat_data']}")
+
+ print("\nCombat Summary:")
+ summary = combat_tracker.get_combat_summary()
+ print(f" 总伤害: {summary['total_damage']}")
+ print(f" 总治疗: {summary['total_healing']}")
+ print(f" 净伤害: {summary['net_damage']}")
+ print("\n 角色统计:")
+ for character, stats in summary['character_stats'].items():
+ print(f" {character}: 造成伤害={stats['damage_dealt']}, 治疗={stats['healing_done']}")
+
+
+def demo_analyzer_plugin():
+ manager = PluginManager()
+
+ dice_analyzer = DiceAnalyzerPlugin()
+ dice_analyzer.initialize()
+ manager.register_plugin(dice_analyzer)
+
+ dice_rolls = [
+ {"type": "dice", "content": "d20=15", "result": 15},
+ {"type": "success", "content": "检定成功"},
+ {"type": "dice", "content": "d6=4", "result": 4},
+ {"type": "dice", "content": "d20=20", "result": 20},
+ {"type": "success", "content": "大成功!Critical hit!"},
+ {"type": "dice", "content": "d20=1", "result": 1},
+ {"type": "failure", "content": "大失败..."},
+ {"type": "dice", "content": "d20=12", "result": 12},
+ {"type": "dice", "content": "d6=3", "result": 3},
+ {"type": "success", "content": "检定成功"},
+ ]
+
+ print("\nDice Roll Data:")
+ for roll in dice_rolls:
+ if roll["type"] == "dice":
+ print(f" {roll['content']}")
+
+ from conventionalrp.plugins.base import AnalyzerPlugin
+ analysis = manager.execute_plugins(dice_rolls, plugin_type=AnalyzerPlugin)
+
+ print("\nAnalyze result:")
+ print(f" 总投掷次数: {analysis['total_rolls']}")
+ print(f" 骰子类型分布: {analysis['dice_types']}")
+ print(f" 成功次数: {analysis['success_count']}")
+ print(f" 失败次数: {analysis['failure_count']}")
+ print(f" 大成功次数: {analysis['critical_hits']}")
+ print(f" 大失败次数: {analysis['critical_fails']}")
+ print(f" 成功率: {analysis['success_rate']:.1%}")
+ print(f" 出现极值比率: {analysis['critical_rate']:.1%}")
+
+
+def demo_plugin_enable_disable():
+ manager = PluginManager()
+ dice_analyzer = DiceAnalyzerPlugin()
+ dice_analyzer.initialize()
+ manager.register_plugin(dice_analyzer)
+
+ combat_tracker = CombatTrackerPlugin()
+ combat_tracker.initialize()
+ manager.register_plugin(combat_tracker)
+
+ print("\nInitial State:")
+ for plugin_info in manager.list_plugins():
+ print(f" {plugin_info['name']}: {'Enabled' if plugin_info['enabled'] else 'Disabled'}")
+
+ # Disable DiceAnalyzer
+ print("\nDisabling DiceAnalyzer...")
+ manager.disable_plugin("DiceAnalyzer")
+
+ print("\nCurrent State:")
+ for plugin_info in manager.list_plugins():
+ print(f" {plugin_info['name']}: {'Enabled' if plugin_info['enabled'] else 'Disabled'}")
+
+ print("\nRe-enabling DiceAnalyzer...")
+ manager.enable_plugin("DiceAnalyzer")
+
+ print("\nFinal State:")
+ for plugin_info in manager.list_plugins():
+ print(f" {plugin_info['name']}: {'Enabled' if plugin_info['enabled'] else 'Disabled'}")
+
+
+def demo_plugin_discovery():
+ plugin_dir = Path(__file__).parent / "plugins"
+ manager = PluginManager(plugin_dirs=[str(plugin_dir)])
+
+ print(f"\nSearching for plugins in directory: {plugin_dir}")
+ discovered = manager.discover_plugins()
+
+ print(f"\nFound {len(discovered)} plugin modules:")
+ for module in discovered:
+ print(f" - {module}")
+
+ print("\nLoading plugins...")
+ for py_file in plugin_dir.glob("*.py"):
+ if py_file.name.startswith("_"):
+ continue
+
+ plugin_class = manager.load_plugin_from_file(str(py_file))
+ if plugin_class:
+ print(f" ✓ Successfully loaded: {py_file.name}")
+
+
+def main():
+ try:
+ demo_basic_plugin_usage()
+ demo_processor_plugin()
+ demo_analyzer_plugin()
+ demo_plugin_enable_disable()
+ demo_plugin_discovery()
+
+ except Exception as e:
+ print(f"\n{e!r}: {e}")
+ import traceback
+ traceback.print_exc()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/plugins/combat_tracker_plugin.py b/examples/plugins/combat_tracker_plugin.py
new file mode 100644
index 0000000..a12bc9e
--- /dev/null
+++ b/examples/plugins/combat_tracker_plugin.py
@@ -0,0 +1,96 @@
+import sys
+import os
+from pathlib import Path
+import re
+from typing import Any, Dict, List
+
+sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
+
+from conventionalrp.plugins.base import ProcessorPlugin
+
+
+class CombatTrackerPlugin(ProcessorPlugin):
+ """战斗数据追踪插件"""
+
+ def __init__(self):
+ super().__init__("CombatTracker", "1.0.0")
+ self.damage_pattern = re.compile(r'(\d+)\s*点?(伤害|damage|dmg)', re.IGNORECASE)
+ self.heal_pattern = re.compile(r'(\d+)\s*点?(治疗|healing|heal)', re.IGNORECASE)
+
+ def initialize(self, config: Dict[str, Any] | None = None):
+ self.config = config or {}
+ self.total_damage = 0
+ self.total_healing = 0
+ self.character_stats = {}
+ self.logger.info("CombatTrackerPlugin initialized")
+
+ def process_token(self, token: Dict[str, Any]) -> Dict[str, Any]:
+ content = token.get("content", "")
+ speaker = token.get("speaker", "Unknown")
+
+ damage_match = self.damage_pattern.search(content)
+ if damage_match:
+ damage = int(damage_match.group(1))
+ self.total_damage += damage
+
+ if speaker not in self.character_stats:
+ self.character_stats[speaker] = {"damage_dealt": 0, "healing_done": 0}
+ self.character_stats[speaker]["damage_dealt"] += damage
+
+ token["combat_data"] = {
+ "type": "damage",
+ "amount": damage,
+ "total_damage": self.total_damage,
+ }
+
+ heal_match = self.heal_pattern.search(content)
+ if heal_match:
+ healing = int(heal_match.group(1))
+ self.total_healing += healing
+
+ if speaker not in self.character_stats:
+ self.character_stats[speaker] = {"damage_dealt": 0, "healing_done": 0}
+ self.character_stats[speaker]["healing_done"] += healing
+
+ token["combat_data"] = {
+ "type": "healing",
+ "amount": healing,
+ "total_healing": self.total_healing,
+ }
+
+ return token
+
+ def get_combat_summary(self) -> Dict[str, Any]:
+ return {
+ "total_damage": self.total_damage,
+ "total_healing": self.total_healing,
+ "net_damage": self.total_damage - self.total_healing,
+ "character_stats": self.character_stats,
+ }
+
+ def reset_stats(self):
+ self.total_damage = 0
+ self.total_healing = 0
+ self.character_stats.clear()
+ self.logger.info("Combat stats reset")
+
+
+if __name__ == "__main__":
+ plugin = CombatTrackerPlugin()
+ plugin.initialize()
+
+ test_tokens = [
+ {"type": "dialogue", "speaker": "战士", "content": "我攻击兽人,造成12点伤害"},
+ {"type": "dialogue", "speaker": "法师", "content": "火球术命中,造成28点伤害"},
+ {"type": "dialogue", "speaker": "牧师", "content": "治疗术,恢复15点生命值"},
+ {"type": "dialogue", "speaker": "战士", "content": "再次攻击,造成8点伤害"},
+ ]
+
+ for token in test_tokens:
+ processed = plugin.process_token(token)
+ if "combat_data" in processed:
+ print(f" {processed['speaker']}: {processed['combat_data']}")
+
+ summary = plugin.get_combat_summary()
+ for key, value in summary.items():
+ print(f" {key}: {value}")
diff --git a/examples/plugins/dice_analyzer_plugin.py b/examples/plugins/dice_analyzer_plugin.py
new file mode 100644
index 0000000..3ab2fb9
--- /dev/null
+++ b/examples/plugins/dice_analyzer_plugin.py
@@ -0,0 +1,90 @@
+import sys
+import os
+from pathlib import Path
+import re
+from typing import Any, Dict, List
+
+sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
+
+from conventionalrp.plugins.base import AnalyzerPlugin
+
+
+class DiceAnalyzerPlugin(AnalyzerPlugin):
+ """骰子投掷数据分析插件"""
+ def __init__(self):
+ super().__init__("DiceAnalyzer", "1.0.0")
+ self.dice_pattern = re.compile(r'd(\d+)')
+
+ def initialize(self, config: Dict[str, Any] | None = None):
+ self.config = config or {}
+ self.logger.info("DiceAnalyzerPlugin initialized")
+
+ def analyze(self, data: Any) -> Dict[str, Any]:
+ if not isinstance(data, list):
+ return {"error": "Input must be a list of tokens"}
+
+ total_rolls = 0
+ dice_types = {}
+ success_count = 0
+ failure_count = 0
+ critical_hits = 0
+ critical_fails = 0
+
+ for token in data:
+ if not isinstance(token, dict):
+ continue
+
+ token_type = token.get("type", "")
+ content = token.get("content", "")
+
+ if token_type == "dice":
+ total_rolls += 1
+
+ match = self.dice_pattern.search(content)
+ if match:
+ dice_type = f"d{match.group(1)}"
+ dice_types[dice_type] = dice_types.get(dice_type, 0) + 1
+
+ if token_type == "success":
+ success_count += 1
+ elif token_type == "failure":
+ failure_count += 1
+
+ if "critical" in content.lower():
+ if "success" in token_type or "成功" in content:
+ critical_hits += 1
+ elif "failure" in token_type or "失败" in content:
+ critical_fails += 1
+
+ result = {
+ "total_rolls": total_rolls,
+ "dice_types": dice_types,
+ "success_count": success_count,
+ "failure_count": failure_count,
+ "critical_hits": critical_hits,
+ "critical_fails": critical_fails,
+ "success_rate": success_count / total_rolls if total_rolls > 0 else 0,
+ "critical_rate": (critical_hits + critical_fails) / total_rolls if total_rolls > 0 else 0,
+ }
+
+ self.logger.info(f"Analyzed {total_rolls} dice rolls")
+ return result
+
+
+if __name__ == "__main__":
+ plugin = DiceAnalyzerPlugin()
+ plugin.initialize()
+
+ test_data = [
+ {"type": "dice", "content": "d20=15"},
+ {"type": "success", "content": "检定成功"},
+ {"type": "dice", "content": "d6=4"},
+ {"type": "dice", "content": "d20=20"},
+ {"type": "success", "content": "大成功!Critical hit!"},
+ {"type": "dice", "content": "d20=1"},
+ {"type": "failure", "content": "大失败..."},
+ ]
+
+ result = plugin.analyze(test_data)
+ for key, value in result.items():
+ print(f" {key}: {value}")
diff --git a/examples/renderer_demo.py b/examples/renderer_demo.py
new file mode 100644
index 0000000..fa89b42
--- /dev/null
+++ b/examples/renderer_demo.py
@@ -0,0 +1,147 @@
+import sys
+import os
+from pathlib import Path
+
+# 添加父目录到 Python 路径
+sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
+
+from conventionalrp.renderers.html_renderer import HTMLRenderer
+from conventionalrp.renderers.markdown_renderer import MarkdownRenderer
+from conventionalrp.renderers.json_renderer import JSONRenderer
+
+
+TEST_DATA = [
+ {
+ "type": "dialogue",
+ "speaker": "战士",
+ "content": "我们需要更小心地前进,前面可能有陷阱。",
+ "timestamp": "2024-01-15 20:30:15",
+ "tags": ["对话", "警告"],
+ },
+ {
+ "type": "dice",
+ "speaker": "战士",
+ "content": "d20+5",
+ "result": 18,
+ "timestamp": "2024-01-15 20:30:30",
+ },
+ {
+ "type": "success",
+ "content": "战士成功发现了隐藏的陷阱!",
+ "timestamp": "2024-01-15 20:30:35",
+ },
+ {
+ "type": "narration",
+ "content": "昏暗的走廊中,石板地面上隐约可见一些不寻常的纹路。",
+ "timestamp": "2024-01-15 20:31:00",
+ "tags": ["环境描述"],
+ },
+ {
+ "type": "dialogue",
+ "speaker": "法师",
+ "content": "让我施放侦测魔法,看看这里是否有魔法陷阱。",
+ "timestamp": "2024-01-15 20:31:15",
+ },
+ {
+ "type": "dice",
+ "speaker": "法师",
+ "content": "d20+8",
+ "result": 23,
+ "timestamp": "2024-01-15 20:31:20",
+ "combat_data": {"type": "damage", "amount": 12, "total_damage": 12},
+ },
+ {
+ "type": "system",
+ "content": "法师侦测到前方10英尺处有一个魔法陷阱(火球术触发)。",
+ "timestamp": "2024-01-15 20:31:25",
+ },
+]
+
+
+def demo_html_themes():
+ themes = ["light", "dark", "fantasy"]
+
+ for theme in themes:
+ print(f"\nGenerate {theme} theme...")
+ renderer = HTMLRenderer(theme=theme)
+ html_output = renderer.render(TEST_DATA)
+
+ output_file = f"output_html_{theme}.html"
+ with open(output_file, "w", encoding="utf-8") as f:
+ f.write(html_output)
+
+def demo_html_custom_css():
+ custom_css = """
+ body {
+ background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%);
+ }
+
+ .token {
+ animation: fadeIn 0.5s;
+ }
+
+ @keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+ }
+ """
+
+ renderer = HTMLRenderer(theme="light", custom_css=custom_css)
+ html_output = renderer.render(TEST_DATA)
+
+ output_file = "output_html_custom.html"
+ with open(output_file, "w", encoding="utf-8") as f:
+ f.write(html_output)
+
+
+def demo_markdown_styles():
+ renderer_full = MarkdownRenderer(enable_syntax_hints=True, enable_emoji=True)
+ md_output_full = renderer_full.render(TEST_DATA)
+
+ with open("output_markdown_full.md", "w", encoding="utf-8") as f:
+ f.write(md_output_full)
+
+ renderer_simple = MarkdownRenderer(enable_syntax_hints=False, enable_emoji=False)
+ md_output_simple = renderer_simple.render(TEST_DATA)
+
+ with open("output_markdown_simple.md", "w", encoding="utf-8") as f:
+ f.write(md_output_simple)
+
+ print(md_output_full[:100] + "...")
+
+
+def demo_json_formats():
+ renderer_pretty = JSONRenderer(pretty=True, indent=2)
+ json_output_pretty = renderer_pretty.render(TEST_DATA)
+
+ with open("output_json_pretty.json", "w", encoding="utf-8") as f:
+ f.write(json_output_pretty)
+
+ renderer_compact = JSONRenderer(pretty=False)
+ json_output_compact = renderer_compact.render(TEST_DATA)
+
+ with open("output_json_compact.json", "w", encoding="utf-8") as f:
+ f.write(json_output_compact)
+
+ renderer_sorted = JSONRenderer(pretty=True, indent=4, sort_keys=True)
+ json_output_sorted = renderer_sorted.render(TEST_DATA)
+
+ with open("output_json_sorted.json", "w", encoding="utf-8") as f:
+ f.write(json_output_sorted)
+
+def main():
+
+ try:
+ demo_html_themes()
+ demo_html_custom_css()
+ demo_markdown_styles()
+ demo_json_formats()
+
+ except Exception as e:
+ print(f"\n{e!r}: {e}")
+ import traceback
+ traceback.print_exc()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/rule_system_demo.py b/examples/rule_system_demo.py
new file mode 100644
index 0000000..d38056b
--- /dev/null
+++ b/examples/rule_system_demo.py
@@ -0,0 +1,210 @@
+import sys
+from pathlib import Path
+
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root / "src"))
+
+from conventionalrp.core.rules import Rule, RuleEngine
+from conventionalrp.core.processor import Processor
+
+
+def example_1_simple_rules():
+ rule = Rule(
+ name="tag_dice_rolls",
+ condition={"type": "dice_roll"},
+ action={"type": "add_tag", "tag": "game_mechanics"},
+ priority=100
+ )
+
+ engine = RuleEngine()
+ engine.add_rule(rule)
+
+ data = {"type": "dice_roll", "content": "[d20 = 18]"}
+
+ print(f"Original data: {data}")
+ result = engine.process(data)
+ print(f"Processed result: {result}")
+ print()
+
+
+def example_2_conditional_rules():
+ engine = RuleEngine()
+
+ # Rule1: High rolls (>=15) get "success" tag
+ engine.add_rule_dict(
+ name="high_roll",
+ condition={
+ "type": "dice_roll",
+ "result": {"type": "greater_than", "value": 15}
+ },
+ action={"type": "add_tag", "tag": "success"},
+ priority=100
+ )
+
+ # Rule2: Low rolls (<10) get "failure" tag
+ engine.add_rule_dict(
+ name="low_roll",
+ condition={
+ "type": "dice_roll",
+ "result": {"type": "less_than", "value": 10}
+ },
+ action={"type": "add_tag", "tag": "failure"},
+ priority=100
+ )
+
+ test_cases = [
+ {"type": "dice_roll", "result": 18, "content": "[d20 = 18]"},
+ {"type": "dice_roll", "result": 5, "content": "[d20 = 5]"},
+ {"type": "dice_roll", "result": 12, "content": "[d20 = 12]"},
+ ]
+
+ for data in test_cases:
+ result = engine.process(data)
+ print(f"结果: {data['result']} -> 标签: {result.get('tags', [])}")
+ print()
+
+
+def example_3_field_transformation():
+ engine = RuleEngine()
+
+ # Rule: Normalize speaker names
+ engine.add_rule_dict(
+ name="normalize_speaker",
+ condition={"type": "metadata"},
+ action={
+ "type": "transform",
+ "field": "speaker",
+ "function": "upper"
+ },
+ priority=90
+ )
+
+ data = {
+ "type": "metadata",
+ "speaker": "艾莉娅",
+ "timestamp": "2025-10-24 14:30:01"
+ }
+
+ print(f"Original data: {data}")
+ result = engine.process(data)
+ print(f"Processed result: {result}")
+ print()
+
+
+def example_4_processor_with_rules():
+ processor = Processor()
+
+ # Add rule: Highlight important dialogues
+ processor.add_rule(Rule(
+ name="highlight_important_dialogue",
+ condition={
+ "type": "dialogue",
+ "content": {"type": "contains", "value": "重要"}
+ },
+ action={"type": "add_tag", "tag": "important"},
+ priority=100
+ ))
+
+ # Add rule: Mark all metadata as processed
+ processor.add_rule(Rule(
+ name="mark_metadata",
+ condition={"type": "metadata"},
+ action={"type": "set_field", "field": "processed_by", "value": "rule_engine"},
+ priority=90
+ ))
+
+ tokens = [
+ {"type": "metadata", "speaker": "DM", "timestamp": "2025-10-24"},
+ {"type": "dialogue", "content": "这是重要的线索"},
+ {"type": "dialogue", "content": "普通对话"},
+ {"type": "dice_roll", "result": 20},
+ ]
+
+ print(f"Processing {len(tokens)} tokens...")
+ results = processor.process_tokens(tokens)
+
+ for i, result in enumerate(results):
+ print(f" [{i+1}] {result.get('type')}: "
+ f"Tags={result.get('tags', [])} "
+ f"Processed by={result.get('processed_by', 'N/A')}")
+ print()
+
+
+def example_5_custom_processor():
+ processor = Processor()
+
+ # Custom processing function: Count characters
+ def add_char_count(data):
+ if "content" in data:
+ data["char_count"] = len(data["content"])
+ return data
+
+ # Custom processing function: Ensure timestamp exists
+ def ensure_timestamp(data):
+ if "timestamp" not in data:
+ from datetime import datetime
+ data["timestamp"] = datetime.now().isoformat()
+ return data
+
+ processor.add_processor(add_char_count)
+ processor.add_processor(ensure_timestamp)
+
+ test_data = [
+ {"type": "dialogue", "content": "你好世界"},
+ {"type": "text", "content": "这是一段很长的文本内容"},
+ ]
+
+ results = processor.process_tokens(test_data)
+
+ for result in results:
+ print(f" {result.get('type')}: "
+ f"Character count={result.get('char_count')} "
+ f"Timestamp={result.get('timestamp', 'N/A')[:19]}")
+ print()
+
+
+def example_6_priority_and_order():
+ engine = RuleEngine()
+
+ engine.add_rule_dict(
+ name="low_priority",
+ condition={"type": "test"},
+ action={"type": "set_field", "field": "processed_by", "value": "low"},
+ priority=10
+ )
+
+ engine.add_rule_dict(
+ name="high_priority",
+ condition={"type": "test"},
+ action={"type": "set_field", "field": "processed_by", "value": "high"},
+ priority=100
+ )
+
+ engine.add_rule_dict(
+ name="medium_priority",
+ condition={"type": "test"},
+ action={"type": "set_field", "field": "processed_by", "value": "medium"},
+ priority=50
+ )
+
+ data = {"type": "test"}
+
+ result1 = engine.process(data, apply_all=False)
+ print(f"Only apply the highest priority matching rule: {result1}")
+
+ result2 = engine.process(data, apply_all=True)
+ print(f"Apply all matching rules: {result2}")
+ print()
+
+
+def main():
+ example_1_simple_rules()
+ example_2_conditional_rules()
+ example_3_field_transformation()
+ example_4_processor_with_rules()
+ example_5_custom_processor()
+ example_6_priority_and_order()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/conventionalrp/__init__.py b/src/conventionalrp/__init__.py
index 06dbd63..ab4b17d 100644
--- a/src/conventionalrp/__init__.py
+++ b/src/conventionalrp/__init__.py
@@ -1,15 +1,45 @@
+"""
+Conventional Role Play SDK (ConventionalRP)
+"""
+
import sys
from importlib.metadata import version
from . import _core
+from .core import Parser, Processor, Rule, RuleEngine
+from .utils import (
+ setup_logging,
+ get_logger,
+ ConventionalRPError,
+ ParserError,
+ RuleError,
+ ProcessorError,
+ ValidationError,
+ ConfigurationError,
+)
-__all__ = ["_core", "__version__"]
+__all__ = [
+ "Parser",
+ "Processor",
+ "Rule",
+ "RuleEngine",
+ "setup_logging",
+ "get_logger",
+ "ConventionalRPError",
+ "ParserError",
+ "RuleError",
+ "ProcessorError",
+ "ValidationError",
+ "ConfigurationError",
+ "__version__",
+]
if sys.version_info >= (3, 8):
- # For Python 3.8+
__version__ = version("conventionalrp")
elif sys.version_info < (3, 8):
from pkg_resources import get_distribution
- # For Python < 3.8
__version__ = get_distribution("conventionalrp").version
+
+_default_logger = setup_logging(level="INFO")
+
diff --git a/src/conventionalrp/__main__.py b/src/conventionalrp/__main__.py
index fa4f45e..e69de29 100644
--- a/src/conventionalrp/__main__.py
+++ b/src/conventionalrp/__main__.py
@@ -1,9 +0,0 @@
-from ._core import sum_as_string
-
-
-def main():
- print(sum_as_string(1, 2))
-
-
-if __name__ == "__main__":
- main()
diff --git a/src/conventionalrp/_core.pyi b/src/conventionalrp/_core.pyi
index 0805887..6f87269 100644
--- a/src/conventionalrp/_core.pyi
+++ b/src/conventionalrp/_core.pyi
@@ -1,3 +1,237 @@
-def sum_as_string(a: int, b: int) -> str: ...
+"""
+提供高性能的文本解析和匹配功能
+"""
-class Base: ...
+from typing import Dict, List, Optional, Tuple
+
+class Base:
+ """基础类(向后兼容)"""
+ ...
+
+class Token:
+ """
+ 解析后的 Token
+
+ 表示文本中的一个语义单元,包含类型、内容和元数据
+ """
+
+ token_type: str
+ content: str
+ metadata: Dict[str, str]
+
+ def __init__(self, token_type: str, content: str) -> None:
+ """
+ 创建新的 Token
+
+ Args:
+ token_type: Token 类型
+ content: Token 内容
+ """
+ ...
+
+ def add_metadata(self, key: str, value: str) -> None:
+ """
+ 添加元数据
+
+ Args:
+ key: 元数据键
+ value: 元数据值
+ """
+ ...
+
+ def get_metadata(self, key: str) -> Optional[str]:
+ """
+ 获取元数据
+
+ Args:
+ key: 元数据键
+
+ Returns:
+ 元数据值,如果不存在返回 None
+ """
+ ...
+
+ def to_dict(self) -> Dict[str, any]:
+ """
+ 转换为 Python 字典
+
+ Returns:
+ 包含 Token 所有信息的字典
+ """
+ ...
+
+ def __repr__(self) -> str: ...
+
+class RegexRule:
+ """
+ 正则表达式规则
+
+ 用于文本匹配和提取
+ """
+
+ rule_type: str
+ priority: int
+
+ def __init__(self, pattern: str, rule_type: str, priority: int) -> None:
+ """
+ 创建正则规则
+
+ Args:
+ pattern: 正则表达式模式
+ rule_type: 规则类型
+ priority: 优先级(数字越大优先级越高)
+
+ Raises:
+ ValueError: 如果正则表达式无效
+ """
+ ...
+
+ def matches(self, text: str) -> bool:
+ """
+ 测试文本是否匹配
+
+ Args:
+ text: 待测试的文本
+
+ Returns:
+ 是否匹配
+ """
+ ...
+
+ def extract(self, text: str) -> Optional[List[str]]:
+ """
+ 提取匹配的捕获组
+
+ Args:
+ text: 待提取的文本
+
+ Returns:
+ 捕获组列表,如果不匹配返回 None
+ """
+ ...
+
+ def find_all(self, text: str) -> List[Tuple[int, int, str]]:
+ """
+ 查找所有匹配
+
+ Args:
+ text: 待搜索的文本
+
+ Returns:
+ 列表,每个元素为 (start, end, matched_text)
+ """
+ ...
+
+class TextParser:
+ """
+ 高性能文本解析器
+
+ 支持多规则、优先级排序的文本解析
+ """
+
+ def __init__(self) -> None:
+ """创建新的解析器"""
+ ...
+
+ def add_rule(self, pattern: str, rule_type: str, priority: int) -> None:
+ """
+ 添加解析规则
+
+ Args:
+ pattern: 正则表达式模式
+ rule_type: 规则类型
+ priority: 优先级
+
+ Raises:
+ ValueError: 如果正则表达式无效
+ """
+ ...
+
+ def parse_line(self, text: str) -> List[Tuple[str, str, int, int]]:
+ """
+ 解析单行文本
+
+ Args:
+ text: 待解析的文本
+
+ Returns:
+ 列表,每个元素为 (type, content, start, end)
+ """
+ ...
+
+ def parse_lines(self, lines: List[str]) -> List[List[Dict[str, any]]]:
+ """
+ 批量解析多行文本
+
+ Args:
+ lines: 文本行列表
+
+ Returns:
+ 每行的解析结果列表
+ """
+ ...
+
+ def clear_rules(self) -> None:
+ """清除所有规则"""
+ ...
+
+ def rule_count(self) -> int:
+ """
+ 获取规则数量
+
+ Returns:
+ 当前规则数量
+ """
+ ...
+
+class FastMatcher:
+ """
+ 快速字符串匹配器
+
+ 用于高效的多模式字符串匹配
+ """
+
+ def __init__(self, patterns: List[str]) -> None:
+ """
+ 创建匹配器
+
+ Args:
+ patterns: 模式列表
+ """
+ ...
+
+ def contains_any(self, text: str) -> bool:
+ """
+ 检查文本是否包含任意模式
+
+ Args:
+ text: 待检查的文本
+
+ Returns:
+ 是否包含任意模式
+ """
+ ...
+
+ def find_matches(self, text: str) -> List[str]:
+ """
+ 查找所有匹配的模式
+
+ Args:
+ text: 待搜索的文本
+
+ Returns:
+ 匹配的模式列表
+ """
+ ...
+
+ def count_matches(self, text: str) -> Dict[str, int]:
+ """
+ 统计每个模式的出现次数
+
+ Args:
+ text: 待统计的文本
+
+ Returns:
+ 字典,键为模式,值为出现次数
+ """
+ ...
diff --git a/src/conventionalrp/base.py b/src/conventionalrp/base.py
deleted file mode 100644
index e69de29..0000000
--- a/src/conventionalrp/base.py
+++ /dev/null
diff --git a/src/conventionalrp/core/__init__.py b/src/conventionalrp/core/__init__.py
index 91d0f8f..08829b8 100644
--- a/src/conventionalrp/core/__init__.py
+++ b/src/conventionalrp/core/__init__.py
@@ -1,3 +1,6 @@
-"""
-This file initializes the core module of the conventionalrp SDK.
-"""
+from .parser import Parser
+from .processor import Processor
+from .rules import Rule, RuleEngine
+
+__all__ = ["Parser", "Processor", "Rule", "RuleEngine"]
+
diff --git a/src/conventionalrp/core/processor.py b/src/conventionalrp/core/processor.py
index bc74ffb..12ca32b 100644
--- a/src/conventionalrp/core/processor.py
+++ b/src/conventionalrp/core/processor.py
@@ -1,68 +1,104 @@
-from typing import List, Dict, Any, Optional
+from typing import List, Dict, Any, Optional, Callable
+import logging
+from .rules import RuleEngine, Rule
+
+logger = logging.getLogger(__name__)
class Processor:
- """处理器,用于处理解析后的token"""
-
def __init__(self, rules: Optional[Dict[str, Any]] = None):
- """
- 初始化处理器
-
- Args:
- rules: 处理规则(可选)
- """
self.rules = rules or {}
-
- def process_tokens(self, tokens: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
- """
- 处理token列表
+ self.rule_engine = RuleEngine()
+ self.custom_processors: List[Callable] = []
+
+ self._load_rules_to_engine()
- Args:
- tokens: 解析后的token列表
+ logger.info("Processor initialized with %d rules",
+ self.rule_engine.rule_count())
+
+ def _load_rules_to_engine(self):
+ if not isinstance(self.rules, dict):
+ return
+
+ rules_list = self.rules.get("rules", [])
+ for rule_dict in rules_list:
+ if not isinstance(rule_dict, dict):
+ continue
- Returns:
- 处理后的数据列表
- """
+ try:
+ self.rule_engine.add_rule_dict(
+ name=rule_dict.get("name", "unnamed"),
+ condition=rule_dict.get("condition", {}),
+ action=rule_dict.get("action", {}),
+ priority=rule_dict.get("priority", 50)
+ )
+ except Exception as e:
+ logger.warning("Failed to load rule: %s", e)
+
+ def add_rule(self, rule: Rule):
+ self.rule_engine.add_rule(rule)
+ logger.debug("Added rule: %s", rule.name)
+
+ def add_processor(self, processor: Callable[[Dict[str, Any]], Dict[str, Any]]):
+ self.custom_processors.append(processor)
+ logger.debug("Added custom processor")
+
+ def process_tokens(
+ self,
+ tokens: List[Dict[str, Any]],
+ apply_all_rules: bool = False
+ ) -> List[Dict[str, Any]]:
+ if not tokens:
+ logger.warning("Empty token list provided")
+ return []
+
+ logger.info("Processing %d tokens", len(tokens))
processed_data = []
- for token in tokens:
- processed_token = self.apply_rules(token)
- processed_data.append(processed_token)
- return processed_data
-
- def apply_rules(self, token: Dict[str, Any]) -> Dict[str, Any]:
- """
- 对单个token应用规则
- Args:
- token: 单个token
-
- Returns:
- 处理后的token
- """
- # 基础实现:直接返回token
- # 可以在此添加更多处理逻辑
+ for i, token in enumerate(tokens):
+ try:
+ processed_token = self.process_single_token(token, apply_all_rules)
+ processed_data.append(processed_token)
+ except Exception as e:
+ logger.error("Error processing token %d: %s", i, e)
+ # 发生错误时保留原始 token
+ processed_data.append(token)
+
+ logger.info("Successfully processed %d tokens", len(processed_data))
+ return processed_data
+
+ def process_single_token(
+ self,
+ token: Dict[str, Any],
+ apply_all_rules: bool = False
+ ) -> Dict[str, Any]:
processed = token.copy()
- # 添加处理时间戳
+ if self.rule_engine.rule_count() > 0:
+ processed = self.rule_engine.process(processed, apply_all_rules)
+
+ for processor in self.custom_processors:
+ try:
+ processed = processor(processed)
+ except Exception as e:
+ logger.error("Custom processor failed: %s", e)
+
if "timestamp" in processed:
processed["processed"] = True
return processed
-
- def generate_output(self, processed_data: List[Dict[str, Any]], format_type: str) -> str:
- """
- 生成指定格式的输出
+
+ def apply_rules(self, token: Dict[str, Any]) -> Dict[str, Any]:
+ return self.process_single_token(token)
+
+ def generate_output(
+ self,
+ processed_data: List[Dict[str, Any]],
+ format_type: str
+ ) -> str:
+ logger.info("Generating %s output for %d items",
+ format_type, len(processed_data))
- Args:
- processed_data: 处理后的数据
- format_type: 输出格式 (json/html/markdown)
-
- Returns:
- 格式化后的字符串
-
- Raises:
- ValueError: 不支持的格式类型
- """
if format_type == "json":
return self.generate_json_output(processed_data)
elif format_type == "html":
@@ -71,20 +107,24 @@ class Processor:
return self.generate_markdown_output(processed_data)
else:
raise ValueError(f"Unsupported format type: {format_type}")
-
+
def generate_json_output(self, processed_data: List[Dict[str, Any]]) -> str:
- """生成JSON格式输出"""
import json
return json.dumps(processed_data, ensure_ascii=False, indent=2)
-
+
def generate_html_output(self, processed_data: List[Dict[str, Any]]) -> str:
- """生成HTML格式输出"""
return (
"<html><body>"
+ "".join(f"<p>{data}</p>" for data in processed_data)
+ "</body></html>"
)
-
+
def generate_markdown_output(self, processed_data: List[Dict[str, Any]]) -> str:
- """生成Markdown格式输出"""
return "\n".join(f"- {data}" for data in processed_data)
+
+ def get_statistics(self) -> Dict[str, Any]:
+ return {
+ "rule_count": self.rule_engine.rule_count(),
+ "custom_processor_count": len(self.custom_processors),
+ "has_rules_config": bool(self.rules),
+ }
diff --git a/src/conventionalrp/core/rules.py b/src/conventionalrp/core/rules.py
new file mode 100644
index 0000000..f198d4e
--- /dev/null
+++ b/src/conventionalrp/core/rules.py
@@ -0,0 +1,238 @@
+from typing import Dict, Any, Callable, List, Optional
+from enum import Enum
+import re
+
+
+class RuleCondition(Enum):
+ """规则条件类型"""
+ EQUALS = "equals"
+ CONTAINS = "contains"
+ MATCHES = "matches"
+ STARTS_WITH = "starts_with"
+ ENDS_WITH = "ends_with"
+ IN_LIST = "in_list"
+ GREATER_THAN = "greater_than"
+ LESS_THAN = "less_than"
+
+
+class Rule:
+ def __init__(
+ self,
+ name: str,
+ condition: Dict[str, Any],
+ action: Dict[str, Any],
+ priority: int = 50
+ ):
+ self.name = name
+ self.condition = condition
+ self.action = action
+ self.priority = priority
+ self._compiled_patterns = {}
+
+ self._precompile_patterns()
+
+ def _precompile_patterns(self):
+ """预编译正则表达式以提高性能"""
+ if isinstance(self.condition, dict):
+ for key, value in self.condition.items():
+ if isinstance(value, dict) and value.get("type") == "matches":
+ pattern = value.get("pattern")
+ if pattern:
+ self._compiled_patterns[key] = re.compile(pattern)
+
+ def matches(self, data: Dict[str, Any]) -> bool:
+ """
+ 检查数据是否匹配规则条件
+ """
+ if not isinstance(self.condition, dict):
+ return False
+
+ for field, condition_spec in self.condition.items():
+ if not self._check_field_condition(data, field, condition_spec):
+ return False
+
+ return True
+
+ def _check_field_condition(
+ self,
+ data: Dict[str, Any],
+ field: str,
+ condition: Any
+ ) -> bool:
+ """检查单个字段的条件"""
+ value = data.get(field)
+
+ if not isinstance(condition, dict):
+ return value == condition
+
+ condition_type = condition.get("type")
+ expected_value = condition.get("value")
+
+ if condition_type == "equals":
+ return value == expected_value
+ elif condition_type == "contains":
+ return expected_value in str(value) if value else False
+ elif condition_type == "matches":
+ if field in self._compiled_patterns:
+ pattern = self._compiled_patterns[field]
+ return bool(pattern.search(str(value))) if value else False
+ return False
+ elif condition_type == "starts_with":
+ return str(value).startswith(expected_value) if value else False
+ elif condition_type == "ends_with":
+ return str(value).endswith(expected_value) if value else False
+ elif condition_type == "in_list":
+ return value in expected_value if isinstance(expected_value, list) else False
+ elif condition_type == "greater_than":
+ try:
+ return float(value) > float(expected_value)
+ except (ValueError, TypeError):
+ return False
+ elif condition_type == "less_than":
+ try:
+ return float(value) < float(expected_value)
+ except (ValueError, TypeError):
+ return False
+
+ return False
+
+ def apply(self, data: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ 对匹配的数据应用规则动作
+ """
+ result = data.copy()
+
+ if not isinstance(self.action, dict):
+ return result
+
+ action_type = self.action.get("type")
+
+ if action_type == "set_field":
+ field = self.action.get("field")
+ value = self.action.get("value")
+ if field:
+ result[field] = value
+
+ elif action_type == "add_field":
+ field = self.action.get("field")
+ value = self.action.get("value")
+ if field and field not in result:
+ result[field] = value
+
+ elif action_type == "remove_field":
+ field = self.action.get("field")
+ if field and field in result:
+ del result[field]
+
+ elif action_type == "transform":
+ field = self.action.get("field")
+ func_name = self.action.get("function")
+ if field and field in result and func_name:
+ result[field] = self._apply_transform(result[field], func_name)
+
+ elif action_type == "add_tag":
+ tag = self.action.get("tag")
+ if tag:
+ if "tags" not in result:
+ result["tags"] = []
+ if tag not in result["tags"]:
+ result["tags"].append(tag)
+
+ elif action_type == "copy_field":
+ source = self.action.get("source")
+ target = self.action.get("target")
+ if source and target and source in result:
+ result[target] = result[source]
+
+ return result
+
+ def _apply_transform(self, value: Any, func_name: str) -> Any:
+ transforms = {
+ "upper": lambda x: str(x).upper(),
+ "lower": lambda x: str(x).lower(),
+ "strip": lambda x: str(x).strip(),
+ "int": lambda x: int(x),
+ "float": lambda x: float(x),
+ "len": lambda x: len(x) if hasattr(x, '__len__') else 0,
+ }
+
+ func = transforms.get(func_name)
+ if func:
+ try:
+ return func(value)
+ except Exception:
+ return value
+ return value
+
+ def __repr__(self) -> str:
+ return f"Rule(name={self.name}, priority={self.priority})"
+
+
+class RuleEngine:
+ """
+ 规则引擎
+ """
+
+ def __init__(self):
+ self.rules: List[Rule] = []
+ self._sorted = False
+
+ def add_rule(self, rule: Rule):
+ self.rules.append(rule)
+ self._sorted = False
+
+ def add_rule_dict(
+ self,
+ name: str,
+ condition: Dict[str, Any],
+ action: Dict[str, Any],
+ priority: int = 50
+ ):
+ """
+ 从字典添加规则
+ """
+ rule = Rule(name, condition, action, priority)
+ self.add_rule(rule)
+
+ def _ensure_sorted(self):
+ """确保规则按优先级排序"""
+ if not self._sorted:
+ self.rules.sort(key=lambda r: r.priority, reverse=True)
+ self._sorted = True
+
+ def process(
+ self,
+ data: Dict[str, Any],
+ apply_all: bool = False
+ ) -> Dict[str, Any]:
+ self._ensure_sorted()
+ result = data.copy()
+
+ for rule in self.rules:
+ if rule.matches(result):
+ result = rule.apply(result)
+ if not apply_all:
+ break
+
+ return result
+
+ def process_batch(
+ self,
+ data_list: List[Dict[str, Any]],
+ apply_all: bool = False
+ ) -> List[Dict[str, Any]]:
+ return [self.process(data, apply_all) for data in data_list]
+
+ def find_matching_rules(self, data: Dict[str, Any]) -> List[Rule]:
+ self._ensure_sorted()
+ return [rule for rule in self.rules if rule.matches(data)]
+
+ def clear_rules(self):
+ self.rules.clear()
+ self._sorted = False
+
+ def rule_count(self) -> int:
+ return len(self.rules)
+
+ def __repr__(self) -> str:
+ return f"RuleEngine(rules={len(self.rules)})"
diff --git a/src/conventionalrp/extractors/__init__.py b/src/conventionalrp/extractors/__init__.py
index e693e03..e69de29 100644
--- a/src/conventionalrp/extractors/__init__.py
+++ b/src/conventionalrp/extractors/__init__.py
@@ -1,3 +0,0 @@
-"""
-This file initializes the extractors module.
-"""
diff --git a/src/conventionalrp/extractors/rule_extractor.py b/src/conventionalrp/extractors/rule_extractor.py
index bfc60c8..12b9e4b 100644
--- a/src/conventionalrp/extractors/rule_extractor.py
+++ b/src/conventionalrp/extractors/rule_extractor.py
@@ -15,12 +15,6 @@ class RuleExtractor(BaseExtractor):
"""规则提取器,用于从配置文件加载解析规则"""
def __init__(self, config_file: Optional[str] = None):
- """
- 初始化规则提取器
-
- Args:
- config_file: 规则配置文件路径(可选)
- """
self.config_file = config_file
self.rules: Dict[str, Any] = {}
if config_file:
@@ -29,16 +23,6 @@ class RuleExtractor(BaseExtractor):
def load_rules_from_file(self, config_file: str) -> Dict[str, Any]:
"""
从文件加载规则
-
- Args:
- config_file: 规则配置文件路径
-
- Returns:
- 解析后的规则字典
-
- Raises:
- FileNotFoundError: 文件不存在
- ValueError: 文件内容为空或格式错误
"""
if not Path(config_file).exists():
raise FileNotFoundError(f"Rule file not found: {config_file}")
@@ -56,12 +40,6 @@ class RuleExtractor(BaseExtractor):
def load_rules(self, config_file: str) -> Dict[str, Any]:
"""
加载规则(兼容旧接口)
-
- Args:
- config_file: 规则配置文件路径
-
- Returns:
- 解析后的规则字典
"""
self.rules = self.load_rules_from_file(config_file)
return self.rules
@@ -69,8 +47,5 @@ class RuleExtractor(BaseExtractor):
def extract(self) -> Dict[str, Any]:
"""
提取规则
-
- Returns:
- 规则字典
"""
return self.rules
diff --git a/src/conventionalrp/html_renderer.py b/src/conventionalrp/html_renderer.py
deleted file mode 100644
index e69de29..0000000
--- a/src/conventionalrp/html_renderer.py
+++ /dev/null
diff --git a/src/conventionalrp/json_renderer.py b/src/conventionalrp/json_renderer.py
deleted file mode 100644
index e69de29..0000000
--- a/src/conventionalrp/json_renderer.py
+++ /dev/null
diff --git a/src/conventionalrp/markdown_renderer.py b/src/conventionalrp/markdown_renderer.py
deleted file mode 100644
index e69de29..0000000
--- a/src/conventionalrp/markdown_renderer.py
+++ /dev/null
diff --git a/src/conventionalrp/plugins/__init__.py b/src/conventionalrp/plugins/__init__.py
index c7bbee6..e759fd3 100644
--- a/src/conventionalrp/plugins/__init__.py
+++ b/src/conventionalrp/plugins/__init__.py
@@ -1,3 +1,17 @@
-"""
-This file initializes the plugins module.
-"""
+from .base import (
+ Plugin,
+ ParserPlugin,
+ ProcessorPlugin,
+ RendererPlugin,
+ AnalyzerPlugin,
+)
+from .plugin_manager import PluginManager
+
+__all__ = [
+ "Plugin",
+ "ParserPlugin",
+ "ProcessorPlugin",
+ "RendererPlugin",
+ "AnalyzerPlugin",
+ "PluginManager",
+]
diff --git a/src/conventionalrp/plugins/base.py b/src/conventionalrp/plugins/base.py
new file mode 100644
index 0000000..f9e92a7
--- /dev/null
+++ b/src/conventionalrp/plugins/base.py
@@ -0,0 +1,103 @@
+from typing import Any, Dict, List, Optional
+from abc import ABC, abstractmethod
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class Plugin(ABC):
+ """
+ 插件基类
+
+ 所有插件必须继承此类并实现必要的方法
+ """
+
+ def __init__(self, name: str, version: str = "1.0.0"):
+ self.name = name
+ self.version = version
+ self.enabled = True
+ self.logger = logging.getLogger(f"conventionalrp.plugins.{name}")
+
+ @abstractmethod
+ def initialize(self, config: Optional[Dict[str, Any]] = None):
+ pass
+
+ @abstractmethod
+ def process(self, data: Any) -> Any:
+ pass
+
+ def on_enable(self):
+ """插件启用时调用"""
+ self.logger.info(f"Plugin {self.name} enabled")
+
+ def on_disable(self):
+ """插件禁用时调用"""
+ self.logger.info(f"Plugin {self.name} disabled")
+
+ def get_metadata(self) -> Dict[str, Any]:
+ return {
+ "name": self.name,
+ "version": self.version,
+ "enabled": self.enabled,
+ "type": self.__class__.__name__,
+ }
+
+ def __repr__(self) -> str:
+ return f"Plugin(name={self.name}, version={self.version}, enabled={self.enabled})"
+
+
+class ParserPlugin(Plugin):
+ def __init__(self, name: str, version: str = "1.0.0"):
+ super().__init__(name, version)
+ self.priority = 50
+
+ @abstractmethod
+ def can_parse(self, text: str) -> bool:
+ """
+ 判断是否可以解析给定文本
+ """
+ pass
+
+ @abstractmethod
+ def parse(self, text: str) -> List[Dict[str, Any]]:
+ """
+ 解析文本
+ """
+ pass
+
+
+class ProcessorPlugin(Plugin):
+ @abstractmethod
+ def process_token(self, token: Dict[str, Any]) -> Dict[str, Any]:
+ """
+ 处理单个 token
+ """
+ pass
+
+ def process(self, data: Any) -> Any:
+ """
+ 处理数据(实现基类方法)
+ """
+ if isinstance(data, dict):
+ return self.process_token(data)
+ elif isinstance(data, list):
+ return [self.process_token(token) for token in data]
+ return data
+
+
+class RendererPlugin(Plugin):
+ @abstractmethod
+ def render(self, data: Any) -> str:
+ pass
+
+ def process(self, data: Any) -> Any:
+ return self.render(data)
+
+
+class AnalyzerPlugin(Plugin):
+ @abstractmethod
+ def analyze(self, data: Any) -> Dict[str, Any]:
+ pass
+
+ def process(self, data: Any) -> Any:
+ return self.analyze(data)
diff --git a/src/conventionalrp/plugins/plugin_manager.py b/src/conventionalrp/plugins/plugin_manager.py
index 0d49a9c..0230bda 100644
--- a/src/conventionalrp/plugins/plugin_manager.py
+++ b/src/conventionalrp/plugins/plugin_manager.py
@@ -1,19 +1,204 @@
+"""
+插件管理器
+"""
+
import os
+import sys
import importlib
+import importlib.util
+from pathlib import Path
+from typing import Dict, List, Optional, Type, Any
+import logging
+
+from .base import Plugin
+
+logger = logging.getLogger(__name__)
class PluginManager:
- def __init__(self, plugin_dir: str):
- self.plugin_dir = plugin_dir
- self.plugins = []
+ def __init__(self, plugin_dirs: Optional[List[str]] = None):
+ self.plugin_dirs = plugin_dirs or []
+ self.plugins: Dict[str, Plugin] = {}
+ self.plugin_classes: Dict[str, Type[Plugin]] = {}
+
+ logger.info("PluginManager initialized with %d directories",
+ len(self.plugin_dirs))
+
+ def add_plugin_dir(self, directory: str):
- def load_plugins(self):
- for plugin in os.listdir(self.plugin_dir):
- if plugin.endswith(".py"):
- plugin_name = plugin.split(".")[0]
- module = importlib.import_module(f"{self.plugin_dir}.{plugin_name}")
- self.plugins.append(module)
+ if directory not in self.plugin_dirs:
+ self.plugin_dirs.append(directory)
+ logger.info(f"Added plugin directory: {directory}")
+
+ def discover_plugins(self) -> List[str]:
+ discovered = []
+
+ for plugin_dir in self.plugin_dirs:
+ path = Path(plugin_dir)
+ if not path.exists():
+ logger.warning(f"Plugin directory does not exist: {plugin_dir}")
+ continue
+
+ if str(path.parent) not in sys.path:
+ sys.path.insert(0, str(path.parent))
- def run_plugins(self):
- for plugin in self.plugins:
- plugin.run()
+ for py_file in path.glob("*.py"):
+ if py_file.name.startswith("_"):
+ continue
+
+ module_name = py_file.stem
+ discovered.append(module_name)
+ logger.debug(f"Discovered plugin module: {module_name}")
+
+ logger.info(f"Discovered {len(discovered)} plugin modules")
+ return discovered
+
+ def load_plugin_from_file(self, file_path: str) -> Optional[Type[Plugin]]:
+ try:
+ module_name = Path(file_path).stem
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
+
+ if spec is None or spec.loader is None:
+ logger.error(f"Failed to load plugin spec: {file_path}")
+ return None
+
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+
+ for name in dir(module):
+ obj = getattr(module, name)
+ if (isinstance(obj, type) and
+ issubclass(obj, Plugin) and
+ obj is not Plugin):
+ logger.info(f"Loaded plugin class: {name} from {file_path}")
+ return obj
+
+ logger.warning(f"No Plugin subclass found in: {file_path}")
+ return None
+
+ except Exception as e:
+ logger.error(f"Error loading plugin from {file_path}: {e}")
+ return None
+
+ def load_plugin(self, module_name: str) -> Optional[str]:
+ try:
+ module = importlib.import_module(module_name)
+
+ # 查找 Plugin 子类
+ for name in dir(module):
+ obj = getattr(module, name)
+ if (isinstance(obj, type) and
+ issubclass(obj, Plugin) and
+ obj is not Plugin):
+
+ plugin_class = obj
+ self.plugin_classes[name] = plugin_class
+ logger.info(f"Loaded plugin class: {name}")
+ return name
+
+ logger.warning(f"No Plugin subclass found in module: {module_name}")
+ return None
+
+ except Exception as e:
+ logger.error(f"Error loading plugin module {module_name}: {e}")
+ return None
+
+ def register_plugin(
+ self,
+ plugin: Plugin,
+ replace: bool = False
+ ) -> bool:
+ if plugin.name in self.plugins and not replace:
+ logger.warning(f"Plugin {plugin.name} already registered")
+ return False
+
+ self.plugins[plugin.name] = plugin
+ plugin.on_enable()
+ logger.info(f"Registered plugin: {plugin.name}")
+ return True
+
+ def unregister_plugin(self, plugin_name: str) -> bool:
+ if plugin_name not in self.plugins:
+ logger.warning(f"Plugin {plugin_name} not found")
+ return False
+
+ plugin = self.plugins[plugin_name]
+ plugin.on_disable()
+ del self.plugins[plugin_name]
+ logger.info(f"Unregistered plugin: {plugin_name}")
+ return True
+
+ def get_plugin(self, plugin_name: str) -> Optional[Plugin]:
+ return self.plugins.get(plugin_name)
+
+ def list_plugins(self) -> List[Dict[str, Any]]:
+ return [plugin.get_metadata() for plugin in self.plugins.values()]
+
+ def enable_plugin(self, plugin_name: str) -> bool:
+ plugin = self.plugins.get(plugin_name)
+ if plugin is None:
+ logger.warning(f"Plugin {plugin_name} not found")
+ return False
+
+ if not plugin.enabled:
+ plugin.enabled = True
+ plugin.on_enable()
+ logger.info(f"Enabled plugin: {plugin_name}")
+
+ return True
+
+ def disable_plugin(self, plugin_name: str) -> bool:
+ plugin = self.plugins.get(plugin_name)
+ if plugin is None:
+ logger.warning(f"Plugin {plugin_name} not found")
+ return False
+
+ if plugin.enabled:
+ plugin.enabled = False
+ plugin.on_disable()
+ logger.info(f"Disabled plugin: {plugin_name}")
+
+ return True
+
+ def execute_plugins(
+ self,
+ data: Any,
+ plugin_type: Optional[Type[Plugin]] = None
+ ) -> Any:
+ result = data
+
+ for plugin in self.plugins.values():
+ if not plugin.enabled:
+ continue
+
+ if plugin_type is not None and not isinstance(plugin, plugin_type):
+ continue
+
+ try:
+ result = plugin.process(result)
+ logger.debug(f"Executed plugin: {plugin.name}")
+ except Exception as e:
+ logger.error(f"Error executing plugin {plugin.name}: {e}")
+
+ return result
+
+ def clear_plugins(self):
+ for plugin_name in list(self.plugins.keys()):
+ self.unregister_plugin(plugin_name)
+
+ self.plugin_classes.clear()
+ logger.info("Cleared all plugins")
+
+ def get_statistics(self) -> Dict[str, Any]:
+ enabled_count = sum(1 for p in self.plugins.values() if p.enabled)
+
+ return {
+ "total_plugins": len(self.plugins),
+ "enabled_plugins": enabled_count,
+ "disabled_plugins": len(self.plugins) - enabled_count,
+ "plugin_classes": len(self.plugin_classes),
+ "plugin_directories": len(self.plugin_dirs),
+ }
+
+ def __repr__(self) -> str:
+ return f"PluginManager(plugins={len(self.plugins)}, enabled={sum(1 for p in self.plugins.values() if p.enabled)})"
diff --git a/src/conventionalrp/renderers/__init__.py b/src/conventionalrp/renderers/__init__.py
index 6d5ac7a..e69de29 100644
--- a/src/conventionalrp/renderers/__init__.py
+++ b/src/conventionalrp/renderers/__init__.py
@@ -1,3 +0,0 @@
-"""
-This file initializes the renderers module.
-"""
diff --git a/src/conventionalrp/renderers/html_renderer.py b/src/conventionalrp/renderers/html_renderer.py
index 75efcd3..d435fc9 100644
--- a/src/conventionalrp/renderers/html_renderer.py
+++ b/src/conventionalrp/renderers/html_renderer.py
@@ -1,22 +1,423 @@
+from typing import Any, Dict, List, Optional
from .base import BaseRenderer
class HTMLRenderer(BaseRenderer):
- def __init__(self):
+ THEMES = {
+ "light": {
+ "bg_color": "#ffffff",
+ "text_color": "#333333",
+ "header_bg": "#f5f5f5",
+ "border_color": "#e0e0e0",
+ "dialogue_color": "#4CAF50",
+ "dice_color": "#2196F3",
+ "narration_color": "#FF9800",
+ "system_color": "#9E9E9E",
+ "success_color": "#43a047",
+ "failure_color": "#e53935",
+ "code_bg": "#f5f5f5",
+ },
+ "dark": {
+ "bg_color": "#1e1e1e",
+ "text_color": "#d4d4d4",
+ "header_bg": "#2d2d30",
+ "border_color": "#3e3e42",
+ "dialogue_color": "#6adb8d",
+ "dice_color": "#5fb3f5",
+ "narration_color": "#ffb74d",
+ "system_color": "#bdbdbd",
+ "success_color": "#66bb6a",
+ "failure_color": "#ef5350",
+ "code_bg": "#2d2d30",
+ },
+ "fantasy": {
+ "bg_color": "#f9f6f1",
+ "text_color": "#3e2723",
+ "header_bg": "#d7ccc8",
+ "border_color": "#bcaaa4",
+ "dialogue_color": "#8d6e63",
+ "dice_color": "#5d4037",
+ "narration_color": "#795548",
+ "system_color": "#a1887f",
+ "success_color": "#7cb342",
+ "failure_color": "#c62828",
+ "code_bg": "#efebe9",
+ },
+ }
+
+ def __init__(self, theme: str = "light", custom_css: Optional[str] = None):
super().__init__()
- self.title = "TRPG Log Output"
-
- def render(self, data):
- html_content = f"<html><head><title>{self.title}</title></head><body>"
- html_content += "<h1>TRPG Log Output</h1>"
- html_content += "<ul>"
-
- for entry in data:
- html_content += f"<li>{entry}</li>"
-
- html_content += "</ul></body></html>"
- return html_content
-
+ self.title = "Rendered Log"
+ self.theme = theme if theme in self.THEMES else "light"
+ self.custom_css = custom_css
+
+ def _get_css(self) -> str:
+ colors = self.THEMES[self.theme]
+
+ css = f"""
+ * {{
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }}
+
+ body {{
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ background-color: {colors['bg_color']};
+ color: {colors['text_color']};
+ line-height: 1.6;
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+ }}
+
+ header {{
+ background: linear-gradient(135deg, {colors['header_bg']} 0%, {colors['border_color']} 100%);
+ padding: 30px;
+ border-radius: 10px;
+ margin-bottom: 30px;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ }}
+
+ h1 {{
+ font-size: 2.5em;
+ font-weight: 700;
+ margin-bottom: 10px;
+ color: {colors['text_color']};
+ }}
+
+ .subtitle {{
+ font-size: 1.1em;
+ opacity: 0.8;
+ }}
+
+ .stats {{
+ display: flex;
+ gap: 20px;
+ margin-top: 20px;
+ flex-wrap: wrap;
+ }}
+
+ .stat-item {{
+ background: {colors['bg_color']};
+ padding: 10px 20px;
+ border-radius: 5px;
+ font-size: 0.9em;
+ border: 1px solid {colors['border_color']};
+ }}
+
+ .stat-label {{
+ opacity: 0.7;
+ margin-right: 5px;
+ }}
+
+ .stat-value {{
+ font-weight: bold;
+ }}
+
+ .content-wrapper {{
+ background: {colors['bg_color']};
+ border-radius: 10px;
+ padding: 20px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ }}
+
+ .token {{
+ margin: 15px 0;
+ padding: 15px;
+ border-left: 4px solid {colors['border_color']};
+ border-radius: 5px;
+ background: {colors['header_bg']};
+ transition: transform 0.2s, box-shadow 0.2s;
+ }}
+
+ .token:hover {{
+ transform: translateX(5px);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+ }}
+
+ .token.dialogue {{
+ border-left-color: {colors['dialogue_color']};
+ }}
+
+ .token.dice {{
+ border-left-color: {colors['dice_color']};
+ }}
+
+ .token.narration {{
+ border-left-color: {colors['narration_color']};
+ }}
+
+ .token.system {{
+ border-left-color: {colors['system_color']};
+ }}
+
+ .token.success {{
+ border-left-color: {colors['success_color']};
+ background: {colors['success_color']}15;
+ }}
+
+ .token.failure {{
+ border-left-color: {colors['failure_color']};
+ background: {colors['failure_color']}15;
+ }}
+
+ .token-header {{
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+ }}
+
+ .type-badge {{
+ display: inline-block;
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-size: 0.75em;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }}
+
+ .type-badge.dialogue {{
+ background: {colors['dialogue_color']};
+ color: white;
+ }}
+
+ .type-badge.dice {{
+ background: {colors['dice_color']};
+ color: white;
+ }}
+
+ .type-badge.narration {{
+ background: {colors['narration_color']};
+ color: white;
+ }}
+
+ .type-badge.system {{
+ background: {colors['system_color']};
+ color: white;
+ }}
+
+ .speaker {{
+ font-weight: 700;
+ font-size: 1.1em;
+ color: {colors['text_color']};
+ }}
+
+ .content {{
+ margin-top: 8px;
+ line-height: 1.8;
+ font-size: 1em;
+ }}
+
+ .metadata {{
+ display: flex;
+ gap: 15px;
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid {colors['border_color']};
+ font-size: 0.85em;
+ opacity: 0.7;
+ flex-wrap: wrap;
+ }}
+
+ .metadata-item {{
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }}
+
+ .tags {{
+ display: flex;
+ gap: 5px;
+ flex-wrap: wrap;
+ }}
+
+ .tag {{
+ background: {colors['code_bg']};
+ padding: 2px 8px;
+ border-radius: 3px;
+ font-size: 0.85em;
+ border: 1px solid {colors['border_color']};
+ }}
+
+ code {{
+ background: {colors['code_bg']};
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9em;
+ }}
+
+ .dice-result {{
+ display: inline-block;
+ background: {colors['dice_color']};
+ color: white;
+ padding: 3px 10px;
+ border-radius: 5px;
+ font-weight: bold;
+ margin-left: 5px;
+ }}
+
+ footer {{
+ margin-top: 40px;
+ padding: 20px;
+ text-align: center;
+ opacity: 0.6;
+ font-size: 0.9em;
+ }}
+
+ @media (max-width: 768px) {{
+ body {{
+ padding: 10px;
+ }}
+
+ h1 {{
+ font-size: 1.8em;
+ }}
+
+ .token {{
+ padding: 10px;
+ }}
+ }}
+ """
+
+ if self.custom_css:
+ css += "\n" + self.custom_css
+
+ return css
+
+ def render(self, data: List[Dict[str, Any]]) -> str:
+ if data and not isinstance(data[0], dict):
+ data = [{"type": "text", "content": str(item)} for item in data]
+
+ stats = self._calculate_stats(data)
+
+ html = f"""<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{self.title}</title>
+ <style>{self._get_css()}</style>
+</head>
+<body>
+ <header>
+ <h1>🎲 {self.title}</h1>
+ <div class="subtitle">完整的游戏日志与数据分析</div>
+ <div class="stats">
+ <div class="stat-item">
+ <span class="stat-label">总条目:</span>
+ <span class="stat-value">{stats['total']}</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">对话:</span>
+ <span class="stat-value">{stats['dialogue']}</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">骰子:</span>
+ <span class="stat-value">{stats['dice']}</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">旁白:</span>
+ <span class="stat-value">{stats['narration']}</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">系统:</span>
+ <span class="stat-value">{stats['system']}</span>
+ </div>
+ </div>
+ </header>
+
+ <div class="content-wrapper">
+"""
+
+ for item in data:
+ html += self._render_token(item)
+
+ html += """ </div>
+
+ <footer>
+ <p>Generated by ConventionalRP HTML Renderer</p>
+ <p>Theme: """ + self.theme.capitalize() + """</p>
+ </footer>
+</body>
+</html>"""
+
+ return html
+
+ def _render_token(self, token: Dict[str, Any]) -> str:
+ token_type = token.get("type", "unknown")
+ speaker = token.get("speaker", "")
+ content = str(token.get("content", ""))
+
+ content = (content
+ .replace("&", "&amp;")
+ .replace("<", "&lt;")
+ .replace(">", "&gt;")
+ .replace('"', "&quot;"))
+
+ if token_type == "dice" and "result" in token:
+ content += f' <span class="dice-result">{token["result"]}</span>'
+
+ html = f' <div class="token {token_type}">\n'
+ html += ' <div class="token-header">\n'
+ html += f' <span class="type-badge {token_type}">{token_type}</span>\n'
+
+ if speaker:
+ html += f' <span class="speaker">{speaker}</span>\n'
+
+ html += ' </div>\n'
+ html += f' <div class="content">{content}</div>\n'
+
+ metadata_items = []
+
+ if "timestamp" in token:
+ metadata_items.append(f'⏰ {token["timestamp"]}')
+
+ if "tags" in token and token["tags"]:
+ tags_html = '<div class="tags">'
+ for tag in token["tags"]:
+ tags_html += f'<span class="tag">{tag}</span>'
+ tags_html += '</div>'
+ metadata_items.append(tags_html)
+
+ if "combat_data" in token:
+ combat = token["combat_data"]
+ if combat.get("type") == "damage":
+ metadata_items.append(f'⚔️ 伤害: {combat["amount"]}')
+ elif combat.get("type") == "healing":
+ metadata_items.append(f'💚 治疗: {combat["amount"]}')
+
+ if metadata_items:
+ html += ' <div class="metadata">\n'
+ for item in metadata_items:
+ html += f' <div class="metadata-item">{item}</div>\n'
+ html += ' </div>\n'
+
+ html += ' </div>\n'
+
+ return html
+
+ def _calculate_stats(self, data: List[Dict[str, Any]]) -> Dict[str, int]:
+ stats = {
+ "total": len(data),
+ "dialogue": 0,
+ "dice": 0,
+ "narration": 0,
+ "system": 0,
+ }
+
+ for item in data:
+ token_type = item.get("type", "unknown")
+ if token_type in stats:
+ stats[token_type] += 1
+
+ return stats
+
def set_style(self, style):
- # Implement style setting if needed
- pass
+ """设置样式(向后兼容)"""
+ if style in self.THEMES:
+ self.theme = style
+
diff --git a/src/conventionalrp/renderers/json_renderer.py b/src/conventionalrp/renderers/json_renderer.py
index 8dcdd6d..42d5fd4 100644
--- a/src/conventionalrp/renderers/json_renderer.py
+++ b/src/conventionalrp/renderers/json_renderer.py
@@ -1,11 +1,78 @@
+import json
+from typing import Any, Dict, List
from .base import BaseRenderer
class JSONRenderer(BaseRenderer):
- def render(self, data):
- import json
-
- return json.dumps(data, ensure_ascii=False, indent=4)
+ def __init__(self, pretty: bool = True, indent: int = 2, sort_keys: bool = False):
+ super().__init__()
+ self.pretty = pretty
+ self.indent = indent if pretty else None
+ self.sort_keys = sort_keys
+ self.style = {}
+
+ def render(self, data: Any) -> str:
+ if self.pretty and isinstance(data, list):
+ output = {
+ "metadata": {
+ "total_entries": len(data),
+ "renderer": "ConventionalRP JSONRenderer",
+ "version": "1.0.0",
+ },
+ "statistics": self._calculate_stats(data),
+ "data": data,
+ }
+ else:
+ output = data
+
+ return json.dumps(
+ output,
+ ensure_ascii=False,
+ indent=self.indent,
+ sort_keys=self.sort_keys,
+ )
+
+ def _calculate_stats(self, data: List[Dict[str, Any]]) -> Dict[str, Any]:
+ stats = {
+ "dialogue": 0,
+ "dice": 0,
+ "narration": 0,
+ "system": 0,
+ "success": 0,
+ "failure": 0,
+ "other": 0,
+ }
+
+ speakers = set()
+
+ for item in data:
+ if not isinstance(item, dict):
+ continue
+
+ token_type = item.get("type", "unknown")
+ if token_type in stats:
+ stats[token_type] += 1
+ else:
+ stats["other"] += 1
+
+ if "speaker" in item and item["speaker"]:
+ speakers.add(item["speaker"])
+
+ stats["unique_speakers"] = len(speakers)
+ stats["speakers"] = sorted(list(speakers))
+
+ return stats
def set_style(self, style):
- self.style = style # Placeholder for potential styling options in the future
+ self.style = style
+
+ # 从 style 中提取设置
+ if isinstance(style, dict):
+ if "pretty" in style:
+ self.pretty = style["pretty"]
+ self.indent = 2 if self.pretty else None
+ if "indent" in style:
+ self.indent = style["indent"]
+ if "sort_keys" in style:
+ self.sort_keys = style["sort_keys"]
+
diff --git a/src/conventionalrp/renderers/markdown_renderer.py b/src/conventionalrp/renderers/markdown_renderer.py
index 9df59a2..7080913 100644
--- a/src/conventionalrp/renderers/markdown_renderer.py
+++ b/src/conventionalrp/renderers/markdown_renderer.py
@@ -1,18 +1,19 @@
+"""
+Markdown 渲染器
+"""
+
+from typing import Any, Dict, List, Union
from .base import BaseRenderer
-from typing import List, Dict, Any, Union
class MarkdownRenderer(BaseRenderer):
+ def __init__(self, enable_syntax_hints: bool = True, enable_emoji: bool = True):
+ super().__init__()
+ self.enable_syntax_hints = enable_syntax_hints
+ self.enable_emoji = enable_emoji
+ self.style = {}
+
def render(self, data: Union[List[Dict[str, Any]], Dict[str, Any]]) -> str:
- """
- Renders the given data in Markdown format.
-
- Args:
- data: The data to render (can be list or dict).
-
- Returns:
- str: The rendered Markdown string.
- """
if isinstance(data, list):
return self._render_list(data)
elif isinstance(data, dict):
@@ -21,40 +22,141 @@ class MarkdownRenderer(BaseRenderer):
return str(data)
def _render_list(self, data: List[Dict[str, Any]]) -> str:
- """渲染列表数据为 Markdown"""
- markdown_output = "# TRPG Log\n\n"
-
- for i, entry in enumerate(data, 1):
- if entry.get("type") == "metadata":
- markdown_output += f"## Entry {i}\n\n"
- markdown_output += f"**Timestamp**: {entry.get('timestamp', 'N/A')} \n"
- markdown_output += f"**Speaker**: {entry.get('speaker', 'N/A')} \n\n"
-
- content_items = entry.get("content", [])
- if content_items:
- markdown_output += "**Content**:\n\n"
- for content in content_items:
- content_type = content.get("type", "unknown")
- content_text = content.get("content", "")
- markdown_output += f"- [{content_type}] {content_text}\n"
- markdown_output += "\n"
- else:
- markdown_output += f"- {entry}\n"
+ if data and not isinstance(data[0], dict):
+ data = [{"type": "text", "content": str(item)} for item in data]
- return markdown_output
+ stats = self._calculate_stats(data)
+
+ emoji_prefix = "🎲 " if self.enable_emoji else ""
+ md = f"# {emoji_prefix}\n\n"
+
+ md += "## Statistics\n\n"
+ md += "| Type | Count |\n"
+ md += "|------|------|\n"
+ md += f"| Total | {stats['total']} |\n"
+ md += f"| Dialogue | {stats['dialogue']} |\n"
+ md += f"| Dice | {stats['dice']} |\n"
+ md += f"| Narration | {stats['narration']} |\n"
+ md += f"| System | {stats['system']} |\n"
+ md += f"| Metadata | {stats['metadata']} |\n\n"
+
+ md += "---\n\n"
+ md += "## Detailed Log\n\n"
+
+ # Render each token
+ for i, item in enumerate(data, 1):
+ md += self._render_token(item, i)
+ md += "\n"
+
+ md += "---\n\n"
+ md += "_Generated by ConventionalRP Markdown Renderer_\n"
+
+ return md
+
+ def _render_token(self, token: Dict[str, Any], index: int) -> str:
+ token_type = token.get("type", "unknown")
+
+ # 处理元数据类型(向后兼容)
+ if token_type == "metadata":
+ return self._render_metadata_token(token, index)
+
+ speaker = token.get("speaker", "")
+ content = str(token.get("content", ""))
+
+ type_emojis = {
+ "dialogue": "💬",
+ "dice": "🎲",
+ "narration": "📖",
+ "system": "⚙️",
+ "success": "✅",
+ "failure": "❌",
+ "text": "📄",
+ "unknown": "❓",
+ }
+
+ emoji = type_emojis.get(token_type, "•") if self.enable_emoji else "-"
+
+ md = f"### {emoji} [{index}] {token_type.upper()}\n\n"
+
+ if speaker:
+ md += f"**说话者:** {speaker}\n\n"
+
+ if token_type == "dice" and self.enable_syntax_hints:
+ md += f"```dice\n{content}\n```\n\n"
+
+ if "result" in token:
+ md += f"**结果:** `{token['result']}`\n\n"
+ else:
+ md += f"{content}\n\n"
+
+ metadata_lines = []
+
+ if "timestamp" in token:
+ metadata_lines.append(f"Timestamp: `{token['timestamp']}`")
+
+ if "tags" in token and token["tags"]:
+ tags_str = " ".join([f"`{tag}`" for tag in token["tags"]])
+ metadata_lines.append(f"Tags: {tags_str}")
+
+ if "combat_data" in token:
+ combat = token["combat_data"]
+ if combat.get("type") == "damage":
+ metadata_lines.append(f"Damage: **{combat['amount']}** (Total: {combat.get('total_damage', '?')})")
+ elif combat.get("type") == "healing":
+ metadata_lines.append(f"Healing: **{combat['amount']}** (Total: {combat.get('total_healing', '?')})")
+
+ if metadata_lines:
+ md += "> " + "\n> ".join(metadata_lines) + "\n\n"
+
+ return md
+
+ def _render_metadata_token(self, token: Dict[str, Any], index: int) -> str:
+ """渲染元数据类型的 token(向后兼容)"""
+ md = f"### Entry {index}\n\n"
+ md += f"**Timestamp**: {token.get('timestamp', 'N/A')} \n"
+ md += f"**Speaker**: {token.get('speaker', 'N/A')} \n\n"
+
+ content_items = token.get("content", [])
+ if content_items:
+ md += "**Content**:\n\n"
+ for content in content_items:
+ content_type = content.get("type", "unknown")
+ content_text = content.get("content", "")
+ md += f"- [{content_type}] {content_text}\n"
+ md += "\n"
+
+ return md
def _render_dict(self, data: Dict[str, Any]) -> str:
- """渲染字典数据为 Markdown"""
markdown_output = ""
for key, value in data.items():
markdown_output += f"## {key}\n\n{value}\n\n"
return markdown_output
+
+ def _calculate_stats(self, data: List[Dict[str, Any]]) -> Dict[str, int]:
+ stats = {
+ "total": len(data),
+ "dialogue": 0,
+ "dice": 0,
+ "narration": 0,
+ "system": 0,
+ "metadata": 0,
+ }
+
+ for item in data:
+ token_type = item.get("type", "unknown")
+ if token_type in stats:
+ stats[token_type] += 1
+
+ return stats
def set_style(self, style):
- """
- Sets the style for the Markdown renderer.
+ self.style = style
+
+ # 从 style 中提取设置
+ if isinstance(style, dict):
+ if "emoji" in style:
+ self.enable_emoji = style["emoji"]
+ if "syntax_hints" in style:
+ self.enable_syntax_hints = style["syntax_hints"]
- Args:
- style (dict): A dictionary of style options.
- """
- self.style = style # Currently, Markdown does not support styling, but this can be extended.
diff --git a/src/conventionalrp/tokenizer.py b/src/conventionalrp/tokenizer.py
deleted file mode 100644
index e69de29..0000000
--- a/src/conventionalrp/tokenizer.py
+++ /dev/null
diff --git a/src/conventionalrp/utils/__init__.py b/src/conventionalrp/utils/__init__.py
index 53b7600..dadb3cd 100644
--- a/src/conventionalrp/utils/__init__.py
+++ b/src/conventionalrp/utils/__init__.py
@@ -1 +1,32 @@
-"""This file initializes the utils module."""
+from .exceptions import (
+ ConventionalRPError,
+ ParserError,
+ RuleError,
+ ProcessorError,
+ ValidationError,
+ ConfigurationError,
+ safe_execute,
+ format_error,
+ validate_not_none,
+ validate_type,
+ validate_not_empty,
+)
+from .logging_config import setup_logging, get_logger, LogContext
+
+__all__ = [
+ "ConventionalRPError",
+ "ParserError",
+ "RuleError",
+ "ProcessorError",
+ "ValidationError",
+ "ConfigurationError",
+ "safe_execute",
+ "format_error",
+ "validate_not_none",
+ "validate_type",
+ "validate_not_empty",
+ "setup_logging",
+ "get_logger",
+ "LogContext",
+]
+
diff --git a/src/conventionalrp/utils/exceptions.py b/src/conventionalrp/utils/exceptions.py
new file mode 100644
index 0000000..28fa179
--- /dev/null
+++ b/src/conventionalrp/utils/exceptions.py
@@ -0,0 +1,104 @@
+from typing import Optional, Any, Dict
+import traceback
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+class ConventionalRPError(Exception):
+ """基础异常类"""
+
+ def __init__(
+ self,
+ message: str,
+ details: Optional[Dict[str, Any]] = None,
+ cause: Optional[Exception] = None
+ ):
+ super().__init__(message)
+ self.message = message
+ self.details = details or {}
+ self.cause = cause
+
+ def __str__(self) -> str:
+ result = self.message
+ if self.details:
+ details_str = ", ".join(f"{k}={v}" for k, v in self.details.items())
+ result += f" ({details_str})"
+ if self.cause:
+ result += f"\nCaused by: {str(self.cause)}"
+ return result
+
+
+class ParserError(ConventionalRPError):
+ """解析错误"""
+ pass
+
+
+class RuleError(ConventionalRPError):
+ """规则相关错误"""
+ pass
+
+
+class ProcessorError(ConventionalRPError):
+ """处理器错误"""
+ pass
+
+
+class ValidationError(ConventionalRPError):
+ """验证错误"""
+ pass
+
+
+class ConfigurationError(ConventionalRPError):
+ """配置错误"""
+ pass
+
+
+def safe_execute(func, *args, default=None, error_msg="Operation failed", **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ logger.error(f"{error_msg}: {e}")
+ return default
+
+
+def format_error(error: Exception, include_traceback: bool = False) -> str:
+ error_type = type(error).__name__
+ error_msg = str(error)
+
+ result = f"[{error_type}] {error_msg}"
+
+ if include_traceback:
+ tb = traceback.format_exc()
+ result += f"\n\nTraceback:\n{tb}"
+
+ return result
+
+
+def validate_not_none(value: Any, name: str):
+ if value is None:
+ raise ValidationError(
+ f"{name} cannot be None",
+ details={"parameter": name}
+ )
+
+
+def validate_type(value: Any, expected_type: type, name: str):
+ if not isinstance(value, expected_type):
+ raise ValidationError(
+ f"{name} must be of type {expected_type.__name__}, "
+ f"got {type(value).__name__}",
+ details={
+ "parameter": name,
+ "expected_type": expected_type.__name__,
+ "actual_type": type(value).__name__
+ }
+ )
+
+
+def validate_not_empty(value: Any, name: str):
+ if not value:
+ raise ValidationError(
+ f"{name} cannot be empty",
+ details={"parameter": name, "value": value}
+ )
diff --git a/src/conventionalrp/utils/logging_config.py b/src/conventionalrp/utils/logging_config.py
new file mode 100644
index 0000000..956d6d4
--- /dev/null
+++ b/src/conventionalrp/utils/logging_config.py
@@ -0,0 +1,77 @@
+import logging
+import sys
+from pathlib import Path
+from typing import Optional
+from datetime import datetime
+
+
+def setup_logging(
+ level: str = "INFO",
+ log_file: Optional[str] = None,
+ format_string: Optional[str] = None,
+ include_timestamp: bool = True,
+ include_module: bool = True
+) -> logging.Logger:
+ log_level = getattr(logging, level.upper(), logging.INFO)
+
+ if format_string is None:
+ parts = []
+ if include_timestamp:
+ parts.append("%(asctime)s")
+ parts.append("[%(levelname)s]")
+ if include_module:
+ parts.append("%(name)s")
+ parts.append("%(message)s")
+ format_string = " - ".join(parts)
+
+ logging.basicConfig(
+ level=log_level,
+ format=format_string,
+ datefmt="%Y-%m-%d %H:%M:%S",
+ handlers=[]
+ )
+
+ logger = logging.getLogger("conventionalrp")
+ logger.setLevel(log_level)
+
+ logger.handlers.clear()
+
+ console_handler = logging.StreamHandler(sys.stdout)
+ console_handler.setLevel(log_level)
+ console_formatter = logging.Formatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
+ console_handler.setFormatter(console_formatter)
+ logger.addHandler(console_handler)
+
+ if log_file:
+ log_path = Path(log_file)
+ log_path.parent.mkdir(parents=True, exist_ok=True)
+
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
+ file_handler.setLevel(log_level)
+ file_handler.setFormatter(console_formatter)
+ logger.addHandler(file_handler)
+
+ logger.info(f"Logging to file: {log_file}")
+
+ logger.info(f"Logging configured at {level} level")
+ return logger
+
+
+def get_logger(name: str) -> logging.Logger:
+ return logging.getLogger(f"conventionalrp.{name}")
+
+
+class LogContext:
+ def __init__(self, logger: logging.Logger, level: str):
+ self.logger = logger
+ self.level = getattr(logging, level.upper())
+ self.original_level = None
+
+ def __enter__(self):
+ self.original_level = self.logger.level
+ self.logger.setLevel(self.level)
+ return self.logger
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.logger.setLevel(self.original_level)
+ return False
diff --git a/src/conventionalrp/utils/text_processing.py b/src/conventionalrp/utils/text_processing.py
deleted file mode 100644
index e69de29..0000000
--- a/src/conventionalrp/utils/text_processing.py
+++ /dev/null
diff --git a/src/lib.rs b/src/lib.rs
index 8f19266..684001c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,19 +1,253 @@
use pyo3::prelude::*;
+use pyo3::types::{PyDict, PyList};
+use regex::Regex;
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
-#[pyfunction]
-fn sum_as_string(a: usize, b: usize) -> PyResult<String> {
- Ok((a + b).to_string())
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[pyclass]
+pub enum TokenType {
+ Metadata,
+ DiceRoll,
+ Dialogue,
+ Action,
+ Ooc,
+ System,
+ Text,
+ Unknown,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[pyclass]
+pub struct Token {
+ #[pyo3(get, set)]
+ pub token_type: String,
+ #[pyo3(get, set)]
+ pub content: String,
+ #[pyo3(get, set)]
+ pub metadata: HashMap<String, String>,
+}
+
+#[pymethods]
+impl Token {
+ #[new]
+ fn new(token_type: String, content: String) -> Self {
+ Token {
+ token_type,
+ content,
+ metadata: HashMap::new(),
+ }
+ }
+
+ fn add_metadata(&mut self, key: String, value: String) {
+ self.metadata.insert(key, value);
+ }
+
+ fn get_metadata(&self, key: &str) -> Option<String> {
+ self.metadata.get(key).cloned()
+ }
+ fn to_dict(&self, py: Python) -> PyResult<Py<PyAny>> {
+ let dict = PyDict::new(py);
+ dict.set_item("type", &self.token_type)?;
+ dict.set_item("content", &self.content)?;
+
+ let metadata_dict = PyDict::new(py);
+ for (k, v) in &self.metadata {
+ metadata_dict.set_item(k, v)?;
+ }
+ dict.set_item("metadata", metadata_dict)?;
+
+ Ok(dict.into())
+ }
+
+ fn __repr__(&self) -> String {
+ format!("Token(type={}, content={})", self.token_type, self.content)
+ }
+}
+
+#[pyclass]
+pub struct RegexRule {
+ pattern: Regex,
+ #[pyo3(get, set)]
+ pub rule_type: String,
+ #[pyo3(get, set)]
+ pub priority: i32,
+}
+
+#[pymethods]
+impl RegexRule {
+ #[new]
+ fn new(pattern: String, rule_type: String, priority: i32) -> PyResult<Self> {
+ let regex = Regex::new(&pattern)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(
+ format!("Invalid regex pattern: {}", e)
+ ))?;
+
+ Ok(RegexRule {
+ pattern: regex,
+ rule_type,
+ priority,
+ })
+ }
+
+ fn matches(&self, text: &str) -> bool {
+ self.pattern.is_match(text)
+ }
+
+
+ fn extract(&self, text: &str) -> Option<Vec<String>> {
+ self.pattern.captures(text).map(|caps| {
+ caps.iter()
+ .skip(1) // 跳过完整匹配
+ .filter_map(|m| m.map(|m| m.as_str().to_string()))
+ .collect()
+ })
+ }
+
+ fn find_all(&self, text: &str, py: Python) -> PyResult<Py<PyAny>> {
+ let matches: Vec<(usize, usize, String)> = self.pattern
+ .find_iter(text)
+ .map(|m| (m.start(), m.end(), m.as_str().to_string()))
+ .collect();
+
+ let list = PyList::empty(py);
+ for (start, end, matched) in matches {
+ let dict = PyDict::new(py);
+ dict.set_item("start", start)?;
+ dict.set_item("end", end)?;
+ dict.set_item("text", matched)?;
+ list.append(dict)?;
+ }
+
+ Ok(list.into())
+ }
+}
+
+#[pyclass]
+pub struct TextParser {
+ rules: Vec<(Regex, String, i32)>, // (pattern, type, priority)
+}
+
+#[pymethods]
+impl TextParser {
+ #[new]
+ fn new() -> Self {
+ TextParser { rules: Vec::new() }
+ }
+
+ fn add_rule(&mut self, pattern: String, rule_type: String, priority: i32) -> PyResult<()> {
+ let regex = Regex::new(&pattern)
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(
+ format!("Invalid regex: {}", e)
+ ))?;
+
+ self.rules.push((regex, rule_type, priority));
+ self.rules.sort_by(|a, b| b.2.cmp(&a.2));
+
+ Ok(())
+ }
+
+ fn parse_line(&self, text: &str) -> Vec<(String, String, usize, usize)> {
+ let mut results = Vec::new();
+ let mut processed_ranges: Vec<(usize, usize)> = Vec::new();
+ for (pattern, rule_type, _priority) in &self.rules {
+ for mat in pattern.find_iter(text) {
+ let start = mat.start();
+ let end = mat.end();
+
+ let overlaps = processed_ranges.iter().any(|(s, e)| {
+ (start >= *s && start < *e) || (end > *s && end <= *e) || (start <= *s && end >= *e)
+ });
+
+ if !overlaps {
+ results.push((
+ rule_type.clone(),
+ mat.as_str().to_string(),
+ start,
+ end,
+ ));
+ processed_ranges.push((start, end));
+ }
+ }
+ }
+
+ results.sort_by_key(|r| r.2);
+ results
+ }
+ fn parse_lines(&self, lines: Vec<String>, py: Python) -> PyResult<Py<PyAny>> {
+ let list = PyList::empty(py);
+
+ for line in lines {
+ let parsed = self.parse_line(&line);
+ let line_result = PyList::empty(py);
+
+ for (rule_type, content, start, end) in parsed {
+ let dict = PyDict::new(py);
+ dict.set_item("type", rule_type)?;
+ dict.set_item("content", content)?;
+ dict.set_item("start", start)?;
+ dict.set_item("end", end)?;
+ line_result.append(dict)?;
+ }
+
+ list.append(line_result)?;
+ }
+
+ Ok(list.into())
+ }
+
+ fn clear_rules(&mut self) {
+ self.rules.clear();
+ }
+
+ fn rule_count(&self) -> usize {
+ self.rules.len()
+ }
}
#[pyclass]
-pub struct Base {}
+pub struct FastMatcher {
+ patterns: Vec<String>,
+}
+
+#[pymethods]
+impl FastMatcher {
+ #[new]
+ fn new(patterns: Vec<String>) -> Self {
+ FastMatcher { patterns }
+ }
+
+
+ fn contains_any(&self, text: &str) -> bool {
+ self.patterns.iter().any(|p| text.contains(p))
+ }
+
+
+ fn find_matches(&self, text: &str) -> Vec<String> {
+ self.patterns
+ .iter()
+ .filter(|p| text.contains(*p))
+ .cloned()
+ .collect()
+ }
+
+ fn count_matches(&self, text: &str, py: Python) -> PyResult<Py<PyAny>> {
+ let dict = PyDict::new(py);
+ for pattern in &self.patterns {
+ let count = text.matches(pattern.as_str()).count();
+ dict.set_item(pattern, count)?;
+ }
+ Ok(dict.into())
+ }
+}
-/// A Python module implemented in Rust. The name of this function must match
-/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
-/// import the module.
#[pymodule]
fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
- m.add_function(wrap_pyfunction!(sum_as_string, m)?)?;
- m.add_class::<Base>()?;
+ // 添加类
+ m.add_class::<Token>()?;
+ m.add_class::<RegexRule>()?;
+ m.add_class::<TextParser>()?;
+ m.add_class::<FastMatcher>()?;
+
Ok(())
} \ No newline at end of file
diff --git a/tests/README.md b/tests/README.md
deleted file mode 100644
index 97a8028..0000000
--- a/tests/README.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# ConventionalRP 测试套件
-
-本目录包含 ConventionalRP SDK 的所有单元测试。
-
-## 测试文件
-
-- `test_parser.py` - Parser 解析器测试
-- `test_processor.py` - Processor 处理器测试
-- `test_rule_extractor.py` - RuleExtractor 规则提取器测试
-- `test_renderers.py` - 渲染器测试(HTML/JSON/Markdown)
-- `test_pyo3.py` - PyO3 Rust 扩展测试
-
-## 运行测试
-
-### 运行所有测试
-
-```bash
-python tests/run_tests.py
-```
-
-### 运行单个测试文件
-
-```bash
-python -m unittest tests/test_parser.py
-python -m unittest tests/test_processor.py
-```
-
-### 运行特定测试类
-
-```bash
-python -m unittest tests.test_parser.TestParser
-```
-
-### 运行特定测试方法
-
-```bash
-python -m unittest tests.test_parser.TestParser.test_load_rules_success
-```
-
-## 测试覆盖率
-
-要查看测试覆盖率,请安装 `coverage` 并运行:
-
-```bash
-pip install coverage
-coverage run -m unittest discover -s tests -p "test_*.py"
-coverage report
-coverage html # 生成 HTML 报告
-```
-
-## 测试数据
-
-测试使用临时文件来模拟规则文件和日志文件,测试完成后会自动清理。
-
-## 添加新测试
-
-创建新的测试文件时,请遵循以下约定:
-
-1. 文件名以 `test_` 开头
-2. 测试类继承自 `unittest.TestCase`
-3. 测试方法以 `test_` 开头
-4. 使用 `setUp()` 和 `tearDown()` 方法管理测试状态
-5. 添加清晰的文档字符串说明测试目的
diff --git a/tests/run_tests.py b/tests/run_tests.py
index 4cfc2d4..b776062 100644
--- a/tests/run_tests.py
+++ b/tests/run_tests.py
@@ -1,34 +1,22 @@
-#!/usr/bin/env python3
-"""
-测试套件运行器
-运行所有单元测试
-"""
-
import sys
import unittest
from pathlib import Path
-# 添加 src 目录到路径
src_path = Path(__file__).parent.parent / "src"
sys.path.insert(0, str(src_path))
def run_all_tests():
- """运行所有测试"""
- # 创建测试加载器
loader = unittest.TestLoader()
- # 从当前目录加载所有测试
suite = loader.discover(
start_dir=Path(__file__).parent,
pattern='test_*.py'
)
- # 运行测试
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
- # 返回结果
return 0 if result.wasSuccessful() else 1
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 595d0b4..1d41a1e 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1,8 +1,3 @@
-#!/usr/bin/env python3
-"""
-Parser 模块单元测试
-"""
-
import unittest
import tempfile
from pathlib import Path
@@ -10,13 +5,9 @@ from conventionalrp.core.parser import Parser
class TestParser(unittest.TestCase):
- """Parser 类的单元测试"""
-
def setUp(self):
- """设置测试环境"""
self.parser = Parser()
- # 创建临时规则文件
self.temp_rules = tempfile.NamedTemporaryFile(
mode='w',
suffix='.json5',
@@ -56,7 +47,6 @@ class TestParser(unittest.TestCase):
}''')
self.temp_rules.close()
- # 创建临时日志文件
self.temp_log = tempfile.NamedTemporaryFile(
mode='w',
suffix='.txt',
@@ -70,30 +60,25 @@ class TestParser(unittest.TestCase):
self.temp_log.close()
def tearDown(self):
- """清理测试环境"""
Path(self.temp_rules.name).unlink(missing_ok=True)
Path(self.temp_log.name).unlink(missing_ok=True)
def test_load_rules_success(self):
- """测试成功加载规则文件"""
self.parser.load_rules(self.temp_rules.name)
self.assertIn("metadata", self.parser.rules)
self.assertIn("content", self.parser.rules)
def test_load_rules_file_not_found(self):
- """测试加载不存在的规则文件"""
with self.assertRaises(FileNotFoundError):
self.parser.load_rules("nonexistent_file.json5")
def test_parse_log_success(self):
- """测试成功解析日志"""
self.parser.load_rules(self.temp_rules.name)
result = self.parser.parse_log(self.temp_log.name)
self.assertIsInstance(result, list)
self.assertGreater(len(result), 0)
- # 检查第一条记录
first_entry = result[0]
self.assertIn("timestamp", first_entry)
self.assertIn("speaker", first_entry)
@@ -101,13 +86,11 @@ class TestParser(unittest.TestCase):
self.assertEqual(first_entry["speaker"], "艾莉娅")
def test_parse_log_file_not_found(self):
- """测试解析不存在的日志文件"""
self.parser.load_rules(self.temp_rules.name)
with self.assertRaises(FileNotFoundError):
self.parser.parse_log("nonexistent_log.txt")
def test_match_metadata(self):
- """测试元数据匹配"""
self.parser.load_rules(self.temp_rules.name)
line = "[2025-10-24 14:30:01] <艾莉娅> 测试内容"
result = self.parser._match_metadata(line)
@@ -118,7 +101,6 @@ class TestParser(unittest.TestCase):
self.assertEqual(result["speaker"], "艾莉娅")
def test_parse_line_content_dialogue(self):
- """测试解析对话内容"""
self.parser.load_rules(self.temp_rules.name)
line = "「这是一段对话」"
result = self.parser._parse_line_content(line)
@@ -128,12 +110,10 @@ class TestParser(unittest.TestCase):
self.assertEqual(result[0]["type"], "dialogue")
def test_parse_line_content_dice_roll(self):
- """测试解析骰子投掷"""
self.parser.load_rules(self.temp_rules.name)
line = "检定结果: [d20 = 18]"
result = self.parser._parse_line_content(line)
- # 应该包含文本和骰子投掷
dice_tokens = [t for t in result if t["type"] == "dice_roll"]
self.assertGreater(len(dice_tokens), 0)
diff --git a/tests/test_processor.py b/tests/test_processor.py
index c08fc52..adaf2db 100644
--- a/tests/test_processor.py
+++ b/tests/test_processor.py
@@ -1,17 +1,9 @@
-#!/usr/bin/env python3
-"""
-Processor 模块单元测试
-"""
-
import unittest
from conventionalrp.core.processor import Processor
class TestProcessor(unittest.TestCase):
- """Processor 类的单元测试"""
-
def setUp(self):
- """设置测试环境"""
self.processor = Processor()
self.sample_tokens = [
{
@@ -33,30 +25,26 @@ class TestProcessor(unittest.TestCase):
]
def test_init_without_rules(self):
- """测试无规则初始化"""
processor = Processor()
self.assertEqual(processor.rules, {})
def test_init_with_rules(self):
- """测试带规则初始化"""
rules = {"test_rule": "value"}
processor = Processor(rules)
self.assertEqual(processor.rules, rules)
def test_process_tokens(self):
- """测试处理 token 列表"""
result = self.processor.process_tokens(self.sample_tokens)
self.assertIsInstance(result, list)
self.assertEqual(len(result), len(self.sample_tokens))
- # 检查处理标记
+ # Check processing marks
for token in result:
if "timestamp" in token:
self.assertTrue(token.get("processed"))
def test_apply_rules(self):
- """测试应用规则到单个 token"""
token = self.sample_tokens[0]
result = self.processor.apply_rules(token)
@@ -65,7 +53,6 @@ class TestProcessor(unittest.TestCase):
self.assertTrue(result.get("processed"))
def test_generate_json_output(self):
- """测试生成 JSON 输出"""
output = self.processor.generate_json_output(self.sample_tokens)
self.assertIsInstance(output, str)
@@ -73,7 +60,6 @@ class TestProcessor(unittest.TestCase):
self.assertIn("speaker", output)
def test_generate_html_output(self):
- """测试生成 HTML 输出"""
output = self.processor.generate_html_output(self.sample_tokens)
self.assertIsInstance(output, str)
@@ -81,29 +67,25 @@ class TestProcessor(unittest.TestCase):
self.assertIn("</html>", output)
def test_generate_markdown_output(self):
- """测试生成 Markdown 输出"""
output = self.processor.generate_markdown_output(self.sample_tokens)
self.assertIsInstance(output, str)
self.assertIn("-", output)
def test_generate_output_json(self):
- """测试生成输出 - JSON 格式"""
output = self.processor.generate_output(self.sample_tokens, "json")
self.assertIsInstance(output, str)
def test_generate_output_html(self):
- """测试生成输出 - HTML 格式"""
+ output = self.processor.generate_output(self.sample_tokens, "html")
output = self.processor.generate_output(self.sample_tokens, "html")
self.assertIsInstance(output, str)
def test_generate_output_markdown(self):
- """测试生成输出 - Markdown 格式"""
output = self.processor.generate_output(self.sample_tokens, "markdown")
self.assertIsInstance(output, str)
def test_generate_output_unsupported_format(self):
- """测试生成输出 - 不支持的格式"""
with self.assertRaises(ValueError) as context:
self.processor.generate_output(self.sample_tokens, "pdf")
diff --git a/tests/test_pyo3.py b/tests/test_pyo3.py
deleted file mode 100644
index 9668519..0000000
--- a/tests/test_pyo3.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from conventionalrp._core import sum_as_string
-
-if __name__ == "__main__":
- print(sum_as_string(1, 2))
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 13e4540..8095550 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -1,8 +1,3 @@
-#!/usr/bin/env python3
-"""
-Renderers 模块单元测试
-"""
-
import unittest
import json
from conventionalrp.renderers.html_renderer import HTMLRenderer
@@ -11,10 +6,7 @@ from conventionalrp.renderers.markdown_renderer import MarkdownRenderer
class TestRenderers(unittest.TestCase):
- """测试所有渲染器"""
-
def setUp(self):
- """设置测试数据"""
self.sample_data = [
{
"type": "metadata",
@@ -40,7 +32,6 @@ class TestRenderers(unittest.TestCase):
}
def test_html_renderer_basic(self):
- """测试 HTML 渲染器基本功能"""
renderer = HTMLRenderer()
output = renderer.render(self.sample_data)
@@ -50,51 +41,45 @@ class TestRenderers(unittest.TestCase):
self.assertIn("<title>", output)
def test_html_renderer_set_style(self):
- """测试 HTML 渲染器设置样式"""
renderer = HTMLRenderer()
renderer.set_style("custom_style")
- # 当前实现为占位符,仅测试不抛出异常
+ # now style is set, just ensure no exceptions
self.assertIsNotNone(renderer)
def test_json_renderer_basic(self):
- """测试 JSON 渲染器基本功能"""
renderer = JSONRenderer()
output = renderer.render(self.sample_data)
self.assertIsInstance(output, str)
- # 验证输出是有效的 JSON
+ # Output should be valid JSON
parsed = json.loads(output)
self.assertIsInstance(parsed, list)
self.assertEqual(len(parsed), len(self.sample_data))
def test_json_renderer_unicode(self):
- """测试 JSON 渲染器处理 Unicode"""
renderer = JSONRenderer()
output = renderer.render(self.sample_data)
- # 应该保留中文字符
+ # should preserve Chinese characters
self.assertIn("艾莉娅", output)
self.assertIn("测试", output)
def test_markdown_renderer_basic(self):
- """测试 Markdown 渲染器基本功能"""
renderer = MarkdownRenderer()
output = renderer.render(self.dict_data)
self.assertIsInstance(output, str)
- self.assertIn("##", output) # 应该有标题标记
- self.assertIn("测试标题", output)
+ self.assertIn("##", output)
+ self.assertIn("test content", output)
def test_markdown_renderer_set_style(self):
- """测试 Markdown 渲染器设置样式"""
renderer = MarkdownRenderer()
style = {"heading_level": 2}
renderer.set_style(style)
self.assertEqual(renderer.style, style)
def test_all_renderers_empty_data(self):
- """测试所有渲染器处理空数据"""
empty_data = []
html_renderer = HTMLRenderer()
diff --git a/tests/test_rule_extractor.py b/tests/test_rule_extractor.py
index 6c4d585..2c2815c 100644
--- a/tests/test_rule_extractor.py
+++ b/tests/test_rule_extractor.py
@@ -1,8 +1,3 @@
-#!/usr/bin/env python3
-"""
-RuleExtractor 模块单元测试
-"""
-
import unittest
import tempfile
import json5
@@ -11,11 +6,7 @@ from conventionalrp.extractors.rule_extractor import RuleExtractor
class TestRuleExtractor(unittest.TestCase):
- """RuleExtractor 类的单元测试"""
-
def setUp(self):
- """设置测试环境"""
- # 创建临时规则文件
self.temp_rules = tempfile.NamedTemporaryFile(
mode='w',
suffix='.json5',
@@ -30,23 +21,19 @@ class TestRuleExtractor(unittest.TestCase):
self.temp_rules.close()
def tearDown(self):
- """清理测试环境"""
Path(self.temp_rules.name).unlink(missing_ok=True)
def test_init_without_file(self):
- """测试不带配置文件的初始化"""
extractor = RuleExtractor()
self.assertEqual(extractor.rules, {})
self.assertIsNone(extractor.config_file)
def test_init_with_file(self):
- """测试带配置文件的初始化"""
extractor = RuleExtractor(self.temp_rules.name)
self.assertIsNotNone(extractor.rules)
self.assertIn("test_rule", extractor.rules)
def test_load_rules_from_file_success(self):
- """测试成功加载规则文件"""
extractor = RuleExtractor()
rules = extractor.load_rules_from_file(self.temp_rules.name)
@@ -55,13 +42,11 @@ class TestRuleExtractor(unittest.TestCase):
self.assertEqual(rules["test_rule"], "test_value")
def test_load_rules_from_file_not_found(self):
- """测试加载不存在的文件"""
extractor = RuleExtractor()
with self.assertRaises(FileNotFoundError):
extractor.load_rules_from_file("nonexistent.json5")
def test_load_rules_empty_file(self):
- """测试加载空文件"""
empty_file = tempfile.NamedTemporaryFile(
mode='w',
suffix='.json5',
@@ -79,7 +64,6 @@ class TestRuleExtractor(unittest.TestCase):
Path(empty_file.name).unlink(missing_ok=True)
def test_load_rules_method(self):
- """测试 load_rules 方法"""
extractor = RuleExtractor()
rules = extractor.load_rules(self.temp_rules.name)
@@ -87,7 +71,6 @@ class TestRuleExtractor(unittest.TestCase):
self.assertEqual(extractor.rules, rules)
def test_extract_method(self):
- """测试 extract 方法"""
extractor = RuleExtractor(self.temp_rules.name)
extracted = extractor.extract()
diff --git a/tests/test_rust_core.py b/tests/test_rust_core.py
new file mode 100644
index 0000000..1848e68
--- /dev/null
+++ b/tests/test_rust_core.py
@@ -0,0 +1,99 @@
+import sys
+from pathlib import Path
+
+project_root = Path(__file__).parent.parent
+sys.path.insert(0, str(project_root / "src"))
+
+from conventionalrp import _core
+
+def test_token():
+ token = _core.Token("dialogue", "「你好世界」")
+ token.add_metadata("speaker", "艾莉娅")
+ token.add_metadata("timestamp", "2025-10-24 14:30:01")
+
+ print(f"Token: {token}")
+ print(f"Type: {token.token_type}")
+ print(f"Content: {token.content}")
+ print(f"Speaker: {token.get_metadata('speaker')}")
+ print(f"Dict: {token.to_dict()}")
+
+
+def test_regex_rule():
+ rule = _core.RegexRule(r"\[d(\d+)\s*=\s*(\d+)\]", "dice_roll", 90)
+
+ text = "检定结果: [d20 = 18]"
+ print(f"Text: {text}")
+ print(f"Matches: {rule.matches(text)}")
+ print(f"Extracted: {rule.extract(text)}")
+ print(f"Find all: {rule.find_all(text)}")
+
+
+def test_text_parser():
+ parser = _core.TextParser()
+
+ parser.add_rule(r"\[d(\d+)\s*=\s*(\d+)\]", "dice_roll", 90)
+ parser.add_rule(r"「(.+?)」", "dialogue", 80)
+ parser.add_rule(r"\*\*(.+?)\*\*", "action", 70)
+
+ print(f"规则数量: {parser.rule_count()}")
+
+ line = "艾莉娅说「我要投掷」然后 **投掷骰子** 结果是 [d20 = 18]"
+ print(f"\n解析文本: {line}")
+ result = parser.parse_line(line)
+ print(f"解析结果: {result}")
+
+ lines = [
+ "「你好」",
+ "**挥剑** [d20 = 15]",
+ "普通文本"
+ ]
+ results = parser.parse_lines(lines)
+ for i, line_result in enumerate(results):
+ print(f" 行 {i+1}: {line_result}")
+
+
+def test_fast_matcher():
+ patterns = ["骰子", "投掷", "检定"]
+ matcher = _core.FastMatcher(patterns)
+
+ text = "艾莉娅进行投掷骰子检定"
+ print(f"Text: {text}")
+ print(f"Contains any: {matcher.contains_any(text)}")
+ print(f"Find matches: {matcher.find_matches(text)}")
+ print(f"Count matches: {matcher.count_matches(text)}")
+
+
+def benchmark_parser():
+ import time
+
+ parser = _core.TextParser()
+ parser.add_rule(r"\[d(\d+)\s*=\s*(\d+)\]", "dice_roll", 90)
+ parser.add_rule(r"「(.+?)」", "dialogue", 80)
+ parser.add_rule(r"\*\*(.+?)\*\*", "action", 70)
+
+ test_lines = [
+ "「你好」**挥剑** [d20 = 15]" for _ in range(1000)
+ ]
+
+ start = time.time()
+ results = parser.parse_lines(test_lines)
+ elapsed = time.time() - start
+
+ print(f"Process {len(test_lines)} lines")
+ print(f"time: {elapsed:.4f} s")
+ print(f"speed: {len(test_lines)/elapsed:.0f} lines/s")
+
+
+if __name__ == "__main__":
+ try:
+ test_token()
+ test_regex_rule()
+ test_text_parser()
+ test_fast_matcher()
+ benchmark_parser()
+
+ except Exception as e:
+ print(f"\nTest failed: {e}")
+ import traceback
+ traceback.print_exc()
+ sys.exit(1)
diff --git a/uv.lock b/uv.lock
deleted file mode 100644
index e38f683..0000000
--- a/uv.lock
+++ /dev/null
@@ -1,1027 +0,0 @@
-version = 1
-revision = 1
-requires-python = ">=3.9"
-resolution-markers = [
- "python_full_version >= '3.10'",
- "python_full_version < '3.10'",
-]
-
-[[package]]
-name = "alabaster"
-version = "0.7.16"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511 },
-]
-
-[[package]]
-name = "alabaster"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 },
-]
-
-[[package]]
-name = "anyio"
-version = "4.8.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "idna" },
- { name = "sniffio" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
-]
-
-[[package]]
-name = "babel"
-version = "2.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 },
-]
-
-[[package]]
-name = "beautifulsoup4"
-version = "4.13.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "soupsieve" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 },
-]
-
-[[package]]
-name = "certifi"
-version = "2025.1.31"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
-]
-
-[[package]]
-name = "charset-normalizer"
-version = "3.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 },
- { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 },
- { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 },
- { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 },
- { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 },
- { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 },
- { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 },
- { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 },
- { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 },
- { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 },
- { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 },
- { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 },
- { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 },
- { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 },
- { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 },
- { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 },
- { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 },
- { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 },
- { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 },
- { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 },
- { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 },
- { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 },
- { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 },
- { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 },
- { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 },
- { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 },
- { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
- { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
- { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
- { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
- { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
- { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
- { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
- { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
- { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
- { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
- { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
- { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
- { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
- { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
- { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
- { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
- { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
- { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
- { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
- { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
- { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
- { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
- { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
- { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
- { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
- { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
- { url = "https://files.pythonhosted.org/packages/7f/c0/b913f8f02836ed9ab32ea643c6fe4d3325c3d8627cf6e78098671cafff86/charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", size = 197867 },
- { url = "https://files.pythonhosted.org/packages/0f/6c/2bee440303d705b6fb1e2ec789543edec83d32d258299b16eed28aad48e0/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", size = 141385 },
- { url = "https://files.pythonhosted.org/packages/3d/04/cb42585f07f6f9fd3219ffb6f37d5a39b4fd2db2355b23683060029c35f7/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", size = 151367 },
- { url = "https://files.pythonhosted.org/packages/54/54/2412a5b093acb17f0222de007cc129ec0e0df198b5ad2ce5699355269dfe/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", size = 143928 },
- { url = "https://files.pythonhosted.org/packages/5a/6d/e2773862b043dcf8a221342954f375392bb2ce6487bcd9f2c1b34e1d6781/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", size = 146203 },
- { url = "https://files.pythonhosted.org/packages/b9/f8/ca440ef60d8f8916022859885f231abb07ada3c347c03d63f283bec32ef5/charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", size = 148082 },
- { url = "https://files.pythonhosted.org/packages/04/d2/42fd330901aaa4b805a1097856c2edf5095e260a597f65def493f4b8c833/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", size = 142053 },
- { url = "https://files.pythonhosted.org/packages/9e/af/3a97a4fa3c53586f1910dadfc916e9c4f35eeada36de4108f5096cb7215f/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", size = 150625 },
- { url = "https://files.pythonhosted.org/packages/26/ae/23d6041322a3556e4da139663d02fb1b3c59a23ab2e2b56432bd2ad63ded/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", size = 153549 },
- { url = "https://files.pythonhosted.org/packages/94/22/b8f2081c6a77cb20d97e57e0b385b481887aa08019d2459dc2858ed64871/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", size = 150945 },
- { url = "https://files.pythonhosted.org/packages/c7/0b/c5ec5092747f801b8b093cdf5610e732b809d6cb11f4c51e35fc28d1d389/charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", size = 146595 },
- { url = "https://files.pythonhosted.org/packages/0c/5a/0b59704c38470df6768aa154cc87b1ac7c9bb687990a1559dc8765e8627e/charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", size = 95453 },
- { url = "https://files.pythonhosted.org/packages/85/2d/a9790237cb4d01a6d57afadc8573c8b73c609ade20b80f4cda30802009ee/charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", size = 102811 },
- { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
-]
-
-[[package]]
-name = "click"
-version = "8.1.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
-]
-
-[[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 = "conventionalrp"
-source = { editable = "." }
-
-[package.dev-dependencies]
-dev = [
- { name = "docutils" },
- { name = "furo" },
- { name = "json5" },
- { name = "myst-parser", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "ruff" },
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "sphinx-autobuild" },
- { name = "sphinx-intl" },
- { name = "sphinxcontrib-httpdomain" },
- { name = "tomli" },
-]
-
-[package.metadata]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "docutils", specifier = ">=0.21.2" },
- { name = "furo", specifier = ">=2024.8.6" },
- { name = "json5", specifier = ">=0.10.0" },
- { name = "myst-parser", specifier = ">=3.0.1" },
- { name = "ruff", specifier = ">=0.9.6" },
- { name = "sphinx", specifier = ">=7.4.7" },
- { name = "sphinx-autobuild", specifier = ">=2024.10.3" },
- { name = "sphinx-intl", specifier = ">=2.3.1" },
- { name = "sphinxcontrib-httpdomain", specifier = ">=1.8.1" },
- { name = "tomli", specifier = ">=2.2.1" },
-]
-
-[[package]]
-name = "docutils"
-version = "0.21.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 },
-]
-
-[[package]]
-name = "exceptiongroup"
-version = "1.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 },
-]
-
-[[package]]
-name = "furo"
-version = "2024.8.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "beautifulsoup4" },
- { name = "pygments" },
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "sphinx-basic-ng" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 },
-]
-
-[[package]]
-name = "h11"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
-]
-
-[[package]]
-name = "idna"
-version = "3.10"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
-]
-
-[[package]]
-name = "imagesize"
-version = "1.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 },
-]
-
-[[package]]
-name = "importlib-metadata"
-version = "8.6.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "zipp", marker = "python_full_version < '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 },
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
-]
-
-[[package]]
-name = "json5"
-version = "0.10.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/85/3d/bbe62f3d0c05a689c711cff57b2e3ac3d3e526380adb7c781989f075115c/json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559", size = 48202 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/42/797895b952b682c3dafe23b1834507ee7f02f4d6299b65aaa61425763278/json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa", size = 34049 },
-]
-
-[[package]]
-name = "markdown-it-py"
-version = "3.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
-]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 },
- { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 },
- { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 },
- { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 },
- { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 },
- { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 },
- { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 },
- { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 },
- { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 },
- { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 },
- { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 },
- { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 },
- { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 },
- { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 },
- { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 },
- { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 },
- { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 },
- { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 },
- { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 },
- { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 },
- { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
- { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
- { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
- { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
- { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
- { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
- { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
- { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
- { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
- { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
- { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
- { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
- { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
- { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
- { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
- { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
- { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
- { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
- { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
- { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
- { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
- { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
- { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
- { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
- { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
- { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
- { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
- { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
- { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
- { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
- { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 },
- { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 },
- { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 },
- { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 },
- { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 },
- { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 },
- { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 },
- { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 },
- { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 },
- { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 },
-]
-
-[[package]]
-name = "mdit-py-plugins"
-version = "0.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown-it-py" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316 },
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
-]
-
-[[package]]
-name = "myst-parser"
-version = "3.0.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-dependencies = [
- { name = "docutils", marker = "python_full_version < '3.10'" },
- { name = "jinja2", marker = "python_full_version < '3.10'" },
- { name = "markdown-it-py", marker = "python_full_version < '3.10'" },
- { name = "mdit-py-plugins", marker = "python_full_version < '3.10'" },
- { name = "pyyaml", marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163 },
-]
-
-[[package]]
-name = "myst-parser"
-version = "4.0.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-dependencies = [
- { name = "docutils", marker = "python_full_version >= '3.10'" },
- { name = "jinja2", marker = "python_full_version >= '3.10'" },
- { name = "markdown-it-py", marker = "python_full_version >= '3.10'" },
- { name = "mdit-py-plugins", marker = "python_full_version >= '3.10'" },
- { name = "pyyaml", marker = "python_full_version >= '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579 },
-]
-
-[[package]]
-name = "packaging"
-version = "24.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
-]
-
-[[package]]
-name = "pyyaml"
-version = "6.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 },
- { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 },
- { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 },
- { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 },
- { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 },
- { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 },
- { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 },
- { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 },
- { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 },
- { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 },
- { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 },
- { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 },
- { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 },
- { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 },
- { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 },
- { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 },
- { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 },
- { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 },
- { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
- { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
- { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
- { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
- { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
- { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
- { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
- { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
- { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
- { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
- { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
- { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
- { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
- { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
- { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
- { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
- { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
- { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
- { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 },
- { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 },
- { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 },
- { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 },
- { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 },
- { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 },
- { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 },
- { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 },
- { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 },
-]
-
-[[package]]
-name = "requests"
-version = "2.32.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
-]
-
-[[package]]
-name = "ruff"
-version = "0.9.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
- { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
- { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
- { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
- { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
- { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
- { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
- { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
- { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
- { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
- { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
- { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
- { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
- { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
- { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
- { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
- { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
-]
-
-[[package]]
-name = "setuptools"
-version = "75.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 },
-]
-
-[[package]]
-name = "six"
-version = "1.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
-]
-
-[[package]]
-name = "sniffio"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
-]
-
-[[package]]
-name = "snowballstemmer"
-version = "2.2.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 },
-]
-
-[[package]]
-name = "soupsieve"
-version = "2.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 },
-]
-
-[[package]]
-name = "sphinx"
-version = "7.4.7"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.10'",
-]
-dependencies = [
- { name = "alabaster", version = "0.7.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "babel", marker = "python_full_version < '3.10'" },
- { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
- { name = "docutils", marker = "python_full_version < '3.10'" },
- { name = "imagesize", marker = "python_full_version < '3.10'" },
- { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
- { name = "jinja2", marker = "python_full_version < '3.10'" },
- { name = "packaging", marker = "python_full_version < '3.10'" },
- { name = "pygments", marker = "python_full_version < '3.10'" },
- { name = "requests", marker = "python_full_version < '3.10'" },
- { name = "snowballstemmer", marker = "python_full_version < '3.10'" },
- { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.10'" },
- { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.10'" },
- { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.10'" },
- { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.10'" },
- { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.10'" },
- { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.10'" },
- { name = "tomli", marker = "python_full_version < '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624 },
-]
-
-[[package]]
-name = "sphinx"
-version = "8.1.3"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.10'",
-]
-dependencies = [
- { name = "alabaster", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "babel", marker = "python_full_version >= '3.10'" },
- { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
- { name = "docutils", marker = "python_full_version >= '3.10'" },
- { name = "imagesize", marker = "python_full_version >= '3.10'" },
- { name = "jinja2", marker = "python_full_version >= '3.10'" },
- { name = "packaging", marker = "python_full_version >= '3.10'" },
- { name = "pygments", marker = "python_full_version >= '3.10'" },
- { name = "requests", marker = "python_full_version >= '3.10'" },
- { name = "snowballstemmer", marker = "python_full_version >= '3.10'" },
- { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.10'" },
- { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.10'" },
- { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.10'" },
- { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.10'" },
- { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.10'" },
- { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.10'" },
- { name = "tomli", marker = "python_full_version == '3.10.*'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 },
-]
-
-[[package]]
-name = "sphinx-autobuild"
-version = "2024.10.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama" },
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
- { name = "starlette" },
- { name = "uvicorn" },
- { name = "watchfiles" },
- { name = "websockets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/c0/eba125db38c84d3c74717008fd3cb5000b68cd7e2cbafd1349c6a38c3d3b/sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa", size = 11908 },
-]
-
-[[package]]
-name = "sphinx-basic-ng"
-version = "1.0.0b2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496 },
-]
-
-[[package]]
-name = "sphinx-intl"
-version = "2.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "babel" },
- { name = "click" },
- { name = "setuptools" },
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a5/01/4f64b7d28e3e0cbf25a70a79f26c4683c987ca7c6c265492115cfb302ca4/sphinx_intl-2.3.1.tar.gz", hash = "sha256:e2d04b907f57a029d46b4da344fd791e660d935eab6ced8a274fc65446389af1", size = 28006 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/5d/6a9d1460ef6e079d00350eb6203efd6a6a899692e927b9b18d652490b5e5/sphinx_intl-2.3.1-py3-none-any.whl", hash = "sha256:e3f4be80743e0e4c778fbc07e72b062c13c88c50645a69b62557a3f57a24b7fc", size = 13801 },
-]
-
-[[package]]
-name = "sphinxcontrib-applehelp"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 },
-]
-
-[[package]]
-name = "sphinxcontrib-devhelp"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 },
-]
-
-[[package]]
-name = "sphinxcontrib-htmlhelp"
-version = "2.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 },
-]
-
-[[package]]
-name = "sphinxcontrib-httpdomain"
-version = "1.8.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six" },
- { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/be/ef/82d3cfafb7febce4f7df8dcf3cde9d072350b41066e05a4f559b4e9105d0/sphinxcontrib-httpdomain-1.8.1.tar.gz", hash = "sha256:6c2dfe6ca282d75f66df333869bb0ce7331c01b475db6809ff9d107b7cdfe04b", size = 19266 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/02/49/aad47b8cf27a0d7703f1311aad8c368bb22866ddee1a2d2cd3f69bc45e0c/sphinxcontrib_httpdomain-1.8.1-py2.py3-none-any.whl", hash = "sha256:21eefe1270e4d9de8d717cc89ee92cc4871b8736774393bafc5e38a6bb77b1d5", size = 25513 },
-]
-
-[[package]]
-name = "sphinxcontrib-jsmath"
-version = "1.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 },
-]
-
-[[package]]
-name = "sphinxcontrib-qthelp"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 },
-]
-
-[[package]]
-name = "sphinxcontrib-serializinghtml"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 },
-]
-
-[[package]]
-name = "starlette"
-version = "0.45.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "typing-extensions", marker = "python_full_version < '3.10'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
-]
-
-[[package]]
-name = "tomli"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 },
- { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 },
- { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 },
- { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 },
- { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 },
- { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 },
- { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 },
- { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 },
- { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 },
- { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 },
- { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 },
- { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 },
- { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 },
- { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 },
- { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 },
- { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 },
- { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 },
- { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 },
- { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 },
- { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 },
- { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 },
- { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 },
- { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 },
- { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 },
- { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 },
- { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 },
- { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 },
- { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 },
- { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 },
- { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 },
- { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.12.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
-]
-
-[[package]]
-name = "urllib3"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
-]
-
-[[package]]
-name = "uvicorn"
-version = "0.34.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "h11" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
-]
-
-[[package]]
-name = "watchfiles"
-version = "1.0.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/14/02/22fcaed0396730b0d362bc8d1ffb3be2658fd473eecbb2ba84243e157f11/watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08", size = 395212 },
- { url = "https://files.pythonhosted.org/packages/e9/3d/ec5a2369a46edf3ebe092c39d9ae48e8cb6dacbde51c4b4f98936c524269/watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1", size = 384815 },
- { url = "https://files.pythonhosted.org/packages/df/b4/898991cececbe171e67142c31905510203649569d9817848f47c4177ee42/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a", size = 450680 },
- { url = "https://files.pythonhosted.org/packages/58/f7/d4aa3000e812cfb5e5c2c6c0a3ec9d0a46a42489a8727edd160631c4e210/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1", size = 455923 },
- { url = "https://files.pythonhosted.org/packages/dd/95/7e2e4c6aba1b02fb5c76d2f6a450b85215921ec5f8f7ad5efd075369563f/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3", size = 482339 },
- { url = "https://files.pythonhosted.org/packages/bb/67/4265b0fabcc2ef2c9e3e8802ba7908cf718a357ebfb49c72e53787156a48/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2", size = 519908 },
- { url = "https://files.pythonhosted.org/packages/0d/96/b57802d5f8164bdf070befb4fd3dec4edba5a364ec0670965a97eb8098ce/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2", size = 501410 },
- { url = "https://files.pythonhosted.org/packages/8b/18/6db0de4e8911ba14e31853201b40c0fa9fea5ecf3feb86b0ad58f006dfc3/watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899", size = 452876 },
- { url = "https://files.pythonhosted.org/packages/df/df/092a961815edf723a38ba2638c49491365943919c3526cc9cf82c42786a6/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff", size = 615353 },
- { url = "https://files.pythonhosted.org/packages/f3/cf/b85fe645de4ff82f3f436c5e9032379fce37c303f6396a18f9726cc34519/watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f", size = 613187 },
- { url = "https://files.pythonhosted.org/packages/f6/d4/a9fea27aef4dd69689bc3556718c1157a7accb72aa035ece87c1fa8483b5/watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f", size = 270799 },
- { url = "https://files.pythonhosted.org/packages/df/02/dbe9d4439f15dd4ad0720b6e039bde9d66d1f830331f34c18eb70fa6608e/watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161", size = 284145 },
- { url = "https://files.pythonhosted.org/packages/0f/bb/8461adc4b1fed009546fb797fc0d5698dcfe5e289cb37e1b8f16a93cdc30/watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19", size = 394869 },
- { url = "https://files.pythonhosted.org/packages/55/88/9ebf36b3547176d1709c320de78c1fa3263a46be31b5b1267571d9102686/watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235", size = 384905 },
- { url = "https://files.pythonhosted.org/packages/03/8a/04335ce23ef78d8c69f0913e8b20cf7d9233e3986543aeef95ef2d6e43d2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202", size = 449944 },
- { url = "https://files.pythonhosted.org/packages/17/4e/c8d5dcd14fe637f4633616dabea8a4af0a10142dccf3b43e0f081ba81ab4/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6", size = 456020 },
- { url = "https://files.pythonhosted.org/packages/5e/74/3e91e09e1861dd7fbb1190ce7bd786700dc0fbc2ccd33bb9fff5de039229/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317", size = 482983 },
- { url = "https://files.pythonhosted.org/packages/a1/3d/e64de2d1ce4eb6a574fd78ce3a28c279da263be9ef3cfcab6f708df192f2/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee", size = 520320 },
- { url = "https://files.pythonhosted.org/packages/2c/bd/52235f7063b57240c66a991696ed27e2a18bd6fcec8a1ea5a040b70d0611/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49", size = 500988 },
- { url = "https://files.pythonhosted.org/packages/3a/b0/ff04194141a5fe650c150400dd9e42667916bc0f52426e2e174d779b8a74/watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c", size = 452573 },
- { url = "https://files.pythonhosted.org/packages/3d/9d/966164332c5a178444ae6d165082d4f351bd56afd9c3ec828eecbf190e6a/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1", size = 615114 },
- { url = "https://files.pythonhosted.org/packages/94/df/f569ae4c1877f96ad4086c153a8eee5a19a3b519487bf5c9454a3438c341/watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226", size = 613076 },
- { url = "https://files.pythonhosted.org/packages/15/ae/8ce5f29e65d5fa5790e3c80c289819c55e12be2e1b9f5b6a0e55e169b97d/watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105", size = 271013 },
- { url = "https://files.pythonhosted.org/packages/a4/c6/79dc4a7c598a978e5fafa135090aaf7bbb03b8dec7bada437dfbe578e7ed/watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74", size = 284229 },
- { url = "https://files.pythonhosted.org/packages/37/3d/928633723211753f3500bfb138434f080363b87a1b08ca188b1ce54d1e05/watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3", size = 276824 },
- { url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 },
- { url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 },
- { url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 },
- { url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 },
- { url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 },
- { url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 },
- { url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 },
- { url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 },
- { url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 },
- { url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 },
- { url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 },
- { url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 },
- { url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 },
- { url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
- { url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
- { url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
- { url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
- { url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
- { url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
- { url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
- { url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
- { url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
- { url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
- { url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
- { url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
- { url = "https://files.pythonhosted.org/packages/15/81/54484fc2fa715abe79694b975692af963f0878fb9d72b8251aa542bf3f10/watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21", size = 394967 },
- { url = "https://files.pythonhosted.org/packages/14/b3/557f0cd90add86586fe3deeebd11e8299db6bc3452b44a534f844c6ab831/watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0", size = 384707 },
- { url = "https://files.pythonhosted.org/packages/03/a3/34638e1bffcb85a405e7b005e30bb211fd9be2ab2cb1847f2ceb81bef27b/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff", size = 450442 },
- { url = "https://files.pythonhosted.org/packages/8f/9f/6a97460dd11a606003d634c7158d9fea8517e98daffc6f56d0f5fde2e86a/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a", size = 455959 },
- { url = "https://files.pythonhosted.org/packages/9d/bb/e0648c6364e4d37ec692bc3f0c77507d17d8bb8f75689148819142010bbf/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a", size = 483187 },
- { url = "https://files.pythonhosted.org/packages/dd/ad/d9290586a25288a81dfa8ad6329cf1de32aa1a9798ace45259eb95dcfb37/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8", size = 519733 },
- { url = "https://files.pythonhosted.org/packages/4e/a9/150c1666825cc9637093f8cae7fc6f53b3296311ab8bd65f1389acb717cb/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3", size = 502275 },
- { url = "https://files.pythonhosted.org/packages/44/dc/5bfd21e20a330aca1706ac44713bc322838061938edf4b53130f97a7b211/watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf", size = 452907 },
- { url = "https://files.pythonhosted.org/packages/50/fe/8f4fc488f1699f564687b697456eb5c0cb8e2b0b8538150511c234c62094/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a", size = 615927 },
- { url = "https://files.pythonhosted.org/packages/ad/19/2e45f6f6eec89dd97a4d281635e3d73c17e5f692e7432063bdfdf9562c89/watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b", size = 613435 },
- { url = "https://files.pythonhosted.org/packages/91/17/dc5ac62ca377827c24321d68050efc2eaee2ebaf3f21d055bbce2206d309/watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27", size = 270810 },
- { url = "https://files.pythonhosted.org/packages/82/2b/dad851342492d538e7ffe72a8c756f747dd147988abb039ac9d6577d2235/watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43", size = 284866 },
- { url = "https://files.pythonhosted.org/packages/6f/06/175d5ac6b838fb319008c0cd981d7bf289317c510154d411d3584ca2b67b/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18", size = 396269 },
- { url = "https://files.pythonhosted.org/packages/86/ee/5db93b0b57dc0587abdbac4149296ee73275f615d790a82cb5598af0557f/watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817", size = 386010 },
- { url = "https://files.pythonhosted.org/packages/75/61/fe0dc5fedf152bfc085a53711f740701f6bdb8ab6b5c950402b681d4858b/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0", size = 450913 },
- { url = "https://files.pythonhosted.org/packages/9f/dd/3c7731af3baf1a9957afc643d176f94480921a690ec3237c9f9d11301c08/watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d", size = 453474 },
- { url = "https://files.pythonhosted.org/packages/6b/b4/c3998f54c91a35cee60ee6d3a855a069c5dff2bae6865147a46e9090dccd/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3", size = 395565 },
- { url = "https://files.pythonhosted.org/packages/3f/05/ac1a4d235beb9ddfb8ac26ce93a00ba6bd1b1b43051ef12d7da957b4a9d1/watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e", size = 385406 },
- { url = "https://files.pythonhosted.org/packages/4c/ea/36532e7d86525f4e52a10efed182abf33efb106a93d49f5fbc994b256bcd/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb", size = 450424 },
- { url = "https://files.pythonhosted.org/packages/7a/e9/3cbcf4d70cd0b6d3f30631deae1bf37cc0be39887ca327a44462fe546bf5/watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42", size = 452488 },
-]
-
-[[package]]
-name = "websockets"
-version = "14.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/28/fa/76607eb7dcec27b2d18d63f60a32e60e2b8629780f343bb83a4dbb9f4350/websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885", size = 163089 },
- { url = "https://files.pythonhosted.org/packages/9e/00/ad2246b5030575b79e7af0721810fdaecaf94c4b2625842ef7a756fa06dd/websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397", size = 160741 },
- { url = "https://files.pythonhosted.org/packages/72/f7/60f10924d333a28a1ff3fcdec85acf226281331bdabe9ad74947e1b7fc0a/websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610", size = 160996 },
- { url = "https://files.pythonhosted.org/packages/63/7c/c655789cf78648c01ac6ecbe2d6c18f91b75bdc263ffee4d08ce628d12f0/websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3", size = 169974 },
- { url = "https://files.pythonhosted.org/packages/fb/5b/013ed8b4611857ac92ac631079c08d9715b388bd1d88ec62e245f87a39df/websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980", size = 168985 },
- { url = "https://files.pythonhosted.org/packages/cd/33/aa3e32fd0df213a5a442310754fe3f89dd87a0b8e5b4e11e0991dd3bcc50/websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8", size = 169297 },
- { url = "https://files.pythonhosted.org/packages/93/17/dae0174883d6399f57853ac44abf5f228eaba86d98d160f390ffabc19b6e/websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7", size = 169677 },
- { url = "https://files.pythonhosted.org/packages/42/e2/0375af7ac00169b98647c804651c515054b34977b6c1354f1458e4116c1e/websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f", size = 169089 },
- { url = "https://files.pythonhosted.org/packages/73/8d/80f71d2a351a44b602859af65261d3dde3a0ce4e76cf9383738a949e0cc3/websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d", size = 169026 },
- { url = "https://files.pythonhosted.org/packages/48/97/173b1fa6052223e52bb4054a141433ad74931d94c575e04b654200b98ca4/websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d", size = 163967 },
- { url = "https://files.pythonhosted.org/packages/c0/5b/2fcf60f38252a4562b28b66077e0d2b48f91fef645d5f78874cd1dec807b/websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2", size = 164413 },
- { url = "https://files.pythonhosted.org/packages/15/b6/504695fb9a33df0ca56d157f5985660b5fc5b4bf8c78f121578d2d653392/websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166", size = 163088 },
- { url = "https://files.pythonhosted.org/packages/81/26/ebfb8f6abe963c795122439c6433c4ae1e061aaedfc7eff32d09394afbae/websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f", size = 160745 },
- { url = "https://files.pythonhosted.org/packages/a1/c6/1435ad6f6dcbff80bb95e8986704c3174da8866ddb751184046f5c139ef6/websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910", size = 160995 },
- { url = "https://files.pythonhosted.org/packages/96/63/900c27cfe8be1a1f2433fc77cd46771cf26ba57e6bdc7cf9e63644a61863/websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c", size = 170543 },
- { url = "https://files.pythonhosted.org/packages/00/8b/bec2bdba92af0762d42d4410593c1d7d28e9bfd952c97a3729df603dc6ea/websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473", size = 169546 },
- { url = "https://files.pythonhosted.org/packages/6b/a9/37531cb5b994f12a57dec3da2200ef7aadffef82d888a4c29a0d781568e4/websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473", size = 169911 },
- { url = "https://files.pythonhosted.org/packages/60/d5/a6eadba2ed9f7e65d677fec539ab14a9b83de2b484ab5fe15d3d6d208c28/websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56", size = 170183 },
- { url = "https://files.pythonhosted.org/packages/76/57/a338ccb00d1df881c1d1ee1f2a20c9c1b5b29b51e9e0191ee515d254fea6/websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142", size = 169623 },
- { url = "https://files.pythonhosted.org/packages/64/22/e5f7c33db0cb2c1d03b79fd60d189a1da044e2661f5fd01d629451e1db89/websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d", size = 169583 },
- { url = "https://files.pythonhosted.org/packages/aa/2e/2b4662237060063a22e5fc40d46300a07142afe30302b634b4eebd717c07/websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a", size = 163969 },
- { url = "https://files.pythonhosted.org/packages/94/a5/0cda64e1851e73fc1ecdae6f42487babb06e55cb2f0dc8904b81d8ef6857/websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b", size = 164408 },
- { url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 },
- { url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 },
- { url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 },
- { url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 },
- { url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 },
- { url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 },
- { url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 },
- { url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 },
- { url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 },
- { url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 },
- { url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 },
- { url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 },
- { url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 },
- { url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 },
- { url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 },
- { url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 },
- { url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 },
- { url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 },
- { url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 },
- { url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 },
- { url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 },
- { url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 },
- { url = "https://files.pythonhosted.org/packages/6f/eb/367e0ed7b8a960b4fc12c7c6bf3ebddf06875037de641637994849560d47/websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe", size = 163087 },
- { url = "https://files.pythonhosted.org/packages/96/f7/1f18d028ec4a2c14598dfec6a73381a915c27464b693873198c1de872095/websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12", size = 160740 },
- { url = "https://files.pythonhosted.org/packages/5c/db/b4b353fb9c3f0eaa8138ea4c76e6fa555b6d2821ed2d51d0ac3c320bc57e/websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7", size = 160992 },
- { url = "https://files.pythonhosted.org/packages/b9/b1/9149e420c61f375e432654d5c1545e563b90ac1f829ee1a8d1dccaf0869d/websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5", size = 169757 },
- { url = "https://files.pythonhosted.org/packages/2b/33/0bb58204191e113212360f1392b6b1e9f85f62c7ca5b3b15f52f2f835516/websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0", size = 168762 },
- { url = "https://files.pythonhosted.org/packages/be/3d/c3c192f16210d7b7535fbf4ee9a299612f4dccff665587617b13fa0a6aa3/websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258", size = 169060 },
- { url = "https://files.pythonhosted.org/packages/a6/73/75efa8d9e4b1b257818a7b7a0b9ac84a07c91120b52148941370ef2c8f16/websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0", size = 169457 },
- { url = "https://files.pythonhosted.org/packages/a4/11/300cf36cfd6990ffb218394862f0513be8c21917c9ff5e362f94599caedd/websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4", size = 168860 },
- { url = "https://files.pythonhosted.org/packages/c0/3d/5fd82500714ab7c09f003bde671dad1a3a131ac77b6b11ada72e466de4f6/websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc", size = 168825 },
- { url = "https://files.pythonhosted.org/packages/88/16/715580eb6caaacc232f303e9619103a42dcd354b0854baa5ed26aacaf828/websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661", size = 163960 },
- { url = "https://files.pythonhosted.org/packages/63/a7/a1035cb198eaa12eaa9621aaaa3ec021b0e3bac96e1df9ceb6bfe5e53e5f/websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef", size = 164424 },
- { url = "https://files.pythonhosted.org/packages/10/3d/91d3d2bb1325cd83e8e2c02d0262c7d4426dc8fa0831ef1aa4d6bf2041af/websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29", size = 160773 },
- { url = "https://files.pythonhosted.org/packages/33/7c/cdedadfef7381939577858b1b5718a4ab073adbb584e429dd9d9dc9bfe16/websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c", size = 161007 },
- { url = "https://files.pythonhosted.org/packages/ca/35/7a20a3c450b27c04e50fbbfc3dfb161ed8e827b2a26ae31c4b59b018b8c6/websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2", size = 162264 },
- { url = "https://files.pythonhosted.org/packages/e8/9c/e3f9600564b0c813f2448375cf28b47dc42c514344faed3a05d71fb527f9/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c", size = 161873 },
- { url = "https://files.pythonhosted.org/packages/3f/37/260f189b16b2b8290d6ae80c9f96d8b34692cf1bb3475df54c38d3deb57d/websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a", size = 161818 },
- { url = "https://files.pythonhosted.org/packages/ff/1e/e47dedac8bf7140e59aa6a679e850c4df9610ae844d71b6015263ddea37b/websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3", size = 164465 },
- { url = "https://files.pythonhosted.org/packages/f7/c0/8e9325c4987dcf66d4a0d63ec380d4aefe8dcc1e521af71ad17adf2c1ae2/websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f", size = 160773 },
- { url = "https://files.pythonhosted.org/packages/5a/6e/c9a7f2edd4afddc4f8cccfc4e12468b7f6ec40f28d1b1e966a8d0298b875/websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42", size = 161006 },
- { url = "https://files.pythonhosted.org/packages/f3/10/b90ece894828c954e674a81cb0db250e6c324c54db30a8b19e96431f928f/websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f", size = 162260 },
- { url = "https://files.pythonhosted.org/packages/52/93/1147b6b5464a5fb6e8987da3ec7991dcc44f9090f67d9c841d7382fed429/websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574", size = 161868 },
- { url = "https://files.pythonhosted.org/packages/32/ab/f7d80b4049bff0aa617507330db3a27389d0e70df54e29f7a3d76bbd2086/websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270", size = 161813 },
- { url = "https://files.pythonhosted.org/packages/cd/cc/adc9fb85f031b8df8e9f3d96cc004df25d2643e503953af5223c5b6825b7/websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365", size = 164457 },
- { url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 },
-]
-
-[[package]]
-name = "zipp"
-version = "3.21.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 },
-]