diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..52beec2
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,17 @@
+.git
+.github
+**/__pycache__/
+*.py[cod]
+.venv
+*venv/
+.env
+.env.*
+todo_folder/
+.pytest_cache/
+.mypy_cache/
+.ruff_cache/
+.coverage
+htmlcov/
+docs/
+tests/
+scl/storage/skills/
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..dc765e3
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,25 @@
+# Copy this file to `.env` and fill in the values below.
+# `.env` is gitignored — never commit real credentials.
+
+# OceanBase Configuration
+OCEANBASE_HOST=
+OCEANBASE_PORT=
+OCEANBASE_USER=
+OCEANBASE_PASSWORD=
+OCEANBASE_DATABASE=
+
+# LLM Configuration
+API_KEY=
+BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
+MODEL=qwen-plus
+
+# Embedding Configuration
+EMBEDDING_API_KEY=
+EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1
+EMBEDDING_MODEL=BAAI/bge-large-zh-v1.5
+EMBEDDING_MODEL_DIMS=1024
+
+# OpenTelemetry Configuration
+OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+OTEL_TRACES_EXPORTER=otlp
+OTEL_EXPORTER_OTLP_PROTOCOL=grpc
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 47e1412..0f14863 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -1,22 +1,40 @@
# path .github/workflows/test.yaml
-# This workflow runs unit tests on every PR and after merges.
-# Features (as defined in project header):
-# - runs per PR level and after merge
-# - checks out code
-# - sets up Python environment
-# - installs dependencies via requirements.txt
-# - installs pytest and pytest-asyncio as test tool
-# - runs tests via pytest ./scl/test/
+# CI: lint, type-check, and unit tests on every PR and after merges to main.
+# Dependencies are installed with uv from the locked pyproject/uv.lock.
-name: Unit Tests
+name: CI
on:
push:
branches: [ main ] # after merge
- pull_request: # per PR level
+ pull_request: # per PR level
branches: [ main ]
jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
+ - name: Install dependencies
+ run: uv sync --extra dev
+
+ - name: Ruff lint
+ run: uv run ruff check scl tests main.py
+
+ - name: Ruff format check
+ run: uv run ruff format --check scl tests main.py
+
+ - name: Mypy type check
+ # Non-blocking for now: the codebase is being typed incrementally.
+ # Remove continue-on-error once mypy passes cleanly.
+ continue-on-error: true
+ run: uv run mypy scl
+
test:
runs-on: ubuntu-latest
strategy:
@@ -27,26 +45,21 @@ jobs:
- name: Check out code
uses: actions/checkout@v4
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
+ run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
-
- - name: Install test tools
- run: pip install pytest pytest-asyncio
+ run: uv sync --extra dev --python ${{ matrix.python-version }}
- name: Run unit tests
- run: pytest ./scl/test/
-
-# Example usage:
-# This workflow is automatically triggered by:
-# - Pushing to main/master
-# - Creating, updating, or reopening a pull request targeting main/master
-# Developers can also manually trigger it via the "Actions" tab if needed.
-# To debug, inspect the "Run unit tests" step logs in the GitHub Actions console.
-# run test via pytest ./scl/test/
\ No newline at end of file
+ run: uv run --python ${{ matrix.python-version }} pytest ./tests/ --cov=scl --cov-report=xml
+
+ - name: Upload coverage artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-${{ matrix.python-version }}
+ path: coverage.xml
+ if-no-files-found: ignore
diff --git a/.gitignore b/.gitignore
index 7b97130..b4816f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,51 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.egg-info/
+*.egg
+.eggs/
+build/
+dist/
+*.so
+
+# Virtual environments
.venv
-__pycache__
-node_modules
+*venv
+env/
+
+# Environment / secrets
.env
+
+# Testing & coverage
+.pytest_cache/
+.coverage
+.coverage.*
+coverage.xml
+htmlcov/
+.tox/
+.cache/
+
+# Type/lint caches
+.mypy_cache/
+.ruff_cache/
+.dmypy.json
+
+# Logs
*.log
-*venv
\ No newline at end of file
+
+# Editors / IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Node
+node_modules/
+
+# SCL runtime
+todo_folder/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..3a8257e
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,14 @@
+repos:
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.14.0
+ hooks:
+ - id: ruff
+ args: [--fix]
+ - id: ruff-format
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.18.2
+ hooks:
+ - id: mypy
+ args: [--ignore-missing-imports]
+ files: ^scl/
+ exclude: ^scl/(test|storage/skills)/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d2ca728
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,20 @@
+FROM python:3.13-slim
+
+# uv for fast, reproducible installs
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
+
+WORKDIR /app
+
+# Install dependencies from pyproject.toml for better layer caching.
+# README.md is needed by setuptools during build metadata resolution.
+COPY pyproject.toml README.md ./
+RUN uv pip install --system --no-cache -e .
+
+# Application code (skills submodule is intentionally not included; it is
+# optional runtime data fetched separately).
+COPY scl ./scl
+COPY main.py prometheus.yml ./
+
+EXPOSE 8080
+
+CMD ["python", "main.py"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8931215
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,48 @@
+.DEFAULT_GOAL := help
+.PHONY: help install lint format format-check typecheck test check run build lock docker-build docker-up docker-down clean
+
+help: ## Show this help
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
+ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
+
+install: ## Install dependencies (core + dev) into the uv environment
+ uv sync --extra dev
+
+lint: ## Run ruff linter
+ uv run ruff check scl tests main.py
+
+format: ## Auto-format code with ruff
+ uv run ruff format scl tests main.py
+
+format-check: ## Check formatting without modifying files
+ uv run ruff format --check scl tests main.py
+
+typecheck: ## Run mypy type checker
+ uv run mypy scl
+
+test: ## Run the test suite with coverage
+ uv run pytest
+
+check: lint format-check typecheck test ## Run all CI checks (lint, format, types, tests)
+
+run: ## Start the SCL service
+ uv run python main.py
+
+build: ## Build the wheel and sdist into dist/
+ uv build
+
+lock: ## Refresh uv.lock
+ uv lock
+
+docker-build: ## Build the application Docker image
+ docker build -t scl-app .
+
+docker-up: ## Start app + prometheus via docker compose
+ docker compose up --build
+
+docker-down: ## Stop and remove docker compose services
+ docker compose down
+
+clean: ## Remove caches and build artifacts
+ find . -type d -name __pycache__ -not -path '*/skills/*' -exec rm -rf {} + 2>/dev/null || true
+ rm -rf .pytest_cache .ruff_cache .mypy_cache .coverage coverage.xml *.egg-info dist build
diff --git a/README.md b/README.md
index 273b1ce..9df7405 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
# Structured Context Language (SCL)
-## Vision
+**A standardized agent loop runtime for context engineering.**
+SCL is a middleware layer for LLM-powered agents — analogous to what SQL is for databases and Hibernate is for Java.
+
+---
+
+## Overview
+
+Context engineering decomposes LLM interaction into three independent dimensions, unified into one event-driven runtime:
+
+| Dimension | Description | Nature |
+|-----------|-------------|--------|
+| **Business Content** | Instructions tailored to specific prompts and scenarios | The "query" |
+| **Tool Calling** | Functions, MCP, and skills that fetch or mutate external data | Spatial expansion |
+| **Memory Management** | Selecting relevant history from multi-turn conversations | Temporal expansion |
+
+SCL provides a pluggable, observable agent loop that handles all three dimensions through a **standardized interface** — so you focus on your business logic, not the plumbing.
+
+---
+
+## Features
+
+- **Unified Provider Interface** — Anthropic, OpenAI, Google, xAI, Groq, OpenRouter, and any OpenAI-compatible endpoint via a single API
+- **RAG-based Tool Selection** — Progressively loads relevant tool definitions using BM25 + embedding hybrid search, avoiding context bloat
+- **File-first Persistence** — File system as the backbone; REST API validates and delegates to file watcher for decoupling
+- **Pluggable Storage** — File system, OceanBase, and PostgreSQL/pgvector backends via a common `StoreBase` interface
+- **Composite Embedding** — Cache → local (SentenceTransformer) → web API (OpenAI-compatible) with automatic fallback
+- **Observability** — Full OpenTelemetry instrumentation (traces, metrics, structured logs) across all components
+- **Built-in Toolset** — File read/write, grep, bash, git, cron — extensible via capability registration
+- **Multiple Runtime Forms** — RESTful service, interactive TUI, or direct library import
+- **Minimalist Core** — No built-in planners, sub-agents, or background processes; you control the orchestration
+
+---
+
+## Quick Start
+
+```bash
+# Install
+pip install -e .
+
+# Start the service
+scl
+
+# Submit a task via REST API
+curl -X POST http://localhost:8080/tasks \
+ -H "Content-Type: application/json" \
+ -d '{"system_prompt": "You are a helpful assistant.", "capacity": ["bash", "file_read"]}'
+
+# Or drop a file into the watch directory
+echo '{"system_prompt": "Hello"}' > ./todo_folder/task.json
+```
+
+See the [Getting Started Guide](docs/04-getting-started.md) for full instructions.
+
+---
+
+## Use as a Library
+
+```python
+from scl.meta.task import Task
+from scl.queue.task_queue import TaskQueue
+from scl.processor.task_processor import TaskProcessor
+
+queue = TaskQueue()
+processor = TaskProcessor(queue)
+processor.start()
+
+task = Task(system_prompt="You are a helpful assistant.")
+queue.add(task) # auto-notifies the processor
+```
+
+---
+
+## Documentation
-Everyone is familiar with SQL for interacting with databases. In the era of large language models, our focus is shifting from prompt engineering to context engineering.
+| Document | Description |
+|----------|-------------|
+| [Overview & Philosophy](docs/01-overview.md) | Vision, design principles, and philosophy |
+| [Architecture](docs/02-architecture.md) | Component breakdown, data flow, and system design |
+| [Core Concepts](docs/03-core-concepts.md) | Task, Capability, CapRegistry, Embedding, Storage, Queues, Processors |
+| [Getting Started](docs/04-getting-started.md) | Installation, configuration, and quick start |
+| [SDK Reference](docs/05-sdk-reference.md) | API reference and common usage patterns |
+| [Development Roadmap](docs/06-development.md) | Status, roadmap, and contributing |
-In this project, we aim to build a Structured Context Language (SCL), drawing on the practices of context engineering to occupy a niche similar to that of SQL.
+### Research
-Through this practice, we hope to distill a middleware layer. This middleware will provide a standardized interface for agents, playing a role analogous to Hibernate for Java applications.
+- [A Way to Auto-Scaling Capabilities for Agents](docs/blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md) — RAG-based tool selection evaluated against BFCL, MCPToolBench++, and ToolE benchmarks
-## Deconstructing SCL
+---
-If we treat a prompt as a query language for large language models (LLMs), then context engineering is certainly one implementation of that query language. We can deconstruct context engineering along three independent dimensions:
+## Architecture in Brief
-- **Business content**: Concrete instructions tailored to specific prompts and scenarios.
-- **Tool calling**: Various tools available to the LLM to fetch additional external data.
-- **Memory management**: In multi-turn conversations, deciding which historical content is relevant to the current query.
+```
+Listeners (REST / File Watch / Internal)
+ │
+ ▼ (write files)
+todo_folder/ ─── file-based persistence layer
+ │
+ ▼
+ Queue System ─── TaskQueue, CapabilityTaskQueues, Awaiting Queues
+ │
+ ▼
+ Processors ─── TaskProcessor, CapTaskProcessor, Awaiting Processors
+ │
+ ▼
+ Core Services ─── CapRegistry (RAG), Embedding, Storage, LLM Chat
+ │
+ ▼
+ Observability ─── OpenTelemetry (traces, metrics, logs)
+```
-> We can view tool calling as a spatial expansion of information, and memory management as a temporal expansion of information.
+---
-In engineering practice, we can manage memory through tool calls. Therefore, within context engineering, the expansion of information can be accomplished via a standardized interface, which can be further distilled into a standardized workflow.
+## Project Status
-Inspired by the progressive loading mechanism of Claude Skills, we also see that between different tools, progressive loading can allow the model to autonomously select tools. Unlike stored procedures that are explicitly defined and called in SQL, this provides extra autonomy through progressive loading.
+**Current version: 0.1.0** — Active development.
-## SCL Agent Loop: A Unified Agent Runtime
+| Area | Status |
+|------|--------|
+| Core agent loop | ✅ Stable |
+| RAG tool selection | ✅ Stable (BM25 + Embedding hybrid) |
+| REST & file watch | ✅ Operational |
+| Storage backends | ✅ fsstore, ⏳ OceanBase, ⏳ pgvector |
+| Docker deployment | ✅ Ready |
+| WebSocket hooks | 📋 Planned |
+| Debug framework | 📋 Planned |
-Building on the above ideas, SCL provides a standardized agent loop as the runtime middleware for context engineering. It unifies the processing of these three dimensions into an event-driven execution model.
+See [Development Roadmap](docs/06-development.md).
-### Design Principles
+---
-- **Minimalist YOLO Mode**
- No built-in TODO lists, planning, sub-agents, or background bash processes. Developers externalize state through files, compose tools through bash, and implement skill execution by spawning new tasks. We want the framework to do one thing——run an agent——and give the user full control and observability.
+## Benchmarks
-- **Unified Provider Interface**
- A single API supporting Anthropic, OpenAI, Google, xAI, Groq, Cerebras, OpenRouter, and any OpenAI-compatible endpoint. Provides streaming, tool calls with TypeBox schema validation, reasoning/thinking support, seamless cross-provider context handoff, and token and cost tracking.
+SCL's RAG-based capability selection has been evaluated against industry benchmarks:
-- **Tool Registration and Selection**
- Built-in tool registry that maintains tool metadata and descriptions. The agent uses a RAG-based mechanism to progressively load available tools, injecting only the relevant tool definitions into the context when needed. This avoids context bloat from full tool descriptions and preserves model autonomy while respecting progressive loading.
+| Benchmark | Type | Top-5 Recall |
+|-----------|------|-------------|
+| BFCL (multiple) | Function call selection | 95.2% |
+| BFCL (parallel) | Parallel function calls | 93.8% |
+| MCPToolBench++ (single) | MCP tool selection | 99.6% |
+| ToolE (Qwen3-Embedding) | Tool selection | 83.7% |
-- **Pluggable Content Compression**
- A hot-swappable interface for content compression strategies. During long conversations, a customizable compressor can distill historical messages to reduce token consumption while retaining critical information, improving stability for long-running agents.
+Details in the [research blog](docs/blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md).
-- **Prompt Templates**
- Structured template support for business content, facilitating reuse, version management, and team collaboration. Templates can incorporate proven prompt patterns from context engineering practice.
+---
-- **Multiple Runtime Forms**
- - **RESTful (containerized)** — Deployed as a service, providing API access.
- - **Local TUI** — Interactive terminal usage with slash commands and session management.
- - **Library** — Directly imported for secondary development and deep integration.
+## Contributing
-- **Observability**
- Transparent event streaming across the entire workflow: tool call parameters and results, every incremental model output, and internal state changes are all traceable. This is essential for debugging, evaluation, and building trust.
+```bash
+# Setup
+pip install -e ".[dev]"
-- **Built-in Toolset**
- Out-of-the-box tools covering common coding and operations tasks:
- - File read/write
- - Search (grep, find)
- - Bash
- - Git
- - Create cron jobs
- - Other tools available for function call branching
+# Run checks
+make lint # ruff linter
+make typecheck # mypy
+make test # pytest with coverage
+make check # all of the above
- Tools can be extended through the registration mechanism, supporting both native implementations and CLI-wrapped commands.
+# See all targets
+make help
+```
-### How It Reflects SCL’s Philosophy
+Pull requests are welcome. Please maintain OpenTelemetry instrumentation and structured logging for new components.
-- **Unified Information Expansion**
- Both tool calling (spatial expansion) and memory management (temporal expansion) are handled within the agent loop through the same standardized tool interface. Memory management is no longer a special internal mechanism; it is invoked, recorded, and observed like any other tool.
+---
-- **Progressive Loading Engineered**
- The RAG-based tool selection extends the progressive loading concept from “skill files” to all tools. The model first understands the need, then fetches tool descriptions on demand, and autonomously decides which resources to use. This balances context efficiency with autonomy.
+## License
-- **Middleware Positioning**
- Just as Hibernate provides a standard abstraction for persistence in Java applications, the SCL Agent Loop aims to provide a standardized interface for context management in agent applications. It encapsulates provider differences, tool execution, compression strategies, and runtime modes, allowing developers to focus on business prompts and task-flow design.
+[Apache License 2.0](LICENSE)
diff --git a/README_CN.md b/README_CN.md
index a5f9be5..5f8bc01 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -1,73 +1,182 @@
-# 结构化上下文语言(Structured Context Language)
+
+
+
+
+
+
+
+
+
+
+
-## 愿景
-大家都很熟悉用于与数据库交互的 SQL。如今,面对大语言模型,我们的焦点正从提示工程转向上下文工程。
+# Structured Context Language (SCL)
-在本项目中,我们尝试构建一种结构化上下文语言(Structured Context Language),旨在借鉴上下文工程的实践,占据一个类似于 SQL 的生态位。
+**面向上下文工程的标准化智能体运行时中间件。**
+SCL 致力于成为 LLM 时代的上下文工程基础设施——如同 SQL 之于数据库,Hibernate 之于 Java。
-我们希望通过这一实践,能够总结出一套中间件。该中间件将为智能体提供标准化的接口,其角色类似于 Hibernate 之于 Java 应用程序。
+---
-## 解构 SCL
+## 概述
-如果将提示词视为一种面向大语言模型(LLM)的查询语言,那么上下文工程无疑是这种查询语言的一种实现方式。我们可以从三个相互独立的维度来解构上下文工程:
+上下文工程可以从三个独立维度进行解构,SCL 将它们统一到一个事件驱动的运行时中:
-- 业务内容:面向具体提示词和场景的具体指示。
-- 工具调用:LLM 能够使用的各种工具,旨在获取额外的外部数据。
-- 记忆管理:在多轮对话场景下,决定哪些历史内容与当前查询相关。
+| 维度 | 说明 | 性质 |
+|------|------|------|
+| **业务内容** | 面向具体场景的提示词指令 | 即"查询本身" |
+| **工具调用** | 函数调用、MCP、Skill 等获取或变更外部数据的能力 | 信息的空间扩展 |
+| **记忆管理** | 多轮会话中筛选相关历史内容 | 信息的时间扩展 |
-> 我们可以将工具调用视为对信息的空间扩展,而将记忆管理视为信息在时间维度的扩展。
+SCL 通过一套**标准化接口**处理上述三个维度,让你专注于业务逻辑而非基础设施。
-考虑到在工程实践中,我们可以通过工具调用来实现记忆管理的交互,因此在上下文工程中,对于信息的扩展查询可以使用一种标准化接口来完成,并进一步总结为一个标准化流程。
+---
-受益于 Claude Skill 的渐进式加载机制,我们也看到在不同工具之间,可以通过渐进式加载的方式实现大模型对工具的自主选择。这与在 SQL 中定义并显式调用执行的存储过程不同,它通过渐进式加载提供了额外的自主性。
+## 功能特性
-## SCL Agent Loop:统一智能体运行时
+- **统一供应商接口** — 一个 API 支持 Anthropic、OpenAI、Google、xAI、Groq、OpenRouter 及任何兼容 OpenAI 的端点
+- **RAG 驱动的工具选择** — 通过 BM25 + Embedding 混合搜索渐进式加载工具定义,避免上下文膨胀
+- **文件优先的持久化** — 文件系统为核心,REST API 负责校验后写入文件,由文件监听器解耦处理
+- **可插拔存储后端** — 文件系统、OceanBase、PostgreSQL/pgvector,统一 `StoreBase` 接口
+- **复合 Embedding** — 缓存 → 本地 (SentenceTransformer) → Web API (兼容 OpenAI) 自动降级
+- **可观测性** — 全链路 OpenTelemetry 埋点(调用链、指标、结构化日志)
+- **内置工具集** — 文件读写、grep、bash、git、cron — 通过注册机制可扩展
+- **多种运行形态** — RESTful 服务、交互式 TUI、或作为库直接引用
+- **极简核心** — 不内置规划器、子智能体或后台进程;编排逻辑完全由你掌控
-基于上述思想,SCL 提供一个标准化的智能体循环(Agent Loop),作为上下文工程的运行时中间件。它将上述三个维度的处理统一到一个事件驱动的执行模型中。
+---
-### 设计原则
+## 快速开始
-- **极简 YOLO 模式**
- 不内置 TODO、计划、子智能体、后台 bash。开发者通过文件外化状态、通过 bash 组合工具、通过新建 task 实现技能执行。我们希望框架只做“运行智能体”这一件事,并给使用者完全的控制权和可观测性。
+```bash
+# 安装
+pip install -e .
-- **统一供应商接口**
- 一个 API 支持 Anthropic、OpenAI、Google、xAI、Groq、Cerebras、OpenRouter 及任何兼容 OpenAI 的端点。提供流式传输、基于 TypeBox 模式校验的工具调用、思维/推理支持、无缝跨供应商上下文交接,以及令牌和成本追踪。
+# 启动服务
+scl
-- **工具注册与选择**
- 内置工具注册中心,维护工具的元数据与描述。智能体通过 RAG 机制渐进式加载可用工具,只在需要时将相关工具定义注入上下文,避免全量工具描述带来的上下文膨胀。这种按需选择的方式继承了渐进式加载的思想,同时保留模型的自主性。
+# 通过 REST API 提交任务
+curl -X POST http://localhost:8080/tasks \
+ -H "Content-Type: application/json" \
+ -d '{"system_prompt": "你是一个助手。", "capacity": ["bash", "file_read"]}'
-- **可插拔的内容压缩**
- 提供内容压缩策略的热插拔接口。在长对话中,通过可定制的压缩器精简历史消息,在保留关键信息的前提下降低令牌消耗,提升智能体长期运行时的稳定性。
+# 或者直接往监听目录扔文件
+echo '{"system_prompt": "你好"}' > ./todo_folder/task.json
+```
-- **Prompt 模板**
- 为业务内容提供结构化模板支持,便于复用、版本管理和团队协作。模板可结合上下文工程实践,固化有效的提示模式。
+详见[入门指南](docs/04-getting-started.md)。
-- **多种运行形态**
- - **RESTful(容器)** —— 作为服务部署,提供 API 接入。
- - **本地 TUI** —— 在终端中交互式使用,支持斜杠命令、会话管理。
- - **库文件** —— 直接引用,支持二次开发与深度集成。
+---
-- **可观测性**
- 全流程事件流透明输出:工具调用的参数与结果、模型输出的每一步变化、内部状态的变更均可追踪。这对调试、评估和信任构建至关重要。
+## 作为库使用
-- **内置工具集**
- 开箱即用的基础工具,覆盖常见编码与运维任务:
- - 文件读写
- - 查找(grep、find)
- - bash
- - git
- - 创建 cron job
- - 以及其他可被 function call 分支选用的工具
+```python
+from scl.meta.task import Task
+from scl.queue.task_queue import TaskQueue
+from scl.processor.task_processor import TaskProcessor
- 工具可以通过注册机制扩展,既支持内置实现,也支持通过 CLI 包装已有命令。
+queue = TaskQueue()
+processor = TaskProcessor(queue)
+processor.start()
-### 如何呼应 SCL 的思想
+task = Task(system_prompt="你是一个助手。")
+queue.add(task) # 自动通知处理器
+```
-- **信息扩展的统一**
- 工具调用(空间扩展)和记忆管理(时间扩展)都在 agent loop 内通过标准化工具接口完成。记忆管理不再是一种特殊的内部机制,而是像其他工具一样被调用、记录和观察。
+---
-- **渐进式加载的工程化**
- 工具选择的 RAG 机制将渐进式加载从“技能文件”延伸到所有工具,模型先理解需求,再按需获取工具说明,自主决定使用哪些资源。这平衡了上下文效率与自主性。
+## 文档
-- **中间件定位**
- 正如 Hibernate 为 Java 应用提供持久化的标准抽象,SCL Agent Loop 旨在为智能体应用提供上下文管理的标准化接口。它封装了供应商差异、工具执行、压缩策略和运行时模式,使开发者可以更专注于业务提示和任务流设计。
+| 文档 | 说明 |
+|------|------|
+| [概览与设计理念](docs/01-overview.md) | 愿景、设计原则与哲学 |
+| [架构](docs/02-architecture.md) | 组件分解、数据流与系统设计 |
+| [核心概念](docs/03-core-concepts.md) | Task、Capability、CapRegistry、Embedding、Storage、Queue、Processor |
+| [入门指南](docs/04-getting-started.md) | 安装、配置与快速上手 |
+| [SDK 参考](docs/05-sdk-reference.md) | API 参考与常见使用模式 |
+| [开发路线图](docs/06-development.md) | 项目状态、路线图与贡献指南 |
+
+### 研究
+
+- [面向 Agent 的能力自动缩放方案](docs/blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md) — 基于 RAG 的工具选择,在 BFCL、MCPToolBench++ 和 ToolE 基准上的评估
+
+---
+
+## 架构概览
+
+```
+监听器 (REST / 文件监听 / 内部任务)
+ │
+ ▼ (写入文件)
+todo_folder/ ─── 文件级持久化层
+ │
+ ▼
+ 队列系统 ─── TaskQueue、CapabilityTaskQueues、等待队列
+ │
+ ▼
+ 处理器 ─── TaskProcessor、CapTaskProcessor、等待处理器
+ │
+ ▼
+ 核心服务 ─── CapRegistry (RAG)、Embedding、Storage、LLM Chat
+ │
+ ▼
+ 可观测性 ─── OpenTelemetry (调用链、指标、日志)
+```
+
+---
+
+## 项目状态
+
+**当前版本: 0.1.0** — 活跃开发中
+
+| 模块 | 状态 |
+|------|------|
+| 核心 Agent 循环 | ✅ 稳定 |
+| RAG 工具选择 | ✅ 稳定(BM25 + Embedding 混合) |
+| REST 与文件监听 | ✅ 可用 |
+| 存储后端 | ✅ fsstore, ⏳ OceanBase, ⏳ pgvector |
+| Docker 部署 | ✅ 就绪 |
+| WebSocket 钩子 | 📋 计划中 |
+| 调试框架 | 📋 计划中 |
+
+详见[开发路线图](docs/06-development.md)。
+
+---
+
+## 基准测试
+
+SCL 的 RAG 能力选择在行业基准上的表现:
+
+| 基准 | 类型 | Top-5 召回率 |
+|------|------|-------------|
+| BFCL (multiple) | 函数调用选择 | 95.2% |
+| BFCL (parallel) | 并行函数调用 | 93.8% |
+| MCPToolBench++ (single) | MCP 工具选择 | 99.6% |
+| ToolE (Qwen3-Embedding) | 工具选择 | 83.7% |
+
+详见[研究博客](docs/blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md)。
+
+---
+
+## 参与贡献
+
+```bash
+# 环境准备
+pip install -e ".[dev]"
+
+# 运行检查
+make lint # ruff 代码检查
+make typecheck # mypy 类型检查
+make test # pytest + 覆盖率
+make check # 全量检查
+
+# 查看所有目标
+make help
+```
+
+欢迎提交 Pull Request。新组件请保持 OpenTelemetry 埋点和结构化日志的规范。
+
+---
+
+## 许可证
+
+[Apache License 2.0](LICENSE)
diff --git a/doc/SDK_reference.md b/doc/SDK_reference.md
deleted file mode 100644
index 5f64506..0000000
--- a/doc/SDK_reference.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# SDK reference
-```
-# Setup telemetry
-logger = logging.getLogger(__name__)
-from scl.otel.otel import init_telemetry
-init_telemetry()
-
-def main():
- todo_queue = TaskQueue()
- logger.info("Starting Todo Receiver Application")
-
- # Ensure watch directory exists
- watch_dir = os.getenv("TODO_WATCH_DIR", "./todo_folder")
- os.makedirs(watch_dir, exist_ok=True)
-
- # Start todo processor
- processor = TaskProcessor(todo_queue)
- processor.start()
-
- # Start listeners
- file_handler = FileHandler(watch_dir, todo_queue)
- rest_handler = RestFulHandler(watch_dir, host="0.0.0.0", port="8080")
-
- file_observer = file_handler.start()
- api_thread = threading.Thread(target=rest_handler.start, daemon=True)
- # Wait for termination signal
- def shutdown(signum, frame):
- logger.info("Shutting down...")
- file_observer.stop()
- file_observer.join()
- sys.exit(0)
-
- signal.signal(signal.SIGINT, shutdown)
- signal.signal(signal.SIGTERM, shutdown)
- api_thread.start()
-
- # Keep main thread alive
- try:
- while True:
- time.sleep(1)
- except KeyboardInterrupt:
- shutdown(None, None)
-
-if __name__ == "__main__":
- main()
-```
-
-# Impl details
-
-## ./embeddings package
-
-embedding service
-
-## ./listener package
-
-Impls for how receive data from outside and inside.
-Note: That file listener always be 1st class citizen, so that everything keep on disk by design.
-
-## ./meta package
-
-Internal data structure define
-
-## ./otel package
-
-otel support and service, for usage:
-```
-Reference coding rules:
-OTEL:
-import logging
-from scl.otel.otel import tracer,meter
-from opentelemetry import trace
-
-class example:
- def __init__(self):
- self.some_counter = meter.create_counter(
- "business",
- description="business"
- )
- self.logger = logging.getLogger(__name__)
-
- @tracer.start_as_current_span("function...")
- def function(self...)
- current_span = trace.get_current_span()
- ##update span....
- ##business impls(either status change or invoke other packages)
- self.logger.debug("debug msg for business impls")
- self.some_counter.add(1) # metric changes
-```
-
-## processor package
-
-Internal processor task impls
-
-## queue pacakge
-
-Internal queue system impls
-
-## storage
-
-storage service
-
-## config.py
-
-class to handle configuration
\ No newline at end of file
diff --git a/doc/prompt.md b/doc/prompt.md
deleted file mode 100644
index 3da7e9d..0000000
--- a/doc/prompt.md
+++ /dev/null
@@ -1,141 +0,0 @@
-This is a harness prompt template used to generate code for this repo.
-
-```
-
-
-
-
-
-```
-
-Generally and also as feature in PR should have as:
-
-```
-You are a software developer.
-We need to completed the feature in python code.
-You will impl python code as repsonse.
-
-Targets:
-We are going to design a main.py with main function as entry of the project.
-In current phase, we will focus on how this project receive inputs from other parts.
-
-Assumption:
-- You can create an class as todo, don't need impl it for now.
-
-Project Constraints:
-- please impls in python.
-- please relay on otel for tracing, metric, logs.
-- please design log for info and debug level.
-- Any status change/business action should be reported as a metric.
-- Any status change/business action should have a log record.
-- for any python dependency, provides install script as pip install instead of requirements.txt.
-
-Business Constraints:
-- Once we start this code, we expected receive data from:
-1. rest api
-2. listen to file system change, as new file added into todo folder.
-3. todo item generated during item processing.
-
-Testing Constraints:
-- You need to provide script to prepare env.
-- You need to provide a script to test the code.
-```
-
-Which one step further, we suppose we can attempt with self explain style:
-```
-You are a software developer.
-You will see a file, the features and design goals are listed at beginning of the file.
-Please let me know once you are ready.
-
-Targets:
-Your will compare which features are implemented and which are not.
-If there any feature not been implemented please implements it.
-Consider we are doing opensource project, some features may missing in the comments.
-You should list missing features as well but keep them.
-You will return with full file after updated.
-
-Assumption:
-- For any unknown api usage, please ask human's help or impls a fake.
-
-Project Constraints:
-- please relay on otel for tracing, metric, logs.
-- please design log for info and debug level.
-- for any python dependency, provides install script as pip install instead of requirements.txt.
-
-Business Constraints:
-Please read from comments as beginning of the input code.
-
-Reference coding rules:
-OTEL:
-import logging
-from scl.otel.otel import tracer,meter
-from opentelemetry import trace
-
-class example:
- def __init__(self):
- self.some_counter = meter.create_counter(
- "business",
- description="business"
- )
- self.logger = logging.getLogger(__name__)
-
- @tracer.start_as_current_span("function...")
- def function(self...)
- current_span = trace.get_current_span()
- ##update span....
- ##business impls(either status change or invoke other packages)
- self.logger.debug("debug msg for business impls")
- self.some_counter.add(1) # metric changes
-```
-
-```
-You are a software developer.
-You need to completed the feature list below in a python file.
-You need put feature list as comments at beginning of the file.
-
-Feature list:
-Task class has a system prompt property as string.
-Task class has a prompt list property as string list, as prompt history.
-Task class has a capacity property as string list.
-Task class has a status property as string(in "created", "subtasking", "done").
-Task class has a hash property as hash value of system prompt, prompt list and capacity list.
-Task class has a additional property as map[string]string for extending usage.
-Task class supports json and yaml format for serialization.
-Task class has a previous hash property as string, which support as a hash chain way to trace back to the head.
-Task class has a list of sub task allows to check other sub tasks.
-Task class default as LRU view to show the latest status.
-
-Targets:
-Your will compare which features are implemented and which are not.
-If there any feature not been implemented please implements it.
-Consider we are doing opensource project, some features may missing in the comments.
-You should list missing features as well but keep them.
-You will return with full file after updated.
-
-Project Constraints:
-- please relay on otel for tracing, metric, logs.
-- please design log for info and debug level.
-- for any python dependency, provides install script as pip install instead of requirements.txt.
-
-Reference coding rules:
-OTEL:
-import logging
-from scl.otel.otel import tracer,meter
-from opentelemetry import trace
-
-class example:
- def __init__(self):
- self.some_counter = meter.create_counter(
- "business",
- description="business"
- )
- self.logger = logging.getLogger(__name__)
-
- @tracer.start_as_current_span("function...")
- def function(self...)
- current_span = trace.get_current_span()
- ##update span....
- ##business impls(either status change or invoke other packages)
- self.logger.debug("debug msg for business impls")
- self.some_counter.add(1) # metric changes
-```
\ No newline at end of file
diff --git a/doc/todo.md b/doc/todo.md
deleted file mode 100644
index 621fed3..0000000
--- a/doc/todo.md
+++ /dev/null
@@ -1,69 +0,0 @@
-### Describe your use case
-
-- [ ] SCL——一个面向function call,skill,mcp的专属服务
- - [ ] 解决
- - [ ] 工具选择问题——一个专属RAG
- - [ ] 基于特定function call组合
- - [ ] 链接seekdb作为RAG
- - [ ] 工具过程?执行占用上下文问题——上下文是否回馈是否需要隔离
- - [ ] 压缩上下文
- - [ ] 是否支持甩手工具类型
- - [ ] MCP的文件传递问题?
- - [ ] 安全问题不考虑——沙箱启动方式交给商业化sevice mesh?
- - [ ] 纵向整合,横向兼容
- - [x] 分发方式
- - [x] 参数启动容器化处理——restful
- - [x] 文件目录扫描启动方式
- - [x] 支持SDK直接使用——代码使用
- - [ ] 运维方式
- - [x] 暴露可观测性指标
- - [ ] hook(websocket形式)
- - [ ] 执行过程多种模式
- - [ ] 工具注册
- - [ ] 工具注入
- - [ ] 提示词改写
- - [ ] 工具执行后脱手
- - [ ] 日志回馈
- - [ ] 支持SeekDB,PGvector(把数据库SQL注入即可)
- - [ ] 支持调试模式(要有个调试框架,最大化RAG三类查询的平衡点)
- - [ ] 其他非预设工具支持——商业化服务
- - [ ] 案例/论文
- - [ ] [目标测试集合](https://gorilla.cs.berkeley.edu/blogs/15_bfcl_v4_web_search.html)
- - [ ] RAG + 模型组合
- - [ ] BM25 + DeepSeek v4
- - [ ] Qwen embedding + Qwen跑目标测试集合
- - [ ] 测试的项目
- - [ ] RAG本身能选对不?
- - [ ] 基于历史记录能有多少改善
- - [ ] 基于RAG和历史记录
- - [ ] 指标
- - [ ] 正确率
- - [ ] 能节约多少Token(token在对话中以及tool传递的数量)
-
-### Describe the solution you'd like
-
-as above
-
-### Describe alternatives you've considered
-
-_No response_
-
-### Additional context
-
-_No response_
-
-
-taskQueue.py?
-- [ ] Queue size limit or backpressure handling.
-- [ ] Batch processing support.
-
-Cap.py?
-5. [Missing] Serialization/deserialization methods (to_dict, from_dict) for persistence.
-6. [Missing] Validation of function_impl code safety before sandbox execution.
-7. [Missing] Versioning support for capability changes.
-8. [Missing] Async support for embedding generation.
-
-- notice refactor?
-
-task.py as prompt template?
-making service package for service?
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..38cf9f8
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,20 @@
+services:
+ app:
+ build: .
+ image: scl-app
+ ports:
+ - "8080:8080"
+ env_file:
+ - path: .env
+ required: false
+ depends_on:
+ - prometheus
+ restart: unless-stopped
+
+ prometheus:
+ image: prom/prometheus:latest
+ ports:
+ - "9090:9090"
+ volumes:
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ restart: unless-stopped
diff --git a/docs/01-overview.md b/docs/01-overview.md
new file mode 100644
index 0000000..d617878
--- /dev/null
+++ b/docs/01-overview.md
@@ -0,0 +1,79 @@
+# Project Overview
+
+## Vision
+
+Everyone is familiar with SQL for interacting with databases. In the era of large language models, our focus is shifting from **prompt engineering** to **context engineering**.
+
+**Structured Context Language (SCL)** aims to become a standardized approach to context engineering — occupying a niche similar to what SQL provides for databases.
+
+Through this practice, we hope to distill a **middleware layer** that provides a standardized interface for agents, analogous to Hibernate for Java applications.
+
+## Deconstructing Context Engineering
+
+If we treat a prompt as a query language for LLMs, context engineering is an implementation of that query language. We deconstruct context engineering along **three independent dimensions**:
+
+| Dimension | Description | Analogy |
+|-----------|-------------|---------|
+| **Business Content** | Concrete instructions tailored to specific prompts and scenarios | The "query" |
+| **Tool Calling** | Various tools available to the LLM to fetch additional external data | **Spatial** expansion of information |
+| **Memory Management** | In multi-turn conversations, deciding which historical content is relevant | **Temporal** expansion of information |
+
+> Tool calling expands information in **space**; memory management expands it in **time**.
+
+In engineering practice, memory management can be achieved through tool calls. Therefore, within context engineering, information expansion can be accomplished via a **standardized interface** and further distilled into a **standardized workflow**.
+
+Inspired by [Claude Skills](https://docs.anthropic.com/en/docs/agents-and-tools/claude-skills)' progressive loading mechanism, SCL extends the concept: tools can progressively load so the model autonomously selects what it needs, unlike explicitly defined stored procedures in SQL.
+
+## Design Principles
+
+### Minimalist YOLO Mode
+No built-in TODO lists, planning, sub-agents, or background processes. Developers externalize state through files, compose tools through bash, and implement skill execution by spawning new tasks. The framework does one thing — **run an agent** — and gives the user full control and observability.
+
+### Unified Provider Interface
+A single API supporting Anthropic, OpenAI, Google, xAI, Groq, Cerebras, OpenRouter, and any OpenAI-compatible endpoint. Features:
+- Streaming and tool calls with TypeBox schema validation
+- Reasoning/thinking support
+- Seamless cross-provider context handoff
+- Token and cost tracking
+
+### Tool Registration and Selection (RAG-based)
+Built-in tool registry maintaining metadata and descriptions. The agent uses a **RAG-based mechanism** to progressively load available tools, injecting only relevant tool definitions into the context when needed. This avoids context bloat and preserves model autonomy.
+
+### Pluggable Content Compression
+A hot-swappable interface for content compression strategies. During long conversations, a customizable compressor distills historical messages to reduce token consumption while retaining critical information.
+
+### Prompt Templates
+Structured template support for business content, facilitating reuse, version management, and team collaboration.
+
+### Multiple Runtime Forms
+- **RESTful (containerized)** — Deployed as a service with API access
+- **Local TUI** — Interactive terminal usage with slash commands and session management
+- **Library** — Direct import for secondary development
+
+### Observability
+Full OpenTelemetry integration: tool call parameters and results, incremental model outputs, and internal state changes are all traceable via traces, metrics, and structured logs.
+
+### Built-in Toolset
+Out-of-the-box tools covering common tasks:
+- File read/write
+- Search (grep, find)
+- Bash execution
+- Git operations
+- Cron job management
+- Extensible via registration mechanism
+
+## Project Status
+
+Current version: **0.1.0**
+
+SCL is in **active development**. See [06-development.md](06-development.md) for the current roadmap and status.
+
+## Relationships with Ecosystem
+
+| Concept | Relation to SCL |
+|---------|----------------|
+| **Function Call** | A capability type - direct tool invocation |
+| **MCP (Model Context Protocol)** | A capability type - standardized protocol for tool access |
+| **Skill** | A capability type with progressive disclosure semantics |
+| **RAG** | Core mechanism for tool selection and capability injection |
+| **OpenTelemetry** | Observability foundation |
diff --git a/docs/02-architecture.md b/docs/02-architecture.md
new file mode 100644
index 0000000..f50f1e4
--- /dev/null
+++ b/docs/02-architecture.md
@@ -0,0 +1,227 @@
+# System Architecture
+
+## High-Level Architecture
+
+SCL follows an **event-driven architecture** with file-system-based persistence as its backbone. The system can be divided into several layers:
+
+```
+ ┌─────────────────────────────────────────────────────────┐
+ │ Entry Point │
+ │ main.py / CLI │
+ └──────────┬──────────────────────┬──────────────────────┘
+ │ │
+ ┌──────▼──────┐ ┌─────▼──────┐
+ │ Listeners │ │ REST API │
+ │ (File Watch)│ │ (FastAPI) │
+ └──────┬──────┘ └─────┬──────┘
+ │ │
+ └─────────┬───────────┘
+ │ (write files)
+ ┌───────▼────────┐
+ │ todo_folder/ │ ← File-based persistence layer
+ │ (watch dir) │
+ └───────┬────────┘
+ │
+ ┌───────▼──────────────────────┐
+ │ Queue System │
+ │ ┌────────┐ ┌──────────────┐ │
+ │ │TaskQueue│ │CapTaskQueues │ │
+ │ └───┬────┘ └──────┬───────┘ │
+ │ ┌───▼────┐ ┌──────▼───────┐ │
+ │ │Awaiting│ │Awaiting │ │
+ │ │Caps │ │Approvals │ │
+ │ └────────┘ └──────────────┘ │
+ └───────┬──────────────────────┘
+ │
+ ┌───────▼──────────────────────┐
+ │ Processor System │
+ │ ┌──────────┐ ┌────────────┐ │
+ │ │Task │ │CapTask │ │
+ │ │Processor │ │Processor │ │
+ │ └──────────┘ └────────────┘ │
+ │ ┌──────────┐ ┌────────────┐ │
+ │ │Awaiting │ │Awaiting │ │
+ │ │Caps │ │Approve │ │
+ │ │Processor │ │Processor │ │
+ │ └──────────┘ └────────────┘ │
+ └───────┬──────────────────────┘
+ │
+ ┌───────▼──────────────────────┐
+ │ Core Services │
+ │ ┌────────────┐ ┌──────────┐ │
+ │ │ CapRegistry│ │Embedding │ │
+ │ │ (RAG) │ │Service │ │
+ │ └────────────┘ └──────────┘ │
+ │ ┌────────────┐ ┌──────────┐ │
+ │ │ Storage │ │ LLM Chat │ │
+ │ │ Backends │ │ Provider │ │
+ │ └────────────┘ └──────────┘ │
+ └───────┬──────────────────────┘
+ │
+ ┌───────▼──────┐
+ │ Observability │
+ │ (OpenTelemetry)│
+ └──────────────┘
+```
+
+## Component Breakdown
+
+### 1. Listeners (`scl/listener/`)
+
+The entry points for data ingestion. SCL supports three input channels:
+
+| Listener | File | Description |
+|----------|------|-------------|
+| **File Watch** | `file_watch.py` | Watches a directory (`todo_folder`) for new/modified files; the **1st class citizen** |
+| **REST API** | `restful_watch.py` | FastAPI-based RESTful interface for external API access |
+| **Internal Watch** | `internal_watch.py` | Handles tasks generated internally during processing |
+
+**Design philosophy:** File listener is the 1st class citizen. REST API validates incoming data and writes it as a file, leaving processing to the file listener. Internal tasks also create files. This ensures persistence and decoupling.
+
+### 2. Queue System (`scl/queue/`)
+
+Manages task lifecycle through multiple queues:
+
+| Queue | File | Purpose |
+|-------|------|---------|
+| **TaskQueue** | `task_queue.py` | Main queue for Task instances; thread-safe; notifies registered processors |
+| **CapabilityTaskQueues** | `cap_task_queues.py` | Hash-map-based queue for capability tasks (parallel execution) |
+| **AwaitingCapTasksQueue** | `awaiting_cap_tasks_queue.py` | Heap-ordered queue for tasks blocked waiting for capability results |
+| **AwaitingApproveQueue** | `awaiting_approve_queue.py` | Queue for tasks waiting human approval |
+
+### 3. Processor System (`scl/processor/`)
+
+Consumes tasks from queues with exponential backoff:
+
+| Processor | File | Consumes From |
+|-----------|------|---------------|
+| **TaskProcessor** | `task_processor.py` | TaskQueue |
+| **CapTaskProcessor** | `cap_task_processor.py` | CapabilityTaskQueues |
+| **AwaitingCapTasksProcessor** | `await_cap_tasks_processor.py` | AwaitingCapTasksQueue |
+| **AwaitingApproveProcessor** | `awaiting_approve_processor.py` | AwaitingApproveQueue |
+| **BaseQueueProcessor** | `base_queue_processor.py` | Abstract base with backoff/notify/status |
+
+All processors inherit from `BaseQueueProcessor`, which provides:
+- Infinite processing loop with configurable polling
+- Exponential backoff on empty queues
+- Thread-safe start/stop/join lifecycle
+- Wake-up notification mechanism
+
+### 4. Core Services
+
+#### Capability Registry (`scl/cap_reg.py`)
+
+The central `CapRegistry` class manages capability lifecycle:
+- **Name-based retrieval** — `getCapsByNames()` / `get_cap_by_name()`
+- **Semantic search (RAG)** — `getCapsBySimilarity()` using BM25 + Embedding
+- **History-based suggestion** — `getCapsByHistory()` (stub; future enhancement)
+- **Usage recording** — `record()` for collaborative filtering style recommendations
+
+#### Embedding Service (`scl/embeddings/`)
+
+A composite embedding system with priority fallback:
+1. Cache check (persistent JSON)
+2. Local embedding (SentenceTransformer)
+3. Web API (OpenAI-compatible, defaults to SiliconFlow)
+
+| Component | File | Description |
+|-----------|------|-------------|
+| CompositeEmbedding | `embedding.py` | Singleton coordinator, priority-based selection |
+| LocalEmbeddingClient | `local_embedding.py` | SentenceTransformer-based local inference |
+| WebEmbeddingClient | `web_embedding.py` | OpenAI-compatible API client |
+| EmbeddingCache | `embedding_cache.py` | Persistent cache for computed embeddings |
+| BaseEmbedding | `base_embedding.py` | Abstract base class for embedding backends |
+
+#### Storage Backends (`scl/storage/`)
+
+Pluggable storage via `StoreBase` abstract interface:
+
+| Backend | File | Description |
+|---------|------|-------------|
+| **StoreBase** (abstract) | `base.py` | Defines the uniform interface |
+| **FileSystem Store** | `fsstore.py` | File-based capability storage with BM25 + embedding similarity search |
+| **OceanBase Store** | `oceanbasestore.py` | OceanBase vector store backend (optional, requires `[oceanbase]`) |
+| **PostgreSQL Store** | `pgstore.py` | PostgreSQL + pgvector backend (optional, requires `[postgres]`) |
+
+The **FileSystem Store** (`fsstore`) is the primary implementation, featuring:
+- Directory-based capability loading from property files
+- BM25 indexing via `rank-bm25`
+- Embedding similarity search
+- 5 combination strategies for hybrid scoring (minmax, sigmoid, tanh, etc.)
+- Pickle-based cache persistence
+- Duplicate detection with similarity threshold
+
+#### LLM Chat Provider (`scl/llm_chat.py`)
+
+The `send_messages()` function orchestrates:
+1. Named tool lookup
+2. Semantic tool search (autonomy sidecar)
+3. History-based tool suggestion
+4. Tool merging and deduplication
+5. LLM invocation with merged tools
+6. Tool call result processing and recording
+
+#### Capabilities (`scl/capabilities/`)
+
+Built-in tool implementations:
+
+| Capability | File | Description |
+|------------|------|-------------|
+| Bash | `bash.py` | Shell command execution |
+| File Read | `fileread.py` | Read file contents |
+| File Write | `filewrite.py` | Write content to files |
+| Git | `git.py` | Git operations |
+| Grep | `grep.py` | Text search |
+
+### 5. Meta Models (`scl/meta/`)
+
+Core data structures:
+
+| Model | File | Description |
+|-------|------|-------------|
+| **Task** | `task.py` | Main task entity with prompt, capacity, status, hash chain, subtasks |
+| **Capability** (abstract) | `capability.py` | Abstract base for Skill and FunctionCall |
+| **Skill** | `skill.py` | Progressive disclosure skill implementation |
+| **FunctionCall** | `functioncall.py` | Direct function call implementation |
+| **CapTask** | `captask.py` | Invocation task for a specific capability |
+| **Msg** | `msg.py` | Message wrapper with embedding |
+| **Skills Reference** | `skills_ref/` | Parser, models, and error handling for skill configurations |
+
+### 6. Observability (`scl/otel/`)
+
+Full OpenTelemetry instrumentation:
+
+| Component | File | Description |
+|-----------|------|-------------|
+| Init | `init.py` | Telemetry initialization |
+| Core | `otel.py` | Tracer, meter, and shared metric definitions |
+| Traces | `traces.py` | Span management utilities |
+| Metrics | `metrics.py` | Metric definitions |
+| Metric Decorator | `metric_decorator.py` | `@record_latency` decorator |
+
+## Data Flow
+
+```
+1. Input arrives via one of the Listeners (REST, file watch, internal)
+2. Listener writes data as a file to the todo_watch_dir
+3. FileWatcher detects new file and enqueues a Task to TaskQueue
+4. TaskProcessor consumes the Task and processes it
+5. During processing, LLM Chat determines which capabilities to invoke
+6. CapRegistry performs RAG-based tool selection (BM25 + Embedding)
+7. Selected capabilities are merged and sent to the LLM
+8. LLM responds with tool calls → CapTasks are created
+9. CapTasks are queued in CapabilityTaskQueues
+10. CapTaskProcessor executes capabilities in parallel
+11. Results flow back through the system
+```
+
+## Configuration
+
+SCL uses environment-variable-based configuration via the `Config` dataclass (`scl/config.py`). See [04-getting-started.md](04-getting-started.md) for details.
+
+## System Requirements
+
+- Python ≥ 3.11
+- OpenTelemetry-compatible collector (optional, for observability)
+- SentenceTransformer (optional, for local embedding)
+- PostgreSQL with pgvector or OceanBase (optional, for vector storage)
diff --git a/docs/03-core-concepts.md b/docs/03-core-concepts.md
new file mode 100644
index 0000000..06b8ede
--- /dev/null
+++ b/docs/03-core-concepts.md
@@ -0,0 +1,331 @@
+# Core Concepts
+
+## 1. Task (`scl/meta/task.py`)
+
+A `Task` is the fundamental unit of work in SCL. It represents a user request or system operation.
+
+### Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `system_prompt` | `str` | System prompt for the LLM |
+| `prompt_list` | `list[str]` | Prompt history (conversation context) |
+| `capacity` | `list[str]` | Required capability identifiers |
+| `status` | `str` | One of `"created"`, `"subtasking"`, `"done"` |
+| `approval` | `bool` | Approval flag; defaults to `True` |
+| `additional` | `dict[str,str]` | Extension data key-value store |
+| `previous_hash` | `str\|None` | Hash of predecessor (hash chain) |
+| `hash` | `str` | SHA-256 of `(system_prompt, prompt_list, capacity)` |
+| `sub_tasks` | `list[Task]` | Child tasks (hierarchy support) |
+| `cap_tasks` | `list[CapTask]` | Associated capability invocation tasks |
+| `created_at` / `updated_at` | `datetime` | Timestamps for LRU ordering |
+
+### Hash Chain
+
+Tasks form a hash chain via `previous_hash`, enabling traceability back to the root task. The chain is verified through parent-child relationships.
+
+```python
+# Creating a task chain
+root = Task(system_prompt="Root task")
+child = Task(system_prompt="Child task")
+root.add_subtask(child)
+# child.previous_hash automatically set to root.hash
+is_valid = child.verify_hash_chain()
+```
+
+### LRU Status View
+
+`get_latest_status()` walks the task hierarchy and returns the status of the most recently updated node (self or any descendant), providing a quick "freshest" state view.
+
+### Serialization
+
+Tasks support JSON and YAML serialization:
+
+```python
+json_str = task.to_json(indent=2)
+restored = Task.from_json(json_str)
+
+yaml_str = task.to_yaml() # requires PyYAML
+restored = Task.from_yaml(yaml_str)
+```
+
+---
+
+## 2. Capability (`scl/meta/capability.py`)
+
+`Capability` is an **abstract base class** for all tool-like entities in SCL, encompassing both Skills and Function Calls.
+
+### Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `name` | `str` | Unique name of the capability |
+| `type` | `str` | Implementation type (`"skill"`, `"function"`, etc.) |
+| `description` | `str\|None` | Human-readable description for RAG/progressive loading |
+| `original_body` | `str\|None` | Original source/body of the capability |
+| `llm_description` | `str\|None` | LLM-formatted description for tool injection |
+| `function_impl` | `str\|None` | Code implementation for sandbox execution |
+| `embedding_description` | `Any` | **Lazy-loaded** embedding vector for RAG search |
+
+### Embedding Computation
+
+The `embedding_description` property computes and caches the embedding lazily (first access triggers computation). This avoids unnecessary embedding calls for capabilities that are registered but never queried.
+
+### Abstract Method
+
+```python
+class Capability(ABC):
+ @abstractmethod
+ def execute(self, args_dict: dict[str, Any]) -> Any: ...
+```
+
+### Concrete Implementations
+
+| Class | File | Description |
+|-------|------|-------------|
+| **Skill** | `scl/meta/skill.py` | Progressive disclosure skill with `.skill` file parsing |
+| **FunctionCall** | `scl/meta/functioncall.py` | Direct function call capability |
+
+---
+
+## 3. CapTask (`scl/meta/captask.py`)
+
+A `CapTask` represents a **single invocation** of a capability. When created, it automatically writes a JSON file to the `todo_watch_dir`, making it visible to the file watcher.
+
+### Properties
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `cap_name` | `str` | Name of the capability to invoke |
+| `args` | `list[Any]` | Arguments for the capability call |
+| `hash` | `str` | Auto-generated UUID identifier |
+| `task_hash` | `str\|None` | Parent task hash |
+| `approval` | `bool` | Approval flag (default `True`) |
+| `status` | `str` | One of `"created"`, `"Processed"`, `"Error"` |
+| `full_result` | `str` | Complete output from capability invocation |
+| `result` | `str` | **First 500 lines** of `full_result` (property) |
+
+```python
+task = CapTask(cap_name="send_email", args=["user@example.com", "Hello!"])
+task.full_result = "Sent successfully\n..."
+task.set_status("Processed")
+```
+
+---
+
+## 4. Capability Registry (`scl/cap_reg.py`)
+
+The `CapRegistry` is the central hub for capability discovery and management. It wraps any `StoreBase` implementation to provide three retrieval strategies:
+
+### Retrieval Modes
+
+```
+User Query
+ │
+ ├──► getCapsByNames() — Exact name lookup
+ │ (for explicitly specified tools)
+ │
+ ├──► getCapsBySimilarity() — Semantic search (RAG)
+ │ (BM25 + Embedding hybrid scoring)
+ │
+ └──► getCapsByHistory() — Usage history suggestion
+ (collaborative filtering; stub in fsstore)
+```
+
+### How Tools Are Selected (in `llm_chat.py`)
+
+When processing a new user query (turn 0), the system:
+
+1. Fetches **named tools** explicitly provided
+2. Performs **semantic search** to find relevant tools via RAG
+3. Queries **usage history** for contextual suggestions
+4. **Merges** all results (named tools take precedence)
+5. Injects merged tools into the LLM context
+6. Records which capabilities were actually used
+
+```python
+# Pseudocode of the selection flow
+tools_named = cap_registry.getCapsByNames(ToolNames)
+tools_autonomy = cap_registry.getCapsBySimilarity(msg, limit, min_similarity)
+tools_history = cap_registry.getCapsByHistory(msg, limit, min_similarity)
+
+tools_merged = {**tools_named, **tools_autonomy, **tools_history}
+```
+
+---
+
+## 5. Embedding System (`scl/embeddings/`)
+
+The embedding system provides a **composite** interface with priority fallback:
+
+```
+embed(text)
+ │
+ 1. Check cache ──────────────────► hit? → return cached vector
+ │
+ 2. Try local (SentenceTransformer) ──► success? → return vector
+ │ fail? → fall through
+ │
+ 3. Web API (OpenAI-compatible) ──► return vector, cache it
+```
+
+The singleton `CompositeEmbedding` ensures consistent backend selection across the application.
+
+```python
+from scl.embeddings.embedding import embed
+vector = embed("What is the capital of France?")
+```
+
+### Supported Backends
+
+| Backend | Requirements | Priority |
+|---------|-------------|----------|
+| Cache | (built-in) | 1st |
+| Local (SentenceTransformer) | `pip install sentence-transformers`, set `EMBEDDING_LOCAL_MODEL_PATH` | 2nd |
+| Web API (OpenAI-compatible) | Set `EMBEDDING_API_KEY` and `EMBEDDING_BASE_URL` | 3rd |
+
+---
+
+## 6. Storage Backends (`scl/storage/`)
+
+SCL provides a pluggable storage architecture via the `StoreBase` abstract interface.
+
+### StoreBase Interface
+
+```python
+class StoreBase(ABC):
+ def get_cap_by_name(name: str) -> Capability: ...
+ def search_by_similarity(msg: Msg, limit, min_similarity) -> dict[str, Capability]: ...
+ def record(msg: Msg, cap: Capability) -> None: ...
+ def getCapsByHistory(msg: Msg, limit, min_similarity) -> dict[str, Capability]: ...
+ def insert_capability(cap: Capability) -> None: ...
+```
+
+### FileSystem Store (`fsstore`)
+
+The primary backend. Key capabilities:
+
+- **BM25 indexing** via `rank-bm25` for keyword search
+- **Embedding similarity** via cosine similarity for semantic search
+- **5 hybrid scoring methods** combining BM25 and embedding:
+ 1. `minmax` — Min-max normalized BM25 + embedding
+ 2. `sigmoid` — Sigmoid BM25 + embedding
+ 3. `tanh` — Tanh BM25 + embedding
+ 4. `minmax_sigmoid` — Min-max BM25 + sigmoid BM25
+ 5. `minmax_tanh` — Min-max BM25 + tanh BM25
+- **Duplicate detection** with configurable similarity threshold
+- **Pickle cache** for fast startup
+
+```python
+store = fsstore(path="/path/to/capabilities", init=True, embedding_service_on=True)
+
+# Combined search
+results = store.search_by_similarity(
+ query, limit=5, min_similarity=0.3,
+ combine_method="minmax", alpha=0.7
+)
+```
+
+### Vector Database Backends (Optional)
+
+| Backend | Install | Description |
+|---------|---------|-------------|
+| OceanBase | `pip install scl[oceanbase]` | OceanBase vector store |
+| PostgreSQL + pgvector | `pip install scl[postgres]` | PG vector store |
+
+---
+
+## 7. Queue System (`scl/queue/`)
+
+Four queues manage task lifecycle in an event-driven model:
+
+```
+Incoming Task
+ │
+ ▼
+┌──────────────┐ ┌──────────────────┐
+│ TaskQueue │────►│ TaskProcessor │
+│ (main queue) │ │ (consumes tasks) │
+└──────────────┘ └────────┬─────────┘
+ │
+ Creates CapTasks (if LLM calls tools)
+ │
+ ▼
+ ┌──────────────────┐
+ │ CapabilityTask │
+ │ Queues │ ← Hash map for parallel execution
+ └────────┬─────────┘
+ │
+ ┌────────▼────────┐ ┌──────────────────┐
+ │ AwaitingCapTasks│───►│ AwaitingCapTasks │
+ │ Queue (heap) │ │ Processor │
+ └─────────────────┘ └──────────────────┘
+
+ ┌──────────────────┐ ┌──────────────────┐
+ │ AwaitingApprove │──►│ AwaitingApprove │
+ │ Queue │ │ Processor │
+ └──────────────────┘ └──────────────────┘
+```
+
+### Design Rationale
+
+As described in [blog/Story.md](blog/Story.md), different data structures serve different queue needs:
+
+| Queue | Structure | Why |
+|-------|-----------|-----|
+| TaskQueue | FIFO Queue | Sequential task processing |
+| CapabilityTaskQueues | Hash Map | Different capabilities can execute in parallel |
+| AwaitingCapTasks | Heap | Ordered by number of pending caps (highest first) |
+| AwaitingApprove | Queue | Human approval needed |
+
+---
+
+## 8. Processor System (`scl/processor/`)
+
+All processors extend `BaseQueueProcessor` which provides:
+
+### BaseQueueProcessor Features
+
+- **Infinite loop** with configurable polling interval
+- **Exponential backoff** on empty queue (min → max sleep interval)
+- **Status management** (`idle`, `running`, `stopped`)
+- **Notification mechanism** — wake-up signal when new items arrive
+- **Thread-safe lifecycle** — `start()`, `stop()`, `join()`
+- **OpenTelemetry instrumentation** — traces, metrics, structured logging
+
+```python
+class MyProcessor(BaseQueueProcessor):
+ def _get_item(self): # Fetch one item (non-blocking)
+ def _process_item(self, item): # Business logic
+```
+
+---
+
+## 9. Message (`scl/meta/msg.py`)
+
+`Msg` wraps LLM conversation messages and provides embedding for RAG search:
+
+```python
+class Msg:
+ messages: list # List of message dicts (role/content)
+ embed: list # Embedding vector of the messages
+
+ def append(context) # Add a message to the conversation
+ def append_cap_result(out, id) # Add a tool result
+```
+
+---
+
+## 10. Built-in Capabilities (`scl/capabilities/`)
+
+Pre-installed tools available out of the box:
+
+| Tool | File | Module Name |
+|------|------|-------------|
+| Bash | `bash.py` | `scl.capabilities.bash` |
+| File Read | `fileread.py` | `scl.capabilities.fileread` |
+| File Write | `filewrite.py` | `scl.capabilities.filewrite` |
+| Git | `git.py` | `scl.capabilities.git` |
+| Grep/Search | `grep.py` | `scl.capabilities.grep` |
+
+Tools are registered via the capability registry and can be extended through the same mechanism.
diff --git a/docs/04-getting-started.md b/docs/04-getting-started.md
new file mode 100644
index 0000000..60b541d
--- /dev/null
+++ b/docs/04-getting-started.md
@@ -0,0 +1,173 @@
+# Getting Started
+
+## Installation
+
+### From Source
+
+```bash
+# Clone the repository
+git clone https://github.com/Teingi/StructuredContextLanguage.git
+cd StructuredContextLanguage
+
+# Install dependencies
+pip install -e .
+
+# Optional: install extra backends
+pip install -e ".[oceanbase]" # OceanBase vector store
+pip install -e ".[postgres]" # PostgreSQL + pgvector
+pip install -e ".[local]" # Local embedding (SentenceTransformer)
+pip install -e ".[dev]" # Development tools (pytest, ruff, mypy)
+```
+
+### Using pip (future)
+
+```bash
+pip install structured-context-language
+```
+
+## Configuration
+
+SCL is configured via **environment variables**. All settings are optional with sensible defaults.
+
+### Core Configuration
+
+| Environment Variable | Default | Description |
+|---------------------|---------|-------------|
+| `TODO_WATCH_DIR` | `./todo_folder` | Directory for file-based task persistence |
+| `API_HOST` | `0.0.0.0` | REST API bind address |
+| `API_PORT` | `8080` | REST API port |
+| `SERVICE_NAME` | `SCL` | Service name for telemetry |
+| `LOG_LEVEL` | `INFO` | Logging level |
+
+### Embedding Configuration
+
+| Environment Variable | Default | Description |
+|---------------------|---------|-------------|
+| `EMBEDDING_MODEL` | `BAAI/bge-m3` | Embedding model name |
+| `EMBEDDING_MODEL_DIMS` | `1024` | Embedding dimensions |
+| `EMBEDDING_API_KEY` | — | API key for web embedding service |
+| `EMBEDDING_BASE_URL` | `https://api.siliconflow.cn/v1` | Web embedding endpoint |
+| `EMBEDDING_LOCAL_MODEL_PATH` | — | Path for local SentenceTransformer model |
+| `EMBEDDING_CACHE_PATH` | — | Path for embedding cache |
+
+### RAG / Search Configuration
+
+| Environment Variable | Default | Description |
+|---------------------|---------|-------------|
+| `LIMIT` | `5` | Top-k results for capability search |
+| `MIN_SIMILARITY` | `0.5` | Minimum similarity threshold |
+
+### OpenTelemetry Configuration
+
+| Environment Variable | Default | Description |
+|---------------------|---------|-------------|
+| `OTLP_ENDPOINT` | `http://localhost:4318` | OTLP HTTP endpoint |
+| `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` | `http://localhost:4318` | Metrics endpoint |
+
+## Quick Start
+
+### 1. Start the SCL Service
+
+```bash
+# Set the watch directory
+export TODO_WATCH_DIR=./todo_folder
+
+# Run SCL
+python main.py
+```
+
+This starts:
+- A **file watcher** on `./todo_folder`
+- A **REST API** server on `0.0.0.0:8080`
+- The **task processor** loop
+
+### 2. Submit a Task via REST API
+
+```bash
+curl -X POST http://localhost:8080/task \
+ -H "Content-Type: application/json" \
+ -d '{
+ "system_prompt": "You are a helpful assistant.",
+ "prompt_list": ["Hello, what can you do?"],
+ "capacity": ["bash", "file_read"]
+ }'
+```
+
+### 3. Submit a Task via File
+
+Simply create a `.json` file in the watch directory:
+
+```bash
+cat > ./todo_folder/my_task.json << 'EOF'
+{
+ "system_prompt": "You are a helpful assistant.",
+ "prompt_list": ["List files in the current directory."],
+ "capacity": ["bash", "file_read", "file_write"]
+}
+EOF
+```
+
+The file watcher will detect the new file and enqueue it for processing.
+
+### 4. Monitor with OpenTelemetry
+
+If you have an OTLP collector running:
+
+```bash
+# Start a local OTLP collector (example with Docker)
+docker run -p 4318:4318 otel/opentelemetry-collector-contrib
+
+# SCL automatically sends traces, metrics, and logs
+```
+
+## Running Forms
+
+### RESTful (Containerized)
+
+```bash
+# Using Docker
+docker build -t scl .
+docker run -p 8080:8080 \
+ -e TODO_WATCH_DIR=/data/todo \
+ -v ./data:/data \
+ scl
+```
+
+### As a Library
+
+```python
+from scl.config import config
+from scl.queue.task_queue import TaskQueue
+from scl.processor.task_processor import TaskProcessor
+from scl.listener.file_watch import FileHandler
+from scl.meta.task import Task
+from scl.otel.otel import init_telemetry
+
+# Initialize telemetry
+init_telemetry()
+
+# Create queue and processor
+todo_queue = TaskQueue()
+processor = TaskProcessor(todo_queue)
+processor.start()
+
+# Create file watcher
+watch_dir = config.todo_watch_dir
+file_handler = FileHandler(watch_path=watch_dir, task_queue=todo_queue, ...)
+file_observer = file_handler.start()
+
+# Create and submit tasks directly
+task = Task(
+ system_prompt="You are a helpful assistant.",
+ prompt_list=["Hello"],
+ capacity=["bash"]
+)
+todo_queue.add(task)
+```
+
+## Next Steps
+
+- Read [01-overview.md](01-overview.md) to understand the philosophy
+- Explore [03-core-concepts.md](03-core-concepts.md) for detailed model documentation
+- Check [05-sdk-reference.md](05-sdk-reference.md) for SDK integration patterns
+- See [06-development.md](06-development.md) for the current development status
diff --git a/docs/05-sdk-reference.md b/docs/05-sdk-reference.md
new file mode 100644
index 0000000..c9678eb
--- /dev/null
+++ b/docs/05-sdk-reference.md
@@ -0,0 +1,329 @@
+# SDK Reference
+
+## Package Overview
+
+```
+scl/ # Root package
+├── __init__.py # Package init, exports __version__
+├── config.py # Configuration (dataclass, env-based)
+├── cap_reg.py # Capability Registry (CapRegistry)
+├── llm_chat.py # LLM chat orchestration
+├── capabilities/ # Built-in tools
+│ ├── bash.py
+│ ├── fileread.py
+│ ├── filewrite.py
+│ ├── git.py
+│ └── grep.py
+├── embeddings/ # Embedding service
+│ ├── base_embedding.py # Abstract base
+│ ├── embedding.py # CompositeEmbedding (singleton)
+│ ├── embedding_cache.py # Persistent cache
+│ ├── local_embedding.py # SentenceTransformer backend
+│ └── web_embedding.py # OpenAI-compatible API backend
+├── listener/ # Input listeners
+│ ├── file_watch.py # File system watcher
+│ ├── restful_watch.py # REST API (FastAPI)
+│ └── internal_watch.py # Internal task generation
+├── meta/ # Data models
+│ ├── task.py # Task entity
+│ ├── captask.py # Capability task
+│ ├── capability.py # Abstract capability base
+│ ├── functioncall.py # Function call capability
+│ ├── skill.py # Skill capability
+│ ├── msg.py # Message with embedding
+│ └── skills_ref/ # Skill file parser
+├── otel/ # Observability
+│ ├── init.py # Telemetry init
+│ ├── otel.py # Tracer, meter, metrics
+│ ├── traces.py # Span utilities
+│ ├── metrics.py # Metric definitions
+│ └── metric_decorator.py # @record_latency decorator
+├── processor/ # Task processors
+│ ├── base_queue_processor.py
+│ ├── task_processor.py
+│ ├── cap_task_processor.py
+│ ├── await_cap_tasks_processor.py
+│ └── awaiting_approve_processor.py
+├── queue/ # Queue implementations
+│ ├── task_queue.py
+│ ├── cap_task_queues.py
+│ ├── awaiting_cap_tasks_queue.py
+│ └── awaiting_approve_queue.py
+└── storage/ # Storage backends
+ ├── base.py # StoreBase (abstract)
+ ├── fsstore.py # Filesystem store
+ ├── oceanbasestore.py # OceanBase store (optional)
+ └── pgstore.py # PostgreSQL + pgvector (optional)
+```
+
+## Quick API Reference
+
+### Configuration
+
+```python
+from scl.config import config
+
+# Access configuration (all env-based)
+watch_dir = config.todo_watch_dir
+api_port = config.api_port
+limit = config.limit
+
+# Validate
+config.validate()
+```
+
+### Task
+
+```python
+from scl.meta.task import Task
+from scl.meta.captask import CapTask
+
+# Create a task
+task = Task(
+ system_prompt="You are a helpful assistant.",
+ prompt_list=["User: Hello"],
+ capacity=["bash", "file_read"],
+ status="created",
+ approval=True,
+)
+
+# Add subtask
+child = Task(system_prompt="Subtask")
+task.add_subtask(child)
+
+# Add CapTask
+cap = CapTask(cap_name="send_email", args=["user@example.com", "Subject"])
+task.add_cap_task(cap)
+
+# Serialize
+json_str = task.to_json(indent=2)
+yaml_str = task.to_yaml()
+
+# Deserialize
+restored = Task.from_json(json_str)
+
+# Check latest status (LRU)
+latest = task.get_latest_status()
+```
+
+### Capability
+
+```python
+from scl.meta.capability import Capability
+from scl.meta.skill import Skill
+from scl.meta.functioncall import FunctionCall
+
+# Create a skill
+skill = Skill(
+ name="web_search",
+ type="skill",
+ description="Search the web for information",
+)
+
+# Create a function call
+func = FunctionCall(
+ name="calculate",
+ description="Calculate mathematical expression",
+ function_impl="def calculate(expr): return eval(expr)",
+)
+
+# Access embedding (lazy, triggers computation)
+vector = skill.embedding_description
+
+# Execute
+result = func.execute({"expr": "2+2"})
+```
+
+### Message
+
+```python
+from scl.meta.msg import Msg
+
+# Create from messages list
+msg = Msg([{"role": "user", "content": "Hello"}])
+
+# Access properties
+messages = msg.messages
+embedding = msg.embed
+
+# Append tool results
+msg.append_cap_result("tool output", "call_123")
+```
+
+### Capability Registry
+
+```python
+from scl.cap_reg import CapRegistry
+from scl.storage.fsstore import fsstore
+
+# Initialize storage and registry
+store = fsstore(path="./skills", init=True, embedding_service_on=True)
+registry = CapRegistry(store)
+
+# Search by names
+caps = registry.getCapsByNames(["bash", "file_read"])
+
+# Semantic search (RAG)
+msg = Msg([{"role": "user", "content": "I need to read a file"}])
+relevant = registry.getCapsBySimilarity(msg, limit=5, min_similarity=0.5)
+
+# History-based search
+historical = registry.getCapsByHistory(msg, limit=5)
+
+# Record usage
+registry.record(msg, cap)
+```
+
+### Embedding
+
+```python
+from scl.embeddings.embedding import embed
+
+# Get embedding for any text
+vector = embed("What is the capital of France?")
+
+# Also access the singleton client
+from scl.embeddings.embedding import get_embedding_client
+client = get_embedding_client()
+vector = client.embed("Some text")
+```
+
+### LLM Chat
+
+```python
+from scl.llm_chat import send_messages, function_call_playground
+from scl.meta.msg import Msg
+
+# Requires an OpenAI-compatible client
+response = send_messages(
+ client=openai_client,
+ model="gpt-4",
+ cap_registry=registry,
+ ToolNames=["bash"],
+ msg=Msg([{"role": "user", "content": "List files"}]),
+ Turns=0,
+)
+
+# Full function call loop
+result = function_call_playground(
+ client=openai_client,
+ model="gpt-4",
+ cap_registry=registry,
+ ToolNames=["bash"],
+ msg=Msg([{"role": "user", "content": "List files"}]),
+)
+```
+
+### Storage Backend
+
+```python
+from scl.storage.base import StoreBase
+
+# Custom backend
+class MyStore(StoreBase):
+ def get_cap_by_name(self, name):
+ # ...
+ def search_by_similarity(self, msg, limit, min_similarity):
+ # ...
+ def record(self, msg, cap):
+ # ...
+ def getCapsByHistory(self, msg, limit, min_similarity):
+ # ...
+ def insert_capability(self, cap):
+ # ...
+```
+
+### Queues and Processors
+
+```python
+from scl.queue.task_queue import TaskQueue
+from scl.processor.task_processor import TaskProcessor
+
+# Queue
+queue = TaskQueue()
+queue.add(task)
+item = queue.get()
+
+# Processor
+processor = TaskProcessor(input_queue=queue, name="my_processor")
+processor.start()
+# ... later ...
+processor.stop()
+```
+
+### Telemetry
+
+```python
+from scl.otel.otel import init_telemetry, tracer, meter
+
+# Initialize OpenTelemetry
+init_telemetry()
+
+# Use tracer and meter for custom instrumentation
+@tracer.start_as_current_span("my_function")
+def my_function():
+ counter = meter.create_counter("my_counter", description="...")
+ counter.add(1)
+```
+
+## Usage Patterns
+
+### Pattern 1: Task-Driven Agent Loop
+
+```python
+# Setup telemetry
+from scl.otel.otel import init_telemetry
+init_telemetry()
+
+def main():
+ # Initialize queues
+ todo_queue = TaskQueue()
+ captask_queue = CapabilityTaskQueues()
+ waiting_approval_queue = AwaitingApproveQueue()
+ waiting_captask_queue = AwaitingCapTasksQueue()
+
+ # Start processors
+ processor = TaskProcessor(todo_queue)
+ processor.start()
+
+ # Start listeners
+ watch_dir = config.todo_watch_dir
+ file_handler = FileHandler(
+ watch_path=watch_dir,
+ task_queue=todo_queue,
+ captask_queue=captask_queue,
+ waiting_approval_queue=waiting_approval_queue,
+ waiting_captask_queue=waiting_captask_queue,
+ )
+ rest_handler = RestFulHandler(watch_dir, host=config.api_host, port=config.api_port)
+
+ file_observer = file_handler.start()
+ api_thread = threading.Thread(target=rest_handler.start, daemon=True)
+ api_thread.start()
+
+ # Wait for shutdown signal
+ signal.signal(signal.SIGINT, shutdown)
+ signal.signal(signal.SIGTERM, shutdown)
+```
+
+### Pattern 2: Direct SDK Integration
+
+```python
+from scl.config import config
+from scl.queue.task_queue import TaskQueue
+from scl.processor.task_processor import TaskProcessor
+from scl.meta.task import Task
+
+# Minimal setup
+queue = TaskQueue()
+processor = TaskProcessor(queue)
+processor.start()
+
+# Submit work
+task = Task(system_prompt="You are a helpful assistant.")
+queue.add(task)
+
+# Cleanup
+processor.stop()
+processor.join()
+```
diff --git a/docs/06-development.md b/docs/06-development.md
new file mode 100644
index 0000000..ccdca17
--- /dev/null
+++ b/docs/06-development.md
@@ -0,0 +1,131 @@
+# Development
+
+## Project Status
+
+Current version: **0.1.0** — Active development phase.
+
+## Development Setup
+
+```bash
+# Clone and install
+git clone https://github.com/Teingi/StructuredContextLanguage.git
+cd StructuredContextLanguage
+pip install -e ".[dev]"
+
+# Run tests
+pytest
+
+# Lint and type check
+ruff check .
+mypy scl/
+```
+
+## Development Roadmap
+
+### Vision: SCL as a dedicated service for Function Call, Skill, and MCP
+
+- **Tool Selection** — A dedicated RAG system for capability discovery
+ - [x] BM25-based keyword search
+ - [x] Embedding-based semantic search
+ - [x] Hybrid scoring (BM25 + Embedding linear combination)
+ - [ ] Connect to SeekDB as RAG backend
+ - [ ] Debug framework to optimize RAG triple-query balance
+
+- **Context Isolation & Compression**
+ - [ ] Compress context to reduce token usage
+ - [ ] Support fire-and-forget tool execution (detached from context)
+
+- **Integration & Compatibility**
+ - [x] RESTful API (containerized deployment)
+ - [x] File directory watch (local/disk-based)
+ - [x] SDK for direct code usage
+ - [ ] MCP file transfer support
+ - [ ] WebSocket hooks for real-time events
+ - [ ] Support SeekDB, PGvector (SQL-injectable)
+
+- **Runtime Modes**
+ - [ ] Tool registration
+ - [ ] Tool injection
+ - [ ] Prompt rewriting
+ - [ ] Post-execution detachment
+ - [ ] Log feedback loop
+
+- **Security**
+ - Sandbox execution — defer to commercial service mesh
+
+### Benchmark Evaluation (see [research](blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md))
+
+- [x] BFCL test suite (irrelevance, live, parallel, multiple)
+- [x] MCPToolBench++ evaluation
+- [x] MetaTool / ToolE evaluation
+- [ ] RAG + Model combination experiments:
+ - [ ] BM25 + DeepSeek v4
+ - [ ] Qwen embedding + Qwen
+- [ ] Metrics tracking:
+ - [ ] Accuracy / recall
+ - [ ] Token savings in conversation and tool delivery
+
+### Code Quality
+
+- [ ] Queue size limit and backpressure handling
+- [ ] Batch processing support
+- [ ] Capability serialization/deserialization (`to_dict`, `from_dict`)
+- [ ] Versioning support for capability changes
+- [ ] Async support for embedding generation
+- [ ] Validation of `function_impl` code safety before sandbox execution
+
+## Project Structure
+
+```
+StructuredContextLanguage/
+├── main.py # Application entry point
+├── scl/ # Core library
+│ ├── cap_reg.py # Capability Registry
+│ ├── config.py # Configuration
+│ ├── llm_chat.py # LLM interaction orchestration
+│ ├── capabilities/ # Built-in tools
+│ ├── embeddings/ # Embedding backends
+│ ├── listener/ # Input listeners
+│ ├── meta/ # Data models
+│ ├── otel/ # Observability
+│ ├── processor/ # Task processors
+│ ├── queue/ # Queue implementations
+│ └── storage/ # Storage backends
+├── tests/ # Test suite
+├── example/ # Example code and benchmarks
+│ ├── BFCL/ # BFCL benchmark scripts
+│ ├── mcptool/ # MCPTool benchmark scripts
+│ └── MetaTool/ # MetaTool benchmark scripts
+├── docs/ # Documentation
+├── Dockerfile # Container support
+├── docker-compose.yml # Orchestration
+├── prometheus.yml # Prometheus config for metrics
+└── pyproject.toml # Project metadata
+```
+
+## Contributing
+
+### Guidelines
+
+1. **Run tests before submitting**: `pytest`
+2. **Follow linting rules**: `ruff check .`
+3. **Add type hints**: all new code should have proper type annotations
+4. **Add OpenTelemetry instrumentation**: traces and metrics for new components
+5. **Write documentation**: update relevant docs for API changes
+
+### Code Style
+
+- Python ≥ 3.11 with type hints
+- Line length: 100 characters
+- OpenTelemetry instrumentation by default
+- Structured logging at INFO and DEBUG levels
+- Every status change / business action should have:
+ - A trace span (`@tracer.start_as_current_span`)
+ - A log record (`logger.info/debug`)
+ - A metric update (`counter.add()`)
+
+## Releases
+
+- Versioning follows `scl/__init__.py` (`__version__`)
+- CHANGELOG is maintained in commit history
+- See [pyproject.toml](../pyproject.toml) for project metadata
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..d9d3af6
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,61 @@
+# Structured Context Language (SCL) Documentation
+
+> **SCL** — A standardized agent loop runtime for context engineering, providing a middleware layer for LLM-powered agents.
+
+---
+
+## Table of Contents
+
+### 📖 Overview
+
+| Document | Description |
+|----------|-------------|
+| [01-overview.md](01-overview.md) | Project vision, philosophy, and design principles |
+| [02-architecture.md](02-architecture.md) | System architecture, component diagram, and data flow |
+
+### 🎯 Core Concepts
+
+| Document | Description |
+|----------|-------------|
+| [03-core-concepts.md](03-core-concepts.md) | Task, Capability, CapRegistry, Embedding, Storage, Queues, Processors |
+
+### 🚀 Getting Started
+
+| Document | Description |
+|----------|-------------|
+| [04-getting-started.md](04-getting-started.md) | Installation, configuration, and quick start guide |
+
+### 📚 Reference
+
+| Document | Description |
+|----------|-------------|
+| [05-sdk-reference.md](05-sdk-reference.md) | SDK usage and API reference |
+
+### 🔧 Development
+
+| Document | Description |
+|----------|-------------|
+| [06-development.md](06-development.md) | Development roadmap, TODO, and contributing |
+
+### 📝 Internal Reference
+
+| Document | Description |
+|----------|-------------|
+| [prompt.md](prompt.md) | Harness prompt template for code generation (internal) |
+
+### 📄 Research & Blog
+
+| Document | Description |
+|----------|-------------|
+| [blog/Story.md](blog/Story.md) | Design philosophy: "Keep it simple and stupid" |
+| [blog/A way to auto scaling capabilities for Agent.md](blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md) | Research: RAG-based auto-scaling capabilities for Agents |
+| [blog/](blog/) | Supporting images and assets |
+
+---
+
+## Quick Navigation
+
+- **New to SCL?** Start with [01-overview.md](01-overview.md) → [04-getting-started.md](04-getting-started.md)
+- **Understanding the architecture?** See [02-architecture.md](02-architecture.md)
+- **Using the SDK?** See [05-sdk-reference.md](05-sdk-reference.md)
+- **Reviewing research?** See [blog/A way to auto scaling capabilities for Agent.md](blog/A%20way%20to%20auto%20scaling%20capabilities%20for%20Agent.md)
diff --git a/doc/blog/A way to auto scaling capabilities for Agent.md b/docs/blog/A way to auto scaling capabilities for Agent.md
similarity index 100%
rename from doc/blog/A way to auto scaling capabilities for Agent.md
rename to docs/blog/A way to auto scaling capabilities for Agent.md
diff --git a/doc/blog/Story.md b/docs/blog/Story.md
similarity index 100%
rename from doc/blog/Story.md
rename to docs/blog/Story.md
diff --git a/doc/blog/bm25_norm_comparison.png b/docs/blog/bm25_norm_comparison.png
similarity index 100%
rename from doc/blog/bm25_norm_comparison.png
rename to docs/blog/bm25_norm_comparison.png
diff --git a/doc/blog/bm25tanhzoom.png b/docs/blog/bm25tanhzoom.png
similarity index 100%
rename from doc/blog/bm25tanhzoom.png
rename to docs/blog/bm25tanhzoom.png
diff --git a/doc/blog/detailsOnToolE.png b/docs/blog/detailsOnToolE.png
similarity index 100%
rename from doc/blog/detailsOnToolE.png
rename to docs/blog/detailsOnToolE.png
diff --git a/doc/blog/embedding.png b/docs/blog/embedding.png
similarity index 100%
rename from doc/blog/embedding.png
rename to docs/blog/embedding.png
diff --git a/main.py b/main.py
index 8f47c8a..ba881fe 100644
--- a/main.py
+++ b/main.py
@@ -6,25 +6,27 @@
import time
from scl.config import config
-from scl.queue.taskQueue import TaskQueue
-from scl.queue.capTaskQueues import CapabilityTaskQueues
-from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
-from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
-from scl.listener.restful_watch import RestFulHandler
from scl.listener.file_watch import FileHandler
+from scl.listener.restful_watch import RestFulHandler
from scl.processor.task_processor import TaskProcessor
+from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
+from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
+from scl.queue.cap_task_queues import CapabilityTaskQueues
+from scl.queue.task_queue import TaskQueue
# Setup telemetry
logger = logging.getLogger(__name__)
from scl.otel.otel import init_telemetry
+
init_telemetry()
+
def main():
todo_queue = TaskQueue()
captask_queue = CapabilityTaskQueues()
waiting_approval_queue = AwaitingApproveQueue()
waiting_captask_queue = AwaitingCapTasksQueue()
-
+
logger.info("Starting Todo Receiver Application")
# 从 config 读取监听目录
@@ -41,13 +43,9 @@ def main():
task_queue=todo_queue,
captask_queue=captask_queue,
waiting_approval_queue=waiting_approval_queue,
- waiting_captask_queue=waiting_captask_queue
- )
- rest_handler = RestFulHandler(
- watch_dir,
- host=config.api_host,
- port=config.api_port
+ waiting_captask_queue=waiting_captask_queue,
)
+ rest_handler = RestFulHandler(watch_dir, host=config.api_host, port=config.api_port)
file_observer = file_handler.start()
api_thread = threading.Thread(target=rest_handler.start, daemon=True)
@@ -68,5 +66,6 @@ def shutdown(signum, frame):
except KeyboardInterrupt:
shutdown(None, None)
+
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..0179f37
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,95 @@
+[build-system]
+requires = ["setuptools>=64", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "structured-context-language"
+description = "Structured Context Language (SCL) — a standardized agent loop runtime for context engineering."
+readme = "README.md"
+requires-python = ">=3.11"
+license = { text = "Apache-2.0" }
+authors = [{ name = "Teingi" }]
+keywords = ["agent", "llm", "context-engineering", "scl", "rag"]
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "License :: OSI Approved :: Apache Software License",
+ "Operating System :: OS Independent",
+]
+dynamic = ["version"]
+
+dependencies = [
+ "fastapi>=0.135.3",
+ "uvicorn>=0.44.0",
+ "watchdog>=6.0.0",
+ "openai>=2.14.0",
+ "numpy>=2.3.5",
+ "strictyaml>=1.7.3",
+ "PyYAML>=6.0.3",
+ "rank-bm25>=0.2.2",
+ "opentelemetry-api>=1.39.1",
+ "opentelemetry-sdk>=1.39.1",
+ "opentelemetry-exporter-otlp>=1.39.1",
+ "opentelemetry-instrumentation-logging>=0.60b1",
+]
+
+[project.optional-dependencies]
+# OceanBase vector store backend (scl/storage/oceanbasestore.py)
+oceanbase = ["pyobvector", "SQLAlchemy"]
+# PostgreSQL + pgvector store backend (scl/storage/pgstore.py)
+postgres = ["psycopg2-binary>=2.9.11", "pgvector>=0.4.2"]
+# Local embedding model backend (scl/embeddings/local_embedding.py)
+local = ["sentence-transformers"]
+dev = ["pytest", "pytest-asyncio", "pytest-cov", "ruff", "mypy"]
+
+[project.urls]
+Homepage = "https://github.com/Teingi/StructuredContextLanguage"
+Repository = "https://github.com/Teingi/StructuredContextLanguage"
+
+[project.scripts]
+scl = "main:main"
+
+[tool.setuptools]
+py-modules = ["main"]
+
+[tool.setuptools.packages.find]
+include = ["scl*"]
+exclude = ["scl.storage.skills*"]
+
+[tool.setuptools.dynamic]
+version = { attr = "scl.__version__" }
+
+[tool.ruff]
+line-length = 100
+target-version = "py311"
+extend-exclude = ["scl/storage/skills"]
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "UP", "B"]
+# E501 (line length) is owned by the formatter; long strings/comments are left as-is.
+ignore = ["E501"]
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F401"]
+# E402: telemetry must be initialized before other imports.
+"main.py" = ["E402"]
+# E402 in these modules is from imports placed after optional-dependency guards.
+"scl/cap_reg.py" = ["E402"]
+"scl/storage/oceanbasestore.py" = ["E402"]
+"scl/storage/pgstore.py" = ["E402"]
+"tests/*" = ["E402", "F401", "F841", "B"]
+
+[tool.mypy]
+python_version = "3.11"
+ignore_missing_imports = true
+exclude = ["scl/storage/skills/"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+addopts = "--cov=scl --cov-report=term-missing"
+
+[tool.coverage.run]
+source = ["scl"]
+omit = ["tests/*", "scl/storage/skills/*"]
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 605befd..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,69 +0,0 @@
-annotated-doc==0.0.4
-annotated-types==0.7.0
-anyio==4.12.0
-certifi==2025.11.12
-charset-normalizer==3.4.4
-click==8.3.2
-distro==1.9.0
-dotenv==0.9.9
-fastapi==0.135.3
-googleapis-common-protos==1.72.0
-grpcio==1.76.0
-h11==0.16.0
-httpcore==1.0.9
-httpx==0.28.1
-idna==3.11
-importlib_metadata==8.7.0
-jiter==0.12.0
-jsonpatch==1.33
-jsonpointer==3.0.0
-langchain-core==0.3.81
-langchain_sandbox==0.0.6
-langgraph==1.0.1
-langgraph-checkpoint==3.0.1
-langgraph-prebuilt==1.0.1
-langgraph-sdk==0.2.15
-langsmith==0.5.2
-numpy==2.3.5
-openai==2.14.0
-opentelemetry-api==1.39.1
-opentelemetry-exporter-otlp==1.39.1
-opentelemetry-exporter-otlp-proto-common==1.39.1
-opentelemetry-exporter-otlp-proto-grpc==1.39.1
-opentelemetry-exporter-otlp-proto-http==1.39.1
-opentelemetry-exporter-prometheus==0.60b1
-opentelemetry-instrumentation==0.60b1
-opentelemetry-instrumentation-logging==0.60b1
-opentelemetry-proto==1.39.1
-opentelemetry-sdk==1.39.1
-opentelemetry-semantic-conventions==0.60b1
-orjson==3.11.5
-ormsgpack==1.12.1
-packaging==25.0
-pgvector==0.4.2
-prometheus_client==0.25.0
-protobuf==6.33.2
-psycopg2-binary==2.9.11
-pydantic==2.12.5
-pydantic_core==2.41.5
-python-dateutil==2.9.0.post0
-python-dotenv==1.2.2
-PyYAML==6.0.3
-requests==2.32.5
-requests-toolbelt==1.0.0
-six==1.17.0
-sniffio==1.3.1
-starlette==1.0.0
-strictyaml==1.7.3
-tenacity==9.1.2
-tqdm==4.67.1
-typing-inspection==0.4.2
-typing_extensions==4.15.0
-urllib3==2.6.2
-uuid_utils==0.12.0
-uvicorn==0.44.0
-watchdog==6.0.0
-wrapt==1.17.3
-xxhash==3.6.0
-zipp==3.23.0
-zstandard==0.25.0
\ No newline at end of file
diff --git a/scl/__init__.py b/scl/__init__.py
index 6fabfab..daa6dc9 100644
--- a/scl/__init__.py
+++ b/scl/__init__.py
@@ -5,11 +5,4 @@
__version__ = "0.1.0"
# Import main modules for convenience
-from . import meta
-from . import processor
-from . import queue
-from . import storage
-from . import listener
-from . import otel
-from . import embeddings
-from . import capabilities
+from . import capabilities, embeddings, listener, meta, otel, processor, queue, storage
diff --git a/scl/cap_reg.py b/scl/cap_reg.py
index d377775..dcbdc80 100644
--- a/scl/cap_reg.py
+++ b/scl/cap_reg.py
@@ -30,21 +30,21 @@
- Automatic tool schema generation from docstrings.
"""
-import sys
-import os
import logging
-from typing import List, Dict
+import os
+import sys
+
+from opentelemetry import trace
+
from scl.meta.capability import Capability
from scl.otel.metric_decorator import record_latency
-from scl.otel.otel import search_time_histogram, tool_execute_time_histogram
-from scl.otel.otel import tracer, meter
-from opentelemetry import trace
+from scl.otel.otel import meter, search_time_histogram, tracer
# Add the StructuredContextLanguage directory to the path
scl_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(scl_root)
-from scl.storage.base import StoreBase
from scl.meta.msg import Msg
+from scl.storage.base import StoreBase
class CapRegistry:
@@ -76,13 +76,12 @@ def __init__(self, StoreBase: StoreBase):
self.logger = logging.getLogger(__name__)
# Create a counter to track capability retrievals
self.cap_fetch_counter = meter.create_counter(
- "capability.fetches",
- description="Number of capability retrievals by method type"
+ "capability.fetches", description="Number of capability retrievals by method type"
)
# ---------------------------------------------------------
@tracer.start_as_current_span("getCapsByNames")
- def getCapsByNames(self, ToolNames: List[str]) -> Dict[str, Capability]:
+ def getCapsByNames(self, ToolNames: list[str]) -> dict[str, Capability]:
"""
Retrieve multiple capabilities by their exact names.
@@ -155,7 +154,9 @@ def get_cap_by_name(self, name: str) -> Capability:
@tracer.start_as_current_span("getCapsBySimilarity")
@record_latency(search_time_histogram, "search")
- def getCapsBySimilarity(self, msg: Msg, limit: int = 5, min_similarity: float = 0.5) -> Dict[str, Capability]:
+ def getCapsBySimilarity(
+ self, msg: Msg, limit: int = 5, min_similarity: float = 0.5
+ ) -> dict[str, Capability]:
"""
Perform a semantic (RAG-like) search for capabilities based on message context.
@@ -187,8 +188,10 @@ def getCapsBySimilarity(self, msg: Msg, limit: int = 5, min_similarity: float =
current_span.set_attribute("limit", limit)
current_span.set_attribute("min_similarity", min_similarity)
# Avoid logging large messages; record first 100 chars
- if msg and hasattr(msg, 'content'):
- snippet = str(msg.content)[:100] + "..." if len(str(msg.content)) > 100 else str(msg.content)
+ if msg and hasattr(msg, "content"):
+ snippet = (
+ str(msg.content)[:100] + "..." if len(str(msg.content)) > 100 else str(msg.content)
+ )
current_span.set_attribute("message_content_preview", snippet)
self.logger.debug(f"Semantic search with limit={limit}, min_similarity={min_similarity}")
@@ -222,8 +225,10 @@ def record(self, msg: Msg, cap: Capability):
"""
current_span = trace.get_current_span()
if cap:
- current_span.set_attribute("capability_name", cap.name if hasattr(cap, 'name') else "unknown")
- if msg and hasattr(msg, 'id'):
+ current_span.set_attribute(
+ "capability_name", cap.name if hasattr(cap, "name") else "unknown"
+ )
+ if msg and hasattr(msg, "id"):
current_span.set_attribute("message_id", str(msg.id))
self.logger.debug(f"Recording usage of capability '{cap.name if cap else 'None'}'")
@@ -233,7 +238,9 @@ def record(self, msg: Msg, cap: Capability):
@tracer.start_as_current_span("getCapsByHistory")
@record_latency(search_time_histogram, "search")
- def getCapsByHistory(self, msg: Msg, limit: int = 5, min_similarity: float = 0.5) -> Dict[str, Capability]:
+ def getCapsByHistory(
+ self, msg: Msg, limit: int = 5, min_similarity: float = 0.5
+ ) -> dict[str, Capability]:
"""
Retrieve capabilities that are historically associated with similar messages.
@@ -260,8 +267,10 @@ def getCapsByHistory(self, msg: Msg, limit: int = 5, min_similarity: float = 0.5
current_span.set_attribute("limit", limit)
current_span.set_attribute("min_similarity", min_similarity)
- self.logger.debug(f"History-based search with limit={limit}, min_similarity={min_similarity}")
+ self.logger.debug(
+ f"History-based search with limit={limit}, min_similarity={min_similarity}"
+ )
results = self.cap_store.getCapsByHistory(msg, limit, min_similarity)
self.cap_fetch_counter.add(len(results), {"method": "history"})
self.logger.info(f"History-based search returned {len(results)} capabilities.")
- return results
\ No newline at end of file
+ return results
diff --git a/scl/capabilities/bash.py b/scl/capabilities/bash.py
index eca7671..7f8f451 100644
--- a/scl/capabilities/bash.py
+++ b/scl/capabilities/bash.py
@@ -15,20 +15,21 @@
- OpenTelemetry integrated for tracing, metrics, and structured logging.
- Logger provides info and debug levels.
"""
+
import logging
import os
import subprocess
-from typing import Dict, Any, Optional, List
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
from scl.meta.capability import Capability
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
bash_execution_counter = meter.create_counter(
- "bash_command.executed",
- description="Number of times a bash command was executed"
+ "bash_command.executed", description="Number of times a bash command was executed"
)
# Patterns that indicate dangerous commands (case‑insensitive)
@@ -38,7 +39,7 @@
"sudo",
"mkfs",
"dd if=",
- ":(){ :|:& };:", # fork bomb
+ ":(){ :|:& };:", # fork bomb
"chmod 777",
"wget",
"curl",
@@ -61,9 +62,9 @@ def __init__(
name: str,
description: str,
original_body: str,
- llm_description: Optional[str] = None,
- function_impl: Optional[str] = None,
- allowed_directories: Optional[List[str]] = None,
+ llm_description: str | None = None,
+ function_impl: str | None = None,
+ allowed_directories: list[str] | None = None,
):
current_span = trace.get_current_span()
current_span.set_attribute("bash_function_call.name", name)
@@ -94,7 +95,7 @@ def __init__(
logger.info("BashFunctionCall '%s' created", name)
@staticmethod
- def _is_dangerous(command: str) -> Optional[str]:
+ def _is_dangerous(command: str) -> str | None:
"""Check if the command contains a dangerous pattern."""
command_lower = command.lower()
for pattern in DANGEROUS_PATTERNS:
@@ -103,7 +104,7 @@ def _is_dangerous(command: str) -> Optional[str]:
return None
@tracer.start_as_current_span("BashFunctionCall.execute")
- def execute(self, args_dict: Dict[str, Any]) -> str:
+ def execute(self, args_dict: dict[str, Any]) -> str:
"""
Format the command with `args_dict`, perform safety checks, and run it.
@@ -205,10 +206,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
return stdout
def __repr__(self) -> str:
- return (
- f"BashFunctionCall(name='{self.name}', "
- f"allowed_dirs={self.allowed_directories})"
- )
+ return f"BashFunctionCall(name='{self.name}', allowed_dirs={self.allowed_directories})"
"""
@@ -245,4 +243,4 @@ def __repr__(self) -> str:
dangerous.execute({})
except ValueError as e:
print(e) # will print the safety violation message
-"""
\ No newline at end of file
+"""
diff --git a/scl/capabilities/fileread.py b/scl/capabilities/fileread.py
index c1fc1a3..b56011a 100644
--- a/scl/capabilities/fileread.py
+++ b/scl/capabilities/fileread.py
@@ -20,28 +20,66 @@
import logging
import os
-from typing import Optional, Dict, Any, List
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
from scl.meta.capability import Capability
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Metric counting file read executions
file_read_counter = meter.create_counter(
- "file_read.executed",
- description="Number of times a file was read (successful or attempted)"
+ "file_read.executed", description="Number of times a file was read (successful or attempted)"
)
# Known binary file extensions – these will be refused
_BINARY_EXTENSIONS = {
- ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp",
- ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
- ".exe", ".dll", ".so", ".dylib", ".bin", ".zip", ".gz",
- ".tar", ".7z", ".rar", ".mp3", ".mp4", ".avi", ".mov", ".mkv",
- ".iso", ".psd", ".ai", ".sketch", ".vsd", ".odt", ".ods", ".odp",
- ".ttf", ".otf", ".woff", ".woff2", ".class", ".pyc", ".pyo"
+ ".jpg",
+ ".jpeg",
+ ".png",
+ ".gif",
+ ".bmp",
+ ".tiff",
+ ".webp",
+ ".pdf",
+ ".doc",
+ ".docx",
+ ".xls",
+ ".xlsx",
+ ".ppt",
+ ".pptx",
+ ".exe",
+ ".dll",
+ ".so",
+ ".dylib",
+ ".bin",
+ ".zip",
+ ".gz",
+ ".tar",
+ ".7z",
+ ".rar",
+ ".mp3",
+ ".mp4",
+ ".avi",
+ ".mov",
+ ".mkv",
+ ".iso",
+ ".psd",
+ ".ai",
+ ".sketch",
+ ".vsd",
+ ".odt",
+ ".ods",
+ ".odp",
+ ".ttf",
+ ".otf",
+ ".woff",
+ ".woff2",
+ ".class",
+ ".pyc",
+ ".pyo",
}
@@ -58,12 +96,14 @@ class FileRead(Capability):
"""
@tracer.start_as_current_span("FileRead.__init__")
- def __init__(self,
- name: str,
- description: str,
- original_body: str,
- llm_description: Optional[str] = None,
- allowed_directories: Optional[List[str]] = None):
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ original_body: str,
+ llm_description: str | None = None,
+ allowed_directories: list[str] | None = None,
+ ):
current_span = trace.get_current_span()
current_span.set_attribute("file_read.name", name)
@@ -81,13 +121,13 @@ def __init__(self,
description=description,
original_body=original_body,
llm_description=llm_description,
- function_impl=None # Concrete tool – no dynamic code
+ function_impl=None, # Concrete tool – no dynamic code
)
logger.info(f"FileRead capability '{name}' created")
@tracer.start_as_current_span("FileRead.execute")
- def execute(self, args_dict: Dict[str, Any]) -> str:
+ def execute(self, args_dict: dict[str, Any]) -> str:
"""
Execute the file read operation.
@@ -168,7 +208,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
# Attempt to read the content
try:
- with open(target_path, "r", encoding="utf-8") as f:
+ with open(target_path, encoding="utf-8") as f:
content = f.read()
except UnicodeDecodeError as ude:
error_msg = f"File could not be decoded as UTF-8: {target_path}"
@@ -185,12 +225,13 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
# Success – update observability
file_read_counter.add(1, {"file_read.name": self.name, "outcome": "success"})
current_span.set_attribute("file_read.content_length", len(content))
- logger.info(f"FileRead '{self.name}' successfully read {len(content)} bytes from {target_path}")
+ logger.info(
+ f"FileRead '{self.name}' successfully read {len(content)} bytes from {target_path}"
+ )
return content
def __repr__(self) -> str:
- return (f"FileRead(name='{self.name}', "
- f"allowed_dirs={self._allowed_dirs})")
+ return f"FileRead(name='{self.name}', allowed_dirs={self._allowed_dirs})"
"""
@@ -223,4 +264,4 @@ def __repr__(self) -> str:
# This will raise a ValueError because the extension is binary:
# reader.execute({"path": "photo.png"})
-"""
\ No newline at end of file
+"""
diff --git a/scl/capabilities/filewrite.py b/scl/capabilities/filewrite.py
index e8936b3..6a6b5a5 100644
--- a/scl/capabilities/filewrite.py
+++ b/scl/capabilities/filewrite.py
@@ -18,21 +18,21 @@
- OpenTelemetry integrated for tracing, metrics, and structured logging.
- Logger provides info and debug levels.
"""
+
import logging
import os
-from pathlib import Path
-from typing import Optional, Dict, Any, List
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
from scl.meta.capability import Capability
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Metric for file write operations
file_write_counter = meter.create_counter(
- "file_write.executed",
- description="Number of file write executions"
+ "file_write.executed", description="Number of file write executions"
)
@@ -52,8 +52,8 @@ def __init__(
name: str,
description: str,
original_body: str,
- llm_description: Optional[str] = None,
- allowed_dirs: Optional[List[str]] = None,
+ llm_description: str | None = None,
+ allowed_dirs: list[str] | None = None,
):
current_span = trace.get_current_span()
current_span.set_attribute("file_write.name", name)
@@ -75,13 +75,11 @@ def __init__(
else:
self.allowed_dirs = [os.path.abspath(os.getcwd())]
- logger.debug(
- f"FileWrite '{name}' allowed directories: {self.allowed_dirs}"
- )
+ logger.debug(f"FileWrite '{name}' allowed directories: {self.allowed_dirs}")
logger.info(f"FileWrite '{name}' created")
@tracer.start_as_current_span("FileWrite.execute")
- def execute(self, args_dict: Dict[str, Any]) -> str:
+ def execute(self, args_dict: dict[str, Any]) -> str:
"""
Execute the file write operation.
@@ -141,10 +139,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
# Different drives on Windows, etc.
continue
if not allowed:
- error_msg = (
- f"Path '{target_path}' is outside allowed directories: "
- f"{self.allowed_dirs}"
- )
+ error_msg = f"Path '{target_path}' is outside allowed directories: {self.allowed_dirs}"
logger.error(error_msg)
current_span.set_status(trace.Status(trace.StatusCode.ERROR, error_msg))
raise PermissionError(error_msg)
@@ -170,8 +165,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
current_span.set_attribute("file_write.bytes_written", len(content))
logger.info(
- f"File written successfully: {target_path} "
- f"(mode={mode}, bytes={len(content)})"
+ f"File written successfully: {target_path} (mode={mode}, bytes={len(content)})"
)
# Return the written content as per design goal
@@ -185,10 +179,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
raise OSError(error_msg) from e
def __repr__(self) -> str:
- return (
- f"FileWrite(name='{self.name}', "
- f"allowed_dirs={self.allowed_dirs})"
- )
+ return f"FileWrite(name='{self.name}', allowed_dirs={self.allowed_dirs})"
"""
@@ -224,4 +215,4 @@ def __repr__(self) -> str:
writer.execute({"path": "/etc/passwd", "content": "malicious"})
except PermissionError as e:
print(f"Blocked: {e}")
-"""
\ No newline at end of file
+"""
diff --git a/scl/capabilities/git.py b/scl/capabilities/git.py
index 39e7e85..1e54b48 100644
--- a/scl/capabilities/git.py
+++ b/scl/capabilities/git.py
@@ -25,18 +25,18 @@
import logging
import subprocess
-from typing import Dict, Any, List, Optional
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
from scl.meta.capability import Capability
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Metric: number of git operations by action type
git_operation_counter = meter.create_counter(
- "git.operation",
- description="Number of git operations performed, tagged by action type"
+ "git.operation", description="Number of git operations performed, tagged by action type"
)
@@ -51,11 +51,7 @@ class GitCapability(Capability):
@tracer.start_as_current_span("GitCapability.__init__")
def __init__(
- self,
- name: str,
- description: str,
- original_body: str,
- llm_description: Optional[str] = None
+ self, name: str, description: str, original_body: str, llm_description: str | None = None
):
current_span = trace.get_current_span()
current_span.set_attribute("git_capability.name", name)
@@ -66,14 +62,14 @@ def __init__(
description=description,
original_body=original_body,
llm_description=llm_description,
- function_impl=None # Git operations are built-in, no external code
+ function_impl=None, # Git operations are built-in, no external code
)
logger.debug(f"GitCapability '{name}' initialized")
logger.info(f"GitCapability '{name}' created")
@tracer.start_as_current_span("GitCapability.execute")
- def execute(self, args_dict: Dict[str, Any]) -> Any:
+ def execute(self, args_dict: dict[str, Any]) -> Any:
"""
Execute a Git operation based on the provided arguments.
@@ -93,7 +89,7 @@ def execute(self, args_dict: Dict[str, Any]) -> Any:
RuntimeError: if git command fails or current directory is not a repo.
"""
current_span = trace.get_current_span()
- action = args_dict.get('action')
+ action = args_dict.get("action")
current_span.set_attribute("git.action", action)
if not action:
@@ -104,8 +100,8 @@ def execute(self, args_dict: Dict[str, Any]) -> Any:
logger.debug(f"Executing git action '{action}' with args: {args_dict}")
- if action == 'commit':
- message = args_dict.get('message')
+ if action == "commit":
+ message = args_dict.get("message")
if not message:
error_msg = "Commit requires a 'message' in args_dict."
logger.error(error_msg)
@@ -113,8 +109,8 @@ def execute(self, args_dict: Dict[str, Any]) -> Any:
raise ValueError(error_msg)
result = self._commit(message)
current_span.set_attribute("git.commit.message", message)
- elif action == 'checkout':
- commit_hash = args_dict.get('commit_hash')
+ elif action == "checkout":
+ commit_hash = args_dict.get("commit_hash")
if not commit_hash:
error_msg = "Checkout requires a 'commit_hash' in args_dict."
logger.error(error_msg)
@@ -122,7 +118,7 @@ def execute(self, args_dict: Dict[str, Any]) -> Any:
raise ValueError(error_msg)
result = self._checkout(commit_hash)
current_span.set_attribute("git.checkout.hash", commit_hash)
- elif action == 'history':
+ elif action == "history":
result = self._history()
else:
error_msg = f"Unsupported git action '{action}'"
@@ -141,16 +137,14 @@ def _commit(self, message: str) -> str:
try:
self._verify_git_repo()
# Stage all changes
- subprocess.run(['git', 'add', '.'], check=True, capture_output=True, text=True)
+ subprocess.run(["git", "add", "."], check=True, capture_output=True, text=True)
# Commit
subprocess.run(
- ['git', 'commit', '-m', message],
- check=True, capture_output=True, text=True
+ ["git", "commit", "-m", message], check=True, capture_output=True, text=True
)
# Retrieve the new commit hash
hash_result = subprocess.run(
- ['git', 'rev-parse', 'HEAD'],
- check=True, capture_output=True, text=True
+ ["git", "rev-parse", "HEAD"], check=True, capture_output=True, text=True
)
commit_hash = hash_result.stdout.strip()
logger.info(f"Committed with hash {commit_hash}")
@@ -166,8 +160,7 @@ def _checkout(self, commit_hash: str) -> str:
try:
self._verify_git_repo()
subprocess.run(
- ['git', 'checkout', commit_hash],
- check=True, capture_output=True, text=True
+ ["git", "checkout", commit_hash], check=True, capture_output=True, text=True
)
logger.info(f"Checked out commit {commit_hash}")
return commit_hash
@@ -176,7 +169,7 @@ def _checkout(self, commit_hash: str) -> str:
logger.error(error_msg)
raise RuntimeError(error_msg) from e
- def _history(self) -> List[Dict[str, str]]:
+ def _history(self) -> list[dict[str, str]]:
"""
Return list of commits for the current branch.
@@ -190,21 +183,25 @@ def _history(self) -> List[Dict[str, str]]:
try:
self._verify_git_repo()
result = subprocess.run(
- ['git', 'log', '--pretty=format:%H%x09%an%x09%ad%x09%s', '--date=short'],
- check=True, capture_output=True, text=True
+ ["git", "log", "--pretty=format:%H%x09%an%x09%ad%x09%s", "--date=short"],
+ check=True,
+ capture_output=True,
+ text=True,
)
- lines = result.stdout.strip().split('\n')
+ lines = result.stdout.strip().split("\n")
history = []
for line in lines:
if line:
- parts = line.split('\t')
+ parts = line.split("\t")
if len(parts) >= 4:
- history.append({
- "commit_hash": parts[0],
- "author": parts[1],
- "date": parts[2],
- "message": parts[3]
- })
+ history.append(
+ {
+ "commit_hash": parts[0],
+ "author": parts[1],
+ "date": parts[2],
+ "message": parts[3],
+ }
+ )
logger.info(f"Retrieved {len(history)} commits from history")
return history
except subprocess.CalledProcessError as e:
@@ -216,11 +213,13 @@ def _verify_git_repo(self) -> None:
"""Ensure the current working directory is inside a Git repository."""
try:
subprocess.run(
- ['git', 'rev-parse', '--is-inside-work-tree'],
- check=True, capture_output=True, text=True
+ ["git", "rev-parse", "--is-inside-work-tree"],
+ check=True,
+ capture_output=True,
+ text=True,
)
- except subprocess.CalledProcessError:
- raise RuntimeError("Current directory is not a Git repository")
+ except subprocess.CalledProcessError as e:
+ raise RuntimeError("Current directory is not a Git repository") from e
"""
@@ -246,4 +245,4 @@ def _verify_git_repo(self) -> None:
# Checkout a specific earlier commit
git_cap.execute({"action": "checkout", "commit_hash": "abc123"})
-"""
\ No newline at end of file
+"""
diff --git a/scl/capabilities/grep.py b/scl/capabilities/grep.py
index 65803e7..b610b92 100644
--- a/scl/capabilities/grep.py
+++ b/scl/capabilities/grep.py
@@ -26,23 +26,24 @@
OpenTelemetry: uses tracer, meter and structured logging for full observability.
"""
+
import logging
import os
import re
import subprocess
from itertools import product
-from typing import Optional, Dict, Any, List, Union
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
from scl.meta.capability import Capability
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Meter for grep executions
grep_execution_counter = meter.create_counter(
- "grep_function_call.executed",
- description="Number of times a grep function call was executed"
+ "grep_function_call.executed", description="Number of times a grep function call was executed"
)
@@ -55,20 +56,42 @@ class GrepFunctionCall(Capability):
# Flags that are known to work with igrep (derived from `igrep --help`)
_IGREP_SUPPORTED_OPTIONS = {
- "-i", "--ignore-case", "-S", "--smart-case",
- "-.", "--hidden", "-L", "--follow", "-w", "--word-regexp",
- "-g", "--glob", "-t", "--type", "-T", "--type-not",
- "--editor", "--custom-command", "--theme", "--context-viewer",
- "--type-list", "-h", "--help", "-V", "--version",
+ "-i",
+ "--ignore-case",
+ "-S",
+ "--smart-case",
+ "-.",
+ "--hidden",
+ "-L",
+ "--follow",
+ "-w",
+ "--word-regexp",
+ "-g",
+ "--glob",
+ "-t",
+ "--type",
+ "-T",
+ "--type-not",
+ "--editor",
+ "--custom-command",
+ "--theme",
+ "--context-viewer",
+ "--type-list",
+ "-h",
+ "--help",
+ "-V",
+ "--version",
}
@tracer.start_as_current_span("GrepFunctionCall.__init__")
- def __init__(self,
- name: str,
- description: str,
- original_body: str,
- llm_description: Optional[str] = None,
- search_params: Optional[Dict] = None):
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ original_body: str,
+ llm_description: str | None = None,
+ search_params: dict | None = None,
+ ):
current_span = trace.get_current_span()
current_span.set_attribute("grep.name", name)
@@ -77,7 +100,7 @@ def __init__(self,
type="grep_function_call",
description=description,
original_body=original_body,
- llm_description=llm_description
+ llm_description=llm_description,
)
# Default search parameters (used when not overridden in execute)
@@ -86,7 +109,7 @@ def __init__(self,
logger.info(f"GrepFunctionCall '{name}' created")
@tracer.start_as_current_span("GrepFunctionCall.execute")
- def execute(self, args_dict: Dict[str, Any]) -> str:
+ def execute(self, args_dict: dict[str, Any]) -> str:
"""
Execute the grep search with the provided arguments.
@@ -117,7 +140,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
try:
cmd = self._build_command(merged_args)
logger.info(f"Executing grep command: {' '.join(cmd)}")
- current_span.set_attribute("grep.command", ' '.join(cmd))
+ current_span.set_attribute("grep.command", " ".join(cmd))
result = self._run_command(cmd)
@@ -143,7 +166,7 @@ def execute(self, args_dict: Dict[str, Any]) -> str:
current_span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
raise
- def _build_command(self, args_dict: Dict[str, Any]) -> List[str]:
+ def _build_command(self, args_dict: dict[str, Any]) -> list[str]:
"""
Build the grep command using the appropriate binary (igrep or grep),
translating options as needed for compatibility.
@@ -241,7 +264,7 @@ def _get_grep_binary(self) -> str:
"Please install igrep or ensure grep is in your PATH."
)
- def _parse_glob(self, glob_pattern: str) -> List[str]:
+ def _parse_glob(self, glob_pattern: str) -> list[str]:
"""
Parse glob patterns. Supports comma/space separation and brace expansion.
@@ -254,18 +277,18 @@ def _parse_glob(self, glob_pattern: str) -> List[str]:
depth = 0
simplified = []
for ch in glob_pattern:
- if ch == '{':
+ if ch == "{":
depth += 1
simplified.append(ch)
- elif ch == '}':
+ elif ch == "}":
depth -= 1
simplified.append(ch)
- elif ch == ',' and depth == 0:
- simplified.append(' ') # treat as whitespace separator
+ elif ch == "," and depth == 0:
+ simplified.append(" ") # treat as whitespace separator
else:
simplified.append(ch)
# Step 2: split by whitespace to obtain raw tokens
- raw_tokens = re.split(r'\s+', ''.join(simplified).strip())
+ raw_tokens = re.split(r"\s+", "".join(simplified).strip())
# Step 3: expand braces in each token
expanded = []
for token in raw_tokens:
@@ -275,7 +298,7 @@ def _parse_glob(self, glob_pattern: str) -> List[str]:
return expanded if expanded else [glob_pattern]
@staticmethod
- def _expand_braces(text: str) -> List[str]:
+ def _expand_braces(text: str) -> list[str]:
"""
Expand brace groups like "{a,b}" into a list of strings.
@@ -283,24 +306,24 @@ def _expand_braces(text: str) -> List[str]:
No nesting of braces is supported.
"""
# Find all brace groups
- brace_re = re.compile(r'\{([^{}]*)\}')
+ brace_re = re.compile(r"\{([^{}]*)\}")
matches = list(brace_re.finditer(text))
if not matches:
return [text]
# Extract the comma-separated options for each group
- option_lists = [m.group(1).split(',') for m in matches]
+ option_lists = [m.group(1).split(",") for m in matches]
results = []
for combo in product(*option_lists):
# Reconstruct the string by replacing each brace group with the chosen option
last_idx = 0
parts = []
- for match, opt in zip(matches, combo):
+ for match, opt in zip(matches, combo, strict=False):
start, end = match.span()
parts.append(text[last_idx:start])
parts.append(opt)
last_idx = end
parts.append(text[last_idx:])
- results.append(''.join(parts))
+ results.append("".join(parts))
return results
@staticmethod
@@ -311,7 +334,7 @@ def _is_binary_available(name: str) -> bool:
except FileNotFoundError:
return False
- def _run_command(self, cmd: List[str]) -> str:
+ def _run_command(self, cmd: list[str]) -> str:
"""
Execute the grep command and return the output.
"""
@@ -320,7 +343,7 @@ def _run_command(self, cmd: List[str]) -> str:
cmd,
capture_output=True,
text=True,
- check=False # grep returns 1 if no matches found
+ check=False, # grep returns 1 if no matches found
)
if result.returncode == 0:
return result.stdout
@@ -336,7 +359,9 @@ def _run_command(self, cmd: List[str]) -> str:
raise
def __repr__(self) -> str:
- return f"GrepFunctionCall(name='{self.name}', pattern='{self.search_params.get('pattern')}')"
+ return (
+ f"GrepFunctionCall(name='{self.name}', pattern='{self.search_params.get('pattern')}')"
+ )
"""
@@ -392,4 +417,4 @@ def __repr__(self) -> str:
"output_mode": "content"
})
print(result)
-"""
\ No newline at end of file
+"""
diff --git a/scl/config.py b/scl/config.py
index 23c9ac8..3d61fbc 100644
--- a/scl/config.py
+++ b/scl/config.py
@@ -1,15 +1,16 @@
import os
-from typing import Any, Optional
from dataclasses import dataclass
@dataclass
class Config:
"""使用dataclass的极简配置类"""
-
+
# otel related settings
otlp_endpoint: str = os.getenv("OTLP_ENDPOINT", "http://localhost:4318")
- otlp_metrics_endpoint: str = os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")
+ otlp_metrics_endpoint: str = os.getenv(
+ "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318"
+ )
log_level: str = os.getenv("LOG_LEVEL", "INFO")
service_name: str = os.getenv("SERVICE_NAME", "SCL")
@@ -22,23 +23,24 @@ class Config:
# embedding related settings
embedding_model: str = os.getenv("EMBEDDING_MODEL", "BAAI/bge-m3")
embedding_model_dims: int = int(os.getenv("EMBEDDING_MODEL_DIMS", "1024"))
- embedding_api_key: Optional[str] = os.getenv("EMBEDDING_API_KEY")
+ embedding_api_key: str | None = os.getenv("EMBEDDING_API_KEY")
embedding_base_url: str = os.getenv("EMBEDDING_BASE_URL", "https://api.siliconflow.cn/v1")
- embedding_local_model_path: Optional[str] = os.getenv("EMBEDDING_LOCAL_MODEL_PATH")
- embedding_cache_path: Optional[str] = os.getenv("EMBEDDING_CACHE_PATH")
-
+ embedding_local_model_path: str | None = os.getenv("EMBEDDING_LOCAL_MODEL_PATH")
+ embedding_cache_path: str | None = os.getenv("EMBEDDING_CACHE_PATH")
+
## todo, vars here may changes
limit: int = int(os.getenv("LIMIT", "5"))
min_similarity: float = float(os.getenv("MIN_SIMILARITY", "0.5"))
-
+
@property
def has_api_key(self) -> bool:
return bool(self.embedding_api_key)
-
+
def validate(self) -> bool:
"""简单的验证"""
- if not self.otlp_endpoint.startswith(('http://', 'https://')):
+ if not self.otlp_endpoint.startswith(("http://", "https://")):
raise ValueError(f"Invalid OTLP endpoint: {self.otlp_endpoint}")
return True
+
config = Config()
diff --git a/scl/embeddings/base_embedding.py b/scl/embeddings/base_embedding.py
index d950844..46adc0a 100644
--- a/scl/embeddings/base_embedding.py
+++ b/scl/embeddings/base_embedding.py
@@ -11,7 +11,7 @@
from abc import ABC, abstractmethod
from scl.otel.otel import meter
-from opentelemetry import trace
+
class BaseEmbeddingClient(ABC):
_instances = {}
@@ -31,9 +31,9 @@ def __init__(self):
self._initialized = True
self._init_subclass()
- def _init_subclass(self):
+ def _init_subclass(self): # noqa: B027 - optional hook for subclasses
pass
@abstractmethod
def embed(self, text, **kwargs):
- pass
\ No newline at end of file
+ pass
diff --git a/scl/embeddings/embedding.py b/scl/embeddings/embedding.py
index 56e40e0..86b8030 100644
--- a/scl/embeddings/embedding.py
+++ b/scl/embeddings/embedding.py
@@ -12,23 +12,26 @@
"""
import logging
-from functools import lru_cache
-from scl.embeddings.embedding_cache import EmbeddingCache
-from scl.embeddings.local_embedding import LocalEmbeddingClient
-from scl.embeddings.web_embedding import WebEmbeddingClient
-from scl.otel.otel import tracer, meter
+
from opentelemetry import trace
+# Optional backends — imported lazily in CompositeEmbedding.__init__
+# to avoid hard failures when optional dependencies are missing.
+from scl.otel.otel import meter, tracer
+
try:
from scl.config import config
except ImportError:
# Fallback – all backends disabled
class ConfigFallback:
pass
+
config = ConfigFallback()
+
class CompositeEmbedding:
"""Singleton coordinator that chooses the best backend for each request."""
+
_instance = None
def __new__(cls):
@@ -41,25 +44,33 @@ def __init__(self):
if self._initialized:
return
self.logger = logging.getLogger(__name__)
- self._embedding_cache_path = hasattr(config, 'embedding_cache_path')
+ self._embedding_cache_path = hasattr(config, "embedding_cache_path")
self.cache = None
- #if self._embedding_cache_path:
+ # if self._embedding_cache_path:
# self.cache = EmbeddingCache(cache_file=config.embedding_cache_path)
- # Backends are lazy‑loaded – only if config says they exist
- self._local_available = hasattr(config, 'embedding_local_model_path')
- self._web_available = False
- #hasattr(config, 'embedding_api_key') and hasattr(config, 'embedding_base_url')
+ # Backends are lazy‑loaded – only if config provides the required settings.
+ # We use getattr + bool check so a default-None attribute doesn't activate
+ # a backend that lacks a real configuration value.
+ self._local_available = bool(getattr(config, "embedding_local_model_path", None))
+ self._web_available = bool(
+ getattr(config, "embedding_api_key", None)
+ and getattr(config, "embedding_base_url", None)
+ )
if self._local_available:
+ from scl.embeddings.local_embedding import LocalEmbeddingClient
+
self.local_client = LocalEmbeddingClient()
self.logger.info("Local embedding backend enabled")
if self._web_available:
+ from scl.embeddings.web_embedding import WebEmbeddingClient
+
# openai.OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable
self.web_client = WebEmbeddingClient()
self.logger.info("Web embedding backend enabled")
self._counter = meter.create_counter(
"composite_embedding_requests",
- description="Total composite embedding requests (cache hits + computations)"
+ description="Total composite embedding requests (cache hits + computations)",
)
self._initialized = True
@@ -83,7 +94,7 @@ def embed(self, text):
try:
embedding = self.local_client.embed(text)
# Save single vector (already extracted) into cache
- #self.cache.set(text, embedding.tolist() if hasattr(embedding, 'tolist') else embedding)
+ # self.cache.set(text, embedding.tolist() if hasattr(embedding, 'tolist') else embedding)
self._counter.add(1, {"source": "local"})
return embedding
except Exception as e:
@@ -99,12 +110,15 @@ def embed(self, text):
raise RuntimeError("No embedding backend available – configure local model or web API.")
+
def get_embedding_client():
return CompositeEmbedding()
+
def embed(text):
return get_embedding_client().embed(text)
+
"""
Example usage:
from scl.embedding.embedding import embed
@@ -114,4 +128,4 @@ def embed(text):
# Subsequent calls with the same text hit the cache instantly
vector2 = embed("What is the capital of France?")
assert vector == vector2
-"""
\ No newline at end of file
+"""
diff --git a/scl/embeddings/embedding_cache.py b/scl/embeddings/embedding_cache.py
index 2198f00..e36fc85 100644
--- a/scl/embeddings/embedding_cache.py
+++ b/scl/embeddings/embedding_cache.py
@@ -11,10 +11,12 @@
"""
import json
-import os
import logging
+import os
+
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
+from scl.otel.otel import meter, tracer
class EmbeddingCache:
@@ -38,7 +40,7 @@ def _load(self):
"""Load cache from disk."""
if os.path.exists(self.cache_file):
try:
- with open(self.cache_file, 'r', encoding='utf-8') as f:
+ with open(self.cache_file, encoding="utf-8") as f:
self.cache = json.load(f)
self.logger.info("Embedding cache loaded (%d entries)", len(self.cache))
except Exception as e:
@@ -51,7 +53,7 @@ def _load(self):
def _save(self):
"""Persist cache to disk."""
try:
- with open(self.cache_file, 'w', encoding='utf-8') as f:
+ with open(self.cache_file, "w", encoding="utf-8") as f:
json.dump(self.cache, f, indent=2, ensure_ascii=False)
self.logger.debug("Cache saved (%d entries)", len(self.cache))
except Exception as e:
@@ -100,4 +102,4 @@ def __contains__(self, text):
cache.set("hello world", computed)
emb = computed
# use emb
-"""
\ No newline at end of file
+"""
diff --git a/scl/embeddings/local_embedding.py b/scl/embeddings/local_embedding.py
index 23b8b74..564b700 100644
--- a/scl/embeddings/local_embedding.py
+++ b/scl/embeddings/local_embedding.py
@@ -10,32 +10,31 @@
- OpenTelemetry tracing, metrics, and logging.
"""
-import logging
from functools import lru_cache
-from typing import List
-import numpy as np
-from sentence_transformers import SentenceTransformer
-
-from scl.otel.otel import tracer, meter
-from opentelemetry import trace
from scl.embeddings.base_embedding import BaseEmbeddingClient
+from scl.otel.otel import tracer
try:
from scl.config import config
except ImportError:
+
class ConfigFallback:
embedding_local_model_path = "/path/to/bge-m3"
+
config = ConfigFallback()
+
class LocalEmbeddingClient(BaseEmbeddingClient):
def _init_subclass(self):
- model_path = getattr(config, 'embedding_local_model_path', '/path/to/bge-m3')
+ # Imported lazily: the local backend is an optional extra (`pip install .[local]`).
+ from sentence_transformers import SentenceTransformer
+
+ model_path = getattr(config, "embedding_local_model_path", "/path/to/bge-m3")
self.logger.debug("Initializing LocalEmbeddingClient with model: %s", model_path)
self.model = SentenceTransformer(model_path)
self.request_counter = self._meter.create_counter(
- "local_embedding_requests",
- description="Total number of local embedding requests"
+ "local_embedding_requests", description="Total number of local embedding requests"
)
self.logger.info("LocalEmbeddingClient initialized")
@@ -46,10 +45,12 @@ def embed(self, sentence: str):
self.request_counter.add(1, {"status": "success"})
return embedding
+
# Convenience functions for direct use of local client
@lru_cache(maxsize=1)
def get_local_embedding_client():
return LocalEmbeddingClient()
+
def local_embed(text):
- return get_local_embedding_client().embed(text)
\ No newline at end of file
+ return get_local_embedding_client().embed(text)
diff --git a/scl/embeddings/web_embedding.py b/scl/embeddings/web_embedding.py
index 97f7125..24a6900 100644
--- a/scl/embeddings/web_embedding.py
+++ b/scl/embeddings/web_embedding.py
@@ -9,38 +9,44 @@
"""
import time
-import logging
+
from openai import OpenAI
-from scl.otel.otel import tracer, meter
+
from scl.embeddings.base_embedding import BaseEmbeddingClient
+from scl.otel.otel import tracer
try:
from scl.config import config
except ImportError:
+
class ConfigFallback:
embedding_model = "BAAI/bge-m3"
embedding_model_dims = 1024
embedding_api_key = "your-api-key"
embedding_base_url = "https://api.siliconflow.cn/v1"
+
config = ConfigFallback()
+
class WebEmbeddingClient(BaseEmbeddingClient):
"""Singleton web embedding client (OpenAI‑compatible)."""
def _init_subclass(self):
- self.model = getattr(config, 'embedding_model', 'BAAI/bge-m3')
- self.embedding_dims = getattr(config, 'embedding_model_dims', 1024)
- api_key = getattr(config, 'embedding_api_key', 'your-api-key')
- base_url = getattr(config, 'embedding_base_url', 'https://api.siliconflow.cn/v1')
+ self.model = getattr(config, "embedding_model", "BAAI/bge-m3")
+ self.embedding_dims = getattr(config, "embedding_model_dims", 1024)
+ api_key = getattr(config, "embedding_api_key", "your-api-key")
+ base_url = getattr(config, "embedding_base_url", "https://api.siliconflow.cn/v1")
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.supports_dimensions = "openai.com" in base_url.lower()
self.embed_counter = self._meter.create_counter(
"web_embedding_requests_total",
- description="Total number of web embedding API calls (not cache hits)"
+ description="Total number of web embedding API calls (not cache hits)",
+ )
+ self.logger.info(
+ "WebEmbeddingClient initialized (model=%s, base_url=%s)", self.model, base_url
)
- self.logger.info("WebEmbeddingClient initialized (model=%s, base_url=%s)", self.model, base_url)
@tracer.start_as_current_span("embed")
def embed(self, text):
@@ -57,4 +63,4 @@ def embed(self, text):
result = self.client.embeddings.create(**params).data[0].embedding
self.embed_counter.add(1, {"model": self.model})
self.logger.debug("Web embedding successful")
- return result
\ No newline at end of file
+ return result
diff --git a/scl/listener/file_watch.py b/scl/listener/file_watch.py
index 0494ce1..eb5d9b2 100644
--- a/scl/listener/file_watch.py
+++ b/scl/listener/file_watch.py
@@ -2,7 +2,7 @@
File Watcher for Todo Items
1. read files from a specific folder.
2. if the file is task is scl.meta.task format (either json or yaml), accept format file.
-2.1 it converts the task from file into a task instance
+2.1 it converts the task from file into a task instance
2.1.1 if the task instance got approval put into queue as a TaskQueue instance and move the file into processed folder.
2.1.2 if the task instance is not got approval put into waitingapproval folder and move the file into waitingapproval folder.
2.1.3 if the task instance has CapTask to completed put into waitingCapTask queue and move the file into waitingCapTask folder.
@@ -13,25 +13,25 @@
4. if the file is not supported format, move the file into failed folder.
"""
+
+import json
import logging
import os
import shutil
-import json
-import yaml
from pathlib import Path
-from typing import Optional
+import yaml
+from opentelemetry import trace
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
-from scl.queue.taskQueue import TaskQueue
-from scl.queue.capTaskQueues import CapabilityTaskQueues
-from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
-from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
-from scl.meta.task import Task
from scl.meta.captask import CapTask
-from scl.otel.otel import tracer, meter
-from opentelemetry import trace
+from scl.meta.task import Task
+from scl.otel.otel import meter, tracer
+from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
+from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
+from scl.queue.cap_task_queues import CapabilityTaskQueues
+from scl.queue.task_queue import TaskQueue
class FileHandler(FileSystemEventHandler):
@@ -46,7 +46,7 @@ def __init__(
task_queue: TaskQueue,
captask_queue: CapabilityTaskQueues,
waiting_approval_queue: AwaitingApproveQueue,
- waiting_captask_queue: AwaitingCapTasksQueue
+ waiting_captask_queue: AwaitingCapTasksQueue,
):
self.watch_path = watch_path
self.task_queue = task_queue
@@ -70,28 +70,24 @@ def __init__(
# Metrics
self.file_receive_counter = meter.create_counter(
- "file_receive",
- description="Total number of files detected"
+ "file_receive", description="Total number of files detected"
)
self.task_file_approved_counter = meter.create_counter(
- "task_file_approved",
- description="Number of Task files that were approved and queued"
+ "task_file_approved", description="Number of Task files that were approved and queued"
)
self.task_file_unapproved_counter = meter.create_counter(
"task_file_unapproved",
- description="Number of Task files that lacked approval and were moved to waiting"
+ description="Number of Task files that lacked approval and were moved to waiting",
)
self.task_file_pending_captasks_counter = meter.create_counter(
"task_file_pending_captasks",
- description="Number of Task files with incomplete CapTasks moved to waiting"
+ description="Number of Task files with incomplete CapTasks moved to waiting",
)
self.captask_file_valid_counter = meter.create_counter(
- "captask_file_valid",
- description="Number of files successfully processed as CapTask"
+ "captask_file_valid", description="Number of files successfully processed as CapTask"
)
self.file_invalid_counter = meter.create_counter(
- "file_invalid",
- description="Number of files that failed validation or conversion"
+ "file_invalid", description="Number of files that failed validation or conversion"
)
@tracer.start_as_current_span("file_watcher_on_created")
@@ -111,7 +107,9 @@ def on_created(self, event):
# Step 1: Validate file extension
if not self._is_supported_extension(filepath):
- self.logger.warning(f"File {filename} is not a supported format (JSON/YAML). Moving to failed.")
+ self.logger.warning(
+ f"File {filename} is not a supported format (JSON/YAML). Moving to failed."
+ )
self.file_invalid_counter.add(1)
self._move_to_failed(filepath, reason="unsupported_extension")
return
@@ -142,11 +140,11 @@ def on_created(self, event):
def _is_supported_extension(self, filepath: str) -> bool:
ext = Path(filepath).suffix.lower()
- return ext in ('.json', '.yaml', '.yml')
+ return ext in (".json", ".yaml", ".yml")
def _parse_file(self, filepath: str) -> dict:
- with open(filepath, 'r', encoding='utf-8') as f:
- if filepath.lower().endswith('.json'):
+ with open(filepath, encoding="utf-8") as f:
+ if filepath.lower().endswith(".json"):
return json.load(f)
else:
return yaml.safe_load(f)
@@ -181,7 +179,9 @@ def _process_as_task(self, filepath: str, filename: str, data: dict, span) -> bo
span.set_attribute("task.approval", True)
span.set_attribute("task.pending_captasks", True)
span.set_attribute("task.routed_to", "waiting_captasks")
- self.logger.info(f"Task {task_obj.hash} has pending CapTasks, moved to waitingCapTask")
+ self.logger.info(
+ f"Task {task_obj.hash} has pending CapTasks, moved to waitingCapTask"
+ )
else:
# Fully ready -> TaskQueue
self.task_queue.add(task_obj)
@@ -275,10 +275,10 @@ def start(self) -> Observer:
"""
Example usage:
- from scl.queue.taskQueue import TaskQueue
+ from scl.queue.task_queue import TaskQueue
from scl.queue.capabilityTaskQueues import CapabilityTaskQueues
- from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
- from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
+ from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
+ from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
from watchdog.observers import Observer
from file_handler import FileHandler # adjust import
@@ -305,4 +305,4 @@ def start(self) -> Observer:
except KeyboardInterrupt:
observer.stop()
observer.join()
-"""
\ No newline at end of file
+"""
diff --git a/scl/listener/Interal_watch.py b/scl/listener/internal_watch.py
similarity index 85%
rename from scl/listener/Interal_watch.py
rename to scl/listener/internal_watch.py
index 5b43025..43dc568 100644
--- a/scl/listener/Interal_watch.py
+++ b/scl/listener/internal_watch.py
@@ -25,17 +25,16 @@
# - Clear error messages and type validation.
# ---------------------------------------------------------------------------
+import json
import logging
import os
-import json
-from typing import Union
import yaml
from opentelemetry import trace
-from scl.meta.task import Task
from scl.meta.captask import CapTask
-from scl.otel.otel import tracer, meter
+from scl.meta.task import Task
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
@@ -65,31 +64,31 @@ def __init__(self, watch_path: str, output_format: str = "json"):
# Metrics for Task writes
self.internal_task_counter = meter.create_counter(
- "internal_task_write",
- description="Number of internal Task instances written to file"
+ "internal_task_write", description="Number of internal Task instances written to file"
)
self.internal_task_error_counter = meter.create_counter(
"internal_task_error",
- description="Number of errors while writing internal Task instances to file"
+ description="Number of errors while writing internal Task instances to file",
)
# Metrics for CapTask writes
self.internal_captask_counter = meter.create_counter(
"internal_captask_write",
- description="Number of internal CapTask instances written to file"
+ description="Number of internal CapTask instances written to file",
)
self.internal_captask_error_counter = meter.create_counter(
"internal_captask_error",
- description="Number of errors while writing internal CapTask instances to file"
+ description="Number of errors while writing internal CapTask instances to file",
)
self.logger.info(
"InternalWatcher initialized with watch_path=%s, format=%s",
- self.watch_path, self.output_format
+ self.watch_path,
+ self.output_format,
)
@tracer.start_as_current_span("internal_watcher_add")
- def add(self, item: Union[Task, CapTask]) -> str:
+ def add(self, item: Task | CapTask) -> str:
"""
Add a Task or CapTask instance by writing it to a file in the watch_path directory.
@@ -113,7 +112,7 @@ def add(self, item: Union[Task, CapTask]) -> str:
def _add_task(self, task: Task, span: trace.Span) -> str:
"""Handle Task instance writing."""
- task_hash = getattr(task, 'hash', None)
+ task_hash = getattr(task, "hash", None)
if not task_hash:
error_msg = "Task object missing 'hash' attribute"
self.logger.error(error_msg)
@@ -122,15 +121,19 @@ def _add_task(self, task: Task, span: trace.Span) -> str:
self.internal_task_error_counter.add(1)
raise ValueError(error_msg)
- task_id = getattr(task, 'id', 'unknown')
- task_type = getattr(task, 'type', 'unknown')
+ task_id = getattr(task, "id", "unknown")
+ task_type = getattr(task, "type", "unknown")
span.set_attribute("task.id", str(task_id))
span.set_attribute("task.type", task_type)
span.set_attribute("task.hash", str(task_hash))
span.set_attribute("item.type", "Task")
- self.logger.debug("Internally generated Task received: id=%s, hash=%s, type=%s",
- task_id, task_hash, task_type)
+ self.logger.debug(
+ "Internally generated Task received: id=%s, hash=%s, type=%s",
+ task_id,
+ task_hash,
+ task_type,
+ )
try:
file_path = self._write_item_file(task, task_hash, "Task")
@@ -139,14 +142,16 @@ def _add_task(self, task: Task, span: trace.Span) -> str:
self.logger.info("Internal Task %s written to file: %s", task_hash, file_path)
return task_hash
except Exception as e:
- self.logger.error("Failed to write internal Task %s to file: %s", task_hash, e, exc_info=True)
+ self.logger.error(
+ "Failed to write internal Task %s to file: %s", task_hash, e, exc_info=True
+ )
span.record_exception(e)
self.internal_task_error_counter.add(1)
raise
def _add_captask(self, captask: CapTask, span: trace.Span) -> str:
"""Handle CapTask instance writing."""
- captask_hash = getattr(captask, 'hash', None)
+ captask_hash = getattr(captask, "hash", None)
if not captask_hash:
error_msg = "CapTask object missing 'hash' attribute"
self.logger.error(error_msg)
@@ -155,13 +160,14 @@ def _add_captask(self, captask: CapTask, span: trace.Span) -> str:
self.internal_captask_error_counter.add(1)
raise ValueError(error_msg)
- cap_name = getattr(captask, 'cap_name', 'unknown')
+ cap_name = getattr(captask, "cap_name", "unknown")
span.set_attribute("captask.cap_name", cap_name)
span.set_attribute("captask.hash", str(captask_hash))
span.set_attribute("item.type", "CapTask")
- self.logger.debug("Internally generated CapTask received: cap_name=%s, hash=%s",
- cap_name, captask_hash)
+ self.logger.debug(
+ "Internally generated CapTask received: cap_name=%s, hash=%s", cap_name, captask_hash
+ )
try:
file_path = self._write_item_file(captask, captask_hash, "CapTask")
@@ -170,12 +176,14 @@ def _add_captask(self, captask: CapTask, span: trace.Span) -> str:
self.logger.info("Internal CapTask %s written to file: %s", captask_hash, file_path)
return captask_hash
except Exception as e:
- self.logger.error("Failed to write internal CapTask %s to file: %s", captask_hash, e, exc_info=True)
+ self.logger.error(
+ "Failed to write internal CapTask %s to file: %s", captask_hash, e, exc_info=True
+ )
span.record_exception(e)
self.internal_captask_error_counter.add(1)
raise
- def _write_item_file(self, item: Union[Task, CapTask], item_hash: str, item_type: str) -> str:
+ def _write_item_file(self, item: Task | CapTask, item_hash: str, item_type: str) -> str:
"""
Write a Task or CapTask instance to a file in watch_path.
@@ -190,12 +198,12 @@ def _write_item_file(self, item: Union[Task, CapTask], item_hash: str, item_type
file_path = os.path.join(self.watch_path, f"{item_hash}{ext}")
# Extract serializable dictionary
- if hasattr(item, 'to_dict'):
+ if hasattr(item, "to_dict"):
item_dict = item.to_dict()
else:
item_dict = item.__dict__
- with open(file_path, 'w', encoding='utf-8') as f:
+ with open(file_path, "w", encoding="utf-8") as f:
if self.output_format == "json":
json.dump(item_dict, f, indent=2)
else: # yaml
@@ -218,4 +226,4 @@ def _write_item_file(self, item: Union[Task, CapTask], item_hash: str, item_type
#
# # Using YAML
# yaml_watcher = InternalWatcher("/path/to/watch_dir", output_format="yaml")
-# yaml_watcher.add(task) # Writes /path/to/watch_dir/.yaml
\ No newline at end of file
+# yaml_watcher.add(task) # Writes /path/to/watch_dir/.yaml
diff --git a/scl/listener/restful_watch.py b/scl/listener/restful_watch.py
index b1e7907..c4b17af 100644
--- a/scl/listener/restful_watch.py
+++ b/scl/listener/restful_watch.py
@@ -23,21 +23,18 @@
pip install fastapi uvicorn pyyaml opentelemetry-api opentelemetry-sdk
"""
+import json
import logging
import os
-import json
-import shutil
from pathlib import Path
-from typing import Dict, List, Optional, Union
import uvicorn
-from fastapi import FastAPI, Request, HTTPException
+from fastapi import FastAPI, HTTPException, Request
+from opentelemetry import trace
-from scl.otel.otel import tracer, meter
-from scl.meta.task import Task
from scl.meta.captask import CapTask
-
-from opentelemetry import trace
+from scl.meta.task import Task
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
@@ -49,11 +46,7 @@ class RestFulHandler:
"""
def __init__(
- self,
- watch_path: str,
- host: str = "0.0.0.0",
- port: int = 8000,
- log_level: str = "info"
+ self, watch_path: str, host: str = "0.0.0.0", port: int = 8000, log_level: str = "info"
):
"""
Initialize REST handler.
@@ -77,28 +70,23 @@ def __init__(
# Metrics
self.restful_task_counter = meter.create_counter(
- "restful_task_received",
- description="Total number of REST tasks received"
+ "restful_task_received", description="Total number of REST tasks received"
)
self.restful_captask_counter = meter.create_counter(
- "restful_captask_received",
- description="Total number of REST captasks received"
+ "restful_captask_received", description="Total number of REST captasks received"
)
self.restful_item_valid_counter = meter.create_counter(
- "restful_item_valid",
- description="Number of REST items successfully processed"
+ "restful_item_valid", description="Number of REST items successfully processed"
)
self.restful_item_invalid_counter = meter.create_counter(
"restful_item_invalid",
- description="Number of REST items that failed validation/conversion"
+ description="Number of REST items that failed validation/conversion",
)
self.status_check_counter = meter.create_counter(
- "restful_status_check",
- description="Number of status checks performed"
+ "restful_status_check", description="Number of status checks performed"
)
self.approve_counter = meter.create_counter(
- "restful_approve",
- description="Number of approval actions performed"
+ "restful_approve", description="Number of approval actions performed"
)
# FastAPI app
@@ -117,7 +105,7 @@ def _register_routes(self):
# POST /tasks
# -------------------------------------------------------------------------
@tracer.start_as_current_span("rest_api_receive_task")
- async def _receive_task(self, request: Request) -> Dict[str, str]:
+ async def _receive_task(self, request: Request) -> dict[str, str]:
"""
POST /tasks endpoint.
Validates JSON, converts to Task, writes file to watch_path, returns hash.
@@ -136,7 +124,7 @@ async def _receive_task(self, request: Request) -> Dict[str, str]:
self.logger.error(f"Failed to create Task object: {e}")
current_span.record_exception(e)
self.restful_item_invalid_counter.add(1, {"item.type": "Task"})
- raise HTTPException(status_code=422, detail="Invalid task format")
+ raise HTTPException(status_code=422, detail="Invalid task format") from e
return self._finalize_item(task_obj, "Task", current_span)
@@ -144,7 +132,7 @@ async def _receive_task(self, request: Request) -> Dict[str, str]:
# POST /captasks
# -------------------------------------------------------------------------
@tracer.start_as_current_span("rest_api_receive_captask")
- async def _receive_captask(self, request: Request) -> Dict[str, str]:
+ async def _receive_captask(self, request: Request) -> dict[str, str]:
"""
POST /captasks endpoint.
Validates JSON, converts to CapTask, writes file to watch_path, returns hash.
@@ -163,7 +151,7 @@ async def _receive_captask(self, request: Request) -> Dict[str, str]:
self.logger.error(f"Failed to create CapTask object: {e}")
current_span.record_exception(e)
self.restful_item_invalid_counter.add(1, {"item.type": "CapTask"})
- raise HTTPException(status_code=422, detail="Invalid captask format")
+ raise HTTPException(status_code=422, detail="Invalid captask format") from e
return self._finalize_item(captask_obj, "CapTask", current_span)
@@ -171,7 +159,7 @@ async def _receive_captask(self, request: Request) -> Dict[str, str]:
# GET /items/{item_hash}
# -------------------------------------------------------------------------
@tracer.start_as_current_span("rest_api_check_status")
- async def _check_status(self, item_hash: str) -> Dict[str, str]:
+ async def _check_status(self, item_hash: str) -> dict[str, str]:
"""
GET /items/{item_hash} endpoint.
Checks if the item file exists and returns its processing status.
@@ -192,7 +180,7 @@ async def _check_status(self, item_hash: str) -> Dict[str, str]:
# GET /tasks/waiting
# -------------------------------------------------------------------------
@tracer.start_as_current_span("rest_api_list_waiting")
- async def _list_waiting(self) -> List[Dict[str, Union[str, Dict]]]:
+ async def _list_waiting(self) -> list[dict[str, str | dict]]:
"""
GET /tasks/waiting endpoint.
Lists all items (Task/CapTask) currently waiting for approval.
@@ -205,18 +193,14 @@ async def _list_waiting(self) -> List[Dict[str, Union[str, Dict]]]:
return waiting_items
for filename in os.listdir(self.waiting_approval_dir):
- if not filename.lower().endswith('.json'):
+ if not filename.lower().endswith(".json"):
continue
filepath = os.path.join(self.waiting_approval_dir, filename)
try:
- with open(filepath, 'r', encoding='utf-8') as f:
+ with open(filepath, encoding="utf-8") as f:
data = json.load(f)
item_type = self._guess_type(data, filename)
- waiting_items.append({
- "hash": Path(filename).stem,
- "type": item_type,
- "data": data
- })
+ waiting_items.append({"hash": Path(filename).stem, "type": item_type, "data": data})
except Exception as e:
self.logger.warning(f"Failed to read waiting file {filename}: {e}")
@@ -228,7 +212,7 @@ async def _list_waiting(self) -> List[Dict[str, Union[str, Dict]]]:
# POST /items/{item_hash}/approve
# -------------------------------------------------------------------------
@tracer.start_as_current_span("rest_api_approve_item")
- async def _approve_item(self, item_hash: str) -> Dict[str, str]:
+ async def _approve_item(self, item_hash: str) -> dict[str, str]:
"""
POST /items/{item_hash}/approve endpoint.
Approves a waiting item by moving its file from waiting_approval_dir
@@ -239,7 +223,7 @@ async def _approve_item(self, item_hash: str) -> Dict[str, str]:
# Locate file in waiting_approval_dir
src_path = None
- for ext in ['.json', '.yaml', '.yml']:
+ for ext in [".json", ".yaml", ".yml"]:
candidate = os.path.join(self.waiting_approval_dir, f"{item_hash}{ext}")
if os.path.isfile(candidate):
src_path = candidate
@@ -252,21 +236,23 @@ async def _approve_item(self, item_hash: str) -> Dict[str, str]:
try:
# Read file content
- with open(src_path, 'r', encoding='utf-8') as f:
- if src_path.endswith(('.yaml', '.yml')):
+ with open(src_path, encoding="utf-8") as f:
+ if src_path.endswith((".yaml", ".yml")):
import yaml
+
data = yaml.safe_load(f)
else:
data = json.load(f)
# Update approval flag
- data['approval'] = True
+ data["approval"] = True
# Write updated file to watch_path
dest_path = os.path.join(self.watch_path, os.path.basename(src_path))
- with open(dest_path, 'w', encoding='utf-8') as f:
- if dest_path.endswith(('.yaml', '.yml')):
+ with open(dest_path, "w", encoding="utf-8") as f:
+ if dest_path.endswith((".yaml", ".yml")):
import yaml
+
yaml.dump(data, f)
else:
json.dump(data, f, indent=2)
@@ -283,7 +269,7 @@ async def _approve_item(self, item_hash: str) -> Dict[str, str]:
except Exception as e:
self.logger.error(f"Failed to approve item {item_hash}: {e}")
current_span.record_exception(e)
- raise HTTPException(status_code=500, detail="Approval failed")
+ raise HTTPException(status_code=500, detail="Approval failed") from e
# -------------------------------------------------------------------------
# Helper methods
@@ -296,16 +282,20 @@ async def _parse_json(self, request: Request, span: trace.Span) -> dict:
self.logger.warning(f"Invalid JSON received: {e}")
span.record_exception(e)
self.restful_item_invalid_counter.add(1)
- raise HTTPException(status_code=400, detail="Invalid JSON body")
+ raise HTTPException(status_code=400, detail="Invalid JSON body") from e
- def _finalize_item(self, item: Union[Task, CapTask], item_type: str, span: trace.Span) -> Dict[str, str]:
+ def _finalize_item(
+ self, item: Task | CapTask, item_type: str, span: trace.Span
+ ) -> dict[str, str]:
"""Write item to file and return hash response."""
- item_hash = getattr(item, 'hash', None)
+ item_hash = getattr(item, "hash", None)
if not item_hash:
self.logger.error(f"{item_type} object missing 'hash' attribute")
span.set_attribute("error", "missing_hash")
self.restful_item_invalid_counter.add(1, {"item.type": item_type})
- raise HTTPException(status_code=500, detail=f"{item_type} object has no hash identifier")
+ raise HTTPException(
+ status_code=500, detail=f"{item_type} object has no hash identifier"
+ )
span.set_attribute("item.hash", item_hash)
@@ -317,14 +307,14 @@ def _finalize_item(self, item: Union[Task, CapTask], item_type: str, span: trace
return {"status": "accepted", "hash": item_hash}
- def _write_item_file(self, item: Union[Task, CapTask], item_hash: str, item_type: str) -> str:
+ def _write_item_file(self, item: Task | CapTask, item_hash: str, item_type: str) -> str:
"""Write item to a JSON file in watch_path."""
ext = ".json"
file_path = os.path.join(self.watch_path, f"{item_hash}{ext}")
- item_dict = item.to_dict() if hasattr(item, 'to_dict') else item.__dict__
+ item_dict = item.to_dict() if hasattr(item, "to_dict") else item.__dict__
- with open(file_path, 'w', encoding='utf-8') as f:
+ with open(file_path, "w", encoding="utf-8") as f:
json.dump(item_dict, f, indent=2)
return file_path
@@ -342,7 +332,7 @@ def _determine_status(self, item_hash: str) -> str:
waiting_approval_dir = Path(self.waiting_approval_dir)
# Check waiting approval (unapproved items)
- for ext in ['.json', '.yaml', '.yml']:
+ for ext in [".json", ".yaml", ".yml"]:
if (waiting_approval_dir / f"{item_hash}{ext}").is_file():
return "waiting_approval"
@@ -379,9 +369,9 @@ def _determine_status(self, item_hash: str) -> str:
def _guess_type(self, data: dict, filename: str) -> str:
"""Guess item type from data content or filename."""
- if 'cap_name' in data and 'args' in data:
+ if "cap_name" in data and "args" in data:
return "CapTask"
- elif 'system_prompt' in data or 'prompt_list' in data:
+ elif "system_prompt" in data or "prompt_list" in data:
return "Task"
return "Unknown"
@@ -396,4 +386,4 @@ def start(self):
# host="0.0.0.0",
# port=8000
# )
-# handler.start() # Blocks until server stops
\ No newline at end of file
+# handler.start() # Blocks until server stops
diff --git a/scl/llm_chat.py b/scl/llm_chat.py
index f52a48e..8accadc 100644
--- a/scl/llm_chat.py
+++ b/scl/llm_chat.py
@@ -1,10 +1,11 @@
import json
import logging
-from scl.otel.otel import tracer
+
from scl.cap_reg import CapRegistry
-from scl.meta.msg import Msg
from scl.config import config
-from scl.otel.otel import cap_counts
+from scl.meta.msg import Msg
+from scl.otel.otel import cap_counts, tracer
+
## why not we just prvide the metrics and leave the function to user themself?
## using hooks to provide user capbility to overwrite the default behavior
@@ -17,39 +18,43 @@
#### using cache value into hook
@tracer.start_as_current_span("send_messages")
def send_messages(
- client, model,
- cap_registry:CapRegistry,
- ToolNames, # todo here, support both name and give Cap
- msg:Msg,
- Turns):
- if Turns == 0:
+ client,
+ model,
+ cap_registry: CapRegistry,
+ ToolNames, # todo here, support both name and give Cap
+ msg: Msg,
+ Turns,
+):
+ if Turns == 0:
## to do an autonomy sider
### hook of overwrite limit
limit = config.limit
### hook of overwrite min_similarity
min_similarity = config.min_similarity
tools_named = cap_registry.getCapsByNames(ToolNames)
- ## metrics
+ ## metrics
### search time,search number
### a key-value cache for information
tools_autonomy = cap_registry.getCapsBySimilarity(msg, limit, min_similarity)
- ## metrics
+ ## metrics
### search time,search number
### a key-value cache for information
tools_history = cap_registry.getCapsByHistory(msg, limit, min_similarity)
- ## metrics
+ ## metrics
### search time,search number
### a key-value cache for information
### to do an autonomy sider hook?
- tools_merged = {
+ tools_merged = {
**({} if tools_named is None else tools_named),
**({} if tools_autonomy is None else tools_autonomy),
- **({} if tools_history is None else tools_history)
+ **({} if tools_history is None else tools_history),
}
cap_counts["total"] = len(tools_merged)
- cap_counts["duplicate"] = len(tools_named) + len(tools_autonomy) + len(tools_history) - cap_counts["total"]
- ## metrics tool number,metrics as duplicate number?
+ cap_counts["duplicate"] = (
+ len(tools_named) + len(tools_autonomy) + len(tools_history) - cap_counts["total"]
+ )
+ ## metrics tool number,metrics as duplicate number?
## or a cache for duplicate info
tools = []
for tool in list(tools_merged.values()):
@@ -60,29 +65,25 @@ def send_messages(
logging.info(msg)
# Build request parameters - only include tools if not empty
# Some APIs (like DashScope) don't accept empty tools list
- request_params = {
- "model": model,
- "messages": msg.messages
- }
+ request_params = {"model": model, "messages": msg.messages}
if tools: # Only add tools parameter if tools list is not empty
request_params["tools"] = tools
-
+
response = client.chat.completions.create(**request_params)
return response.choices[0].message
else:
- response = client.chat.completions.create(
- model=model,
- messages=msg.messages
- )
+ response = client.chat.completions.create(model=model, messages=msg.messages)
return response.choices[0].message
+
@tracer.start_as_current_span("function_call_playground")
def function_call_playground(
- client, model,
- cap_registry:CapRegistry,
+ client,
+ model,
+ cap_registry: CapRegistry,
ToolNames,
- msg:Msg,
- ):
+ msg: Msg,
+):
turns = 0
## metric execution time
response = send_messages(client, model, cap_registry, ToolNames, msg, turns)
@@ -98,7 +99,7 @@ def function_call_playground(
func1_args = tool_call.function.arguments
## todo-> debug/trace
logging.info(f"func1_name: {func1_name}, func1_args: {func1_args}")
- args_dict = json.loads(func1_args)
+ args_dict = json.loads(func1_args) # noqa: F841 - TODO: wire into call_cap_safe below
cap = cap_registry.get_cap_by_name(func1_name)
# todo
# func1_out = cap_registry.call_cap_safe(cap,args_dict)
@@ -106,7 +107,7 @@ def function_call_playground(
cap_registry.record(msg, cap)
msg.append(response)
- msg.append_cap_result(func1_out, tool_call.id)
+ msg.append_cap_result(func1_out, tool_call.id) # noqa: F821 - TODO: func1_out undefined (call above is commented out)
## metric execution time
response = send_messages(client, model, cap_registry, ToolNames, msg, turns)
else:
diff --git a/scl/meta/__init__.py b/scl/meta/__init__.py
index 3f11e5f..52d002a 100644
--- a/scl/meta/__init__.py
+++ b/scl/meta/__init__.py
@@ -5,9 +5,9 @@
__version__ = "0.1.0"
# Import main classes for convenience
-from .task import Task
-from .captask import CapTask
from .capability import Capability
+from .captask import CapTask
from .functioncall import FunctionCall
from .msg import Msg as Message
from .skill import Skill
+from .task import Task
diff --git a/scl/meta/capability.py b/scl/meta/capability.py
index 2d17543..e688cb6 100644
--- a/scl/meta/capability.py
+++ b/scl/meta/capability.py
@@ -15,16 +15,17 @@
- Logger provides info and debug levels.
- Dependencies are documented as `pip install` commands, not requirements.txt.
"""
+
import logging
from abc import ABC, abstractmethod
-from typing import Optional, Dict, Any
+from typing import Any
# OpenTelemetry imports
from opentelemetry import trace
-from scl.otel.otel import tracer, meter # Assuming this is the correct import path
# External embedding function (behavior: performs embedding operation)
from scl.embeddings.embedding import embed
+from scl.otel.otel import meter, tracer # Assuming this is the correct import path
# Setup logger
logger = logging.getLogger(__name__)
@@ -32,7 +33,7 @@
# Setup metrics
capability_embedding_counter = meter.create_counter(
"capability.embedding.generated",
- description="Number of times embedding_description is computed for a Capability"
+ description="Number of times embedding_description is computed for a Capability",
)
@@ -52,13 +53,15 @@ class Capability(ABC):
"""
@tracer.start_as_current_span("Capability.__init__")
- def __init__(self,
- name: str,
- type: str,
- description: Optional[str] = None,
- original_body: Optional[str] = None,
- llm_description: Optional[str] = None,
- function_impl: Optional[str] = None):
+ def __init__(
+ self,
+ name: str,
+ type: str,
+ description: str | None = None,
+ original_body: str | None = None,
+ llm_description: str | None = None,
+ function_impl: str | None = None,
+ ):
current_span = trace.get_current_span()
current_span.set_attribute("capability.name", name)
current_span.set_attribute("capability.type", type)
@@ -81,8 +84,8 @@ def __init__(self,
"name": self._name,
"type": self._type,
"has_description": self._description is not None,
- "has_function_impl": self._function_impl is not None
- }
+ "has_function_impl": self._function_impl is not None,
+ },
)
logger.debug(
f"Capability created: name='{self._name}', type='{self._type}', "
@@ -95,12 +98,12 @@ def name(self) -> str:
return self._name
@property
- def description(self) -> Optional[str]:
+ def description(self) -> str | None:
"""Function description for progressive loading."""
return self._description
@property
- def original_body(self) -> Optional[str]:
+ def original_body(self) -> str | None:
"""Original description body."""
return self._original_body
@@ -121,7 +124,7 @@ def embedding_description(self):
self._embedding_description = None
logger.warning(
"Cannot generate embedding for capability '%s': no description provided.",
- self._name
+ self._name,
)
current_span.set_attribute("embedding.skipped", True)
return None
@@ -132,9 +135,14 @@ def embedding_description(self):
capability_embedding_counter.add(1, {"capability.type": self._type})
logger.info(f"Successfully generated embedding for capability '{self._name}'")
except Exception as e:
- logger.error(f"Failed to generate embedding for capability '{self._name}': {e}", exc_info=True)
+ logger.error(
+ f"Failed to generate embedding for capability '{self._name}': {e}",
+ exc_info=True,
+ )
current_span.record_exception(e)
- current_span.set_status(trace.Status(trace.StatusCode.ERROR, "Embedding generation failed"))
+ current_span.set_status(
+ trace.Status(trace.StatusCode.ERROR, "Embedding generation failed")
+ )
raise # Re-raise after logging/tracing
return self._embedding_description
@@ -145,18 +153,18 @@ def type(self) -> str:
return self._type
@property
- def llm_description(self) -> Optional[str]:
+ def llm_description(self) -> str | None:
"""LLM-generated description for tool field."""
return self._llm_description
@property
- def function_impl(self) -> Optional[str]:
+ def function_impl(self) -> str | None:
"""Function implementation for sandbox execution."""
return self._function_impl
@abstractmethod
@tracer.start_as_current_span("Capability.execute")
- def execute(self, args_dict: Dict[str, Any]) -> Any:
+ def execute(self, args_dict: dict[str, Any]) -> Any:
"""
Execute the capability with the given arguments.
@@ -185,9 +193,11 @@ def __repr__(self) -> str:
def __eq__(self, other: object) -> bool:
if not isinstance(other, Capability):
return False
- return (self._name == other._name and
- self._description == other._description and
- self._original_body == other._original_body)
+ return (
+ self._name == other._name
+ and self._description == other._description
+ and self._original_body == other._original_body
+ )
# ----------------------------------------------------------------------
@@ -232,4 +242,4 @@ def __eq__(self, other: object) -> bool:
#
# # Execute capabilities
# result_skill = skill1.execute({"query": "latest news"})
-# result_func = func1.execute({"expr": "2+2"})
\ No newline at end of file
+# result_func = func1.execute({"expr": "2+2"})
diff --git a/scl/meta/captask.py b/scl/meta/captask.py
index fb5e17a..b169c8b 100644
--- a/scl/meta/captask.py
+++ b/scl/meta/captask.py
@@ -24,16 +24,18 @@
Installation:
pip install opentelemetry-api opentelemetry-sdk
"""
+
import json
import logging
import os
import uuid
-from dataclasses import dataclass, asdict, field
-from typing import Any, Dict, List, Optional
+from dataclasses import asdict, dataclass
+from typing import Any
# OpenTelemetry imports
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
+
+from scl.otel.otel import meter, tracer
# Try to import todo_watch_dir from config; fallback if not available
try:
@@ -48,24 +50,19 @@
# Setup metrics
cap_task_created_counter = meter.create_counter(
- "cap_task.created",
- description="Number of CapTask instances created"
+ "cap_task.created", description="Number of CapTask instances created"
)
cap_task_serialized_counter = meter.create_counter(
- "cap_task.serialized",
- description="Number of times a CapTask was serialized to JSON"
+ "cap_task.serialized", description="Number of times a CapTask was serialized to JSON"
)
cap_task_deserialized_counter = meter.create_counter(
- "cap_task.deserialized",
- description="Number of times a CapTask was deserialized from JSON"
+ "cap_task.deserialized", description="Number of times a CapTask was deserialized from JSON"
)
cap_task_status_changed_counter = meter.create_counter(
- "cap_task.status_changed",
- description="Number of times a CapTask status changed"
+ "cap_task.status_changed", description="Number of times a CapTask status changed"
)
cap_task_file_written_counter = meter.create_counter(
- "cap_task.file_written",
- description="Number of CapTask files written to todo_watch_dir"
+ "cap_task.file_written", description="Number of CapTask files written to todo_watch_dir"
)
@@ -82,10 +79,10 @@ class CapTask:
approval (bool): Whether the task is approved for execution (default True).
status (str): Current status, one of {"created", "Processed", "Error"}.
full_result (str): The complete result/output from capability invocation.
-
+
Properties:
result (str): First 500 lines of full_result.
-
+
Example usage:
task = CapTask(
cap_name="send_email",
@@ -106,12 +103,12 @@ class CapTask:
"""
cap_name: str
- args: List[Any]
- task_hash: Optional[str] = None
- hash: Optional[str] = None # Auto-generated if not provided
- approval: bool = True # Approval flag
- status: str = "created" # Current status
- full_result: str = "" # Full result of the capability invocation
+ args: list[Any]
+ task_hash: str | None = None
+ hash: str | None = None # Auto-generated if not provided
+ approval: bool = True # Approval flag
+ status: str = "created" # Current status
+ full_result: str = "" # Full result of the capability invocation
VALID_STATUSES = {"created", "Processed", "Error"}
@@ -150,16 +147,21 @@ def __post_init__(self):
span.set_attribute("cap_task.approval", self.approval)
span.set_attribute("cap_task.status", self.status)
- cap_task_created_counter.add(1, {
- "cap_name": self.cap_name,
- "approved": str(self.approval).lower(),
- "status": self.status,
- })
+ cap_task_created_counter.add(
+ 1,
+ {
+ "cap_name": self.cap_name,
+ "approved": str(self.approval).lower(),
+ "status": self.status,
+ },
+ )
logger.debug(
f"Created CapTask: hash={self.hash}, cap_name={self.cap_name}, "
f"approval={self.approval}, status={self.status}"
)
- logger.info(f"CapTask created for capability '{self.cap_name}' with status '{self.status}'")
+ logger.info(
+ f"CapTask created for capability '{self.cap_name}' with status '{self.status}'"
+ )
# Write file to todo_watch_dir
self._write_file_to_watch_dir(span)
@@ -176,7 +178,7 @@ def _write_file_to_watch_dir(self, parent_span: trace.Span):
file_path = os.path.join(todo_watch_dir, f"{self.hash}.json")
json_content = self.to_json(indent=2)
- with open(file_path, 'w', encoding='utf-8') as f:
+ with open(file_path, "w", encoding="utf-8") as f:
f.write(json_content)
cap_task_file_written_counter.add(1, {"cap_name": self.cap_name})
@@ -213,14 +215,17 @@ def set_status(self, new_status: str) -> None:
old_status = self.status
object.__setattr__(self, "status", new_status)
- cap_task_status_changed_counter.add(1, {
- "cap_name": self.cap_name,
- "old_status": old_status,
- "new_status": new_status,
- })
+ cap_task_status_changed_counter.add(
+ 1,
+ {
+ "cap_name": self.cap_name,
+ "old_status": old_status,
+ "new_status": new_status,
+ },
+ )
logger.info(f"CapTask {self.hash} status changed from '{old_status}' to '{new_status}'")
- def to_dict(self) -> Dict[str, Any]:
+ def to_dict(self) -> dict[str, Any]:
"""Convert CapTask to a dictionary suitable for JSON serialization."""
with tracer.start_as_current_span("CapTask.to_dict") as span:
span.set_attribute("cap_task.hash", self.hash)
@@ -229,7 +234,7 @@ def to_dict(self) -> Dict[str, Any]:
logger.debug(f"Serialized CapTask {self.hash} to dict")
return data
- def to_json(self, indent: Optional[int] = None) -> str:
+ def to_json(self, indent: int | None = None) -> str:
"""
Serialize CapTask to a JSON string.
@@ -248,7 +253,7 @@ def to_json(self, indent: Optional[int] = None) -> str:
@classmethod
@tracer.start_as_current_span("CapTask.from_dict")
- def from_dict(cls, data: Dict[str, Any]) -> "CapTask":
+ def from_dict(cls, data: dict[str, Any]) -> "CapTask":
"""
Create a CapTask instance from a dictionary.
@@ -277,7 +282,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "CapTask":
hash=data.get("hash"),
approval=data.get("approval", True),
status=data.get("status", "created"),
- full_result=data.get("full_result", "")
+ full_result=data.get("full_result", ""),
)
cap_task_deserialized_counter.add(1, {"cap_name": task.cap_name})
logger.debug(
@@ -363,4 +368,4 @@ def __repr__(self) -> str:
print(task2.status) # 'created' (or 'Processed' if set)
print(task2.approval) # False
print(task2.result) # '' until full_result is set
-"""
\ No newline at end of file
+"""
diff --git a/scl/meta/functioncall.py b/scl/meta/functioncall.py
index 18a0c8e..ce4f4f6 100644
--- a/scl/meta/functioncall.py
+++ b/scl/meta/functioncall.py
@@ -21,20 +21,20 @@
- OpenTelemetry integrated for tracing, metrics, and structured logging.
- Logger provides info and debug levels.
"""
+
import logging
-from typing import Optional, Dict, Any
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
-from scl.embeddings.embedding import embed
+
from scl.meta.capability import Capability
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Optional metric for function call executions
function_call_counter = meter.create_counter(
- "function_call.executed",
- description="Number of times a FunctionCall was executed"
+ "function_call.executed", description="Number of times a FunctionCall was executed"
)
@@ -47,12 +47,14 @@ class FunctionCall(Capability):
"""
@tracer.start_as_current_span("FunctionCall.__init__")
- def __init__(self,
- name: str,
- description: str,
- original_body: Optional[str] = None,
- llm_description: Optional[str] = None,
- function_impl: Optional[str] = None):
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ original_body: str | None = None,
+ llm_description: str | None = None,
+ function_impl: str | None = None,
+ ):
current_span = trace.get_current_span()
current_span.set_attribute("function_call.name", name)
current_span.set_attribute("function_call.has_function_impl", function_impl is not None)
@@ -64,14 +66,16 @@ def __init__(self,
description=description,
original_body=original_body,
llm_description=llm_description,
- function_impl=function_impl
+ function_impl=function_impl,
)
- logger.debug(f"FunctionCall '{name}' initialized with impl length: {len(function_impl) if function_impl else 0}")
+ logger.debug(
+ f"FunctionCall '{name}' initialized with impl length: {len(function_impl) if function_impl else 0}"
+ )
logger.info(f"FunctionCall '{name}' created")
@tracer.start_as_current_span("FunctionCall.execute")
- def execute(self, args_dict: Dict[str, Any]) -> Any:
+ def execute(self, args_dict: dict[str, Any]) -> Any:
"""
Execute the function call with the provided arguments.
@@ -103,11 +107,11 @@ def execute(self, args_dict: Dict[str, Any]) -> Any:
try:
# Build the dynamic function definition as per the design comments
func_code = self.function_impl
- param_names = ', '.join(args_dict.keys())
+ param_names = ", ".join(args_dict.keys())
func_lines = [f"def dynamic_func({param_names}):"]
# Indent each line of the function implementation for proper Python syntax
- func_lines.extend([f" {line}" for line in func_code.split('\n')])
- func_def = '\n'.join(func_lines)
+ func_lines.extend([f" {line}" for line in func_code.split("\n")])
+ func_def = "\n".join(func_lines)
# Log the generated code for debugging/traceability
logger.info(f"args_dict: {args_dict}")
@@ -116,7 +120,7 @@ def execute(self, args_dict: Dict[str, Any]) -> Any:
local_vars = {}
# Execute the definition in the current global context
exec(func_def, globals(), local_vars)
- func = local_vars['dynamic_func']
+ func = local_vars["dynamic_func"]
# Call the newly defined function with the provided arguments
result = func(**args_dict)
@@ -161,4 +165,4 @@ def __repr__(self) -> str:
function_impl="print(f'Hello, {name}!')"
)
greet_cap.execute({"name": "Alice"}) # prints "Hello, Alice!"
-"""
\ No newline at end of file
+"""
diff --git a/scl/meta/msg.py b/scl/meta/msg.py
index d959ca0..6f5d77e 100644
--- a/scl/meta/msg.py
+++ b/scl/meta/msg.py
@@ -1,20 +1,19 @@
from scl.embeddings.embedding import embed
+
class Msg:
def __init__(self, messages):
self._messages = messages
self._embed = embed(messages)
- #self._embed = embed(messages[0]['content'])
+ # self._embed = embed(messages[0]['content'])
def append(self, context):
self._messages.append(context)
- def append_cap_result(self, func1_out,tool_call_id):
- self._messages.append({
- 'role': 'tool',
- 'content': f'{func1_out}',
- 'tool_call_id': tool_call_id
- })
+ def append_cap_result(self, func1_out, tool_call_id):
+ self._messages.append(
+ {"role": "tool", "content": f"{func1_out}", "tool_call_id": tool_call_id}
+ )
@property
def messages(self):
diff --git a/scl/meta/skill.py b/scl/meta/skill.py
index ea2cf49..59c19e7 100644
--- a/scl/meta/skill.py
+++ b/scl/meta/skill.py
@@ -3,31 +3,30 @@
### support interface to LLM as function call
### support tool call loop
import json
-from typing import Dict, Optional
-from scl.embeddings.embedding import embed
-from scl.meta.skills_ref.parser import SkillProperties
+
from scl.meta.capability import Capability
+from scl.meta.skills_ref.parser import SkillProperties
+
class Skill(Capability):
- def __init__(self,
- SkillProperties: SkillProperties):
+ def __init__(self, SkillProperties: SkillProperties):
if SkillProperties.metadata:
self._original_body_dict = SkillProperties.metadata.copy()
else:
self._original_body_dict = {}
original_body = json.dumps(self._original_body_dict, ensure_ascii=False, indent=2)
-
+
super().__init__(
name=SkillProperties.name,
description=SkillProperties.description,
original_body=original_body,
type="skill",
llm_description=None, # tbd
- function_impl=None # tbd
+ function_impl=None, # tbd
)
@property
- def original_body_dict(self) -> Dict[str, str]:
+ def original_body_dict(self) -> dict[str, str]:
"""原始描述获取字典表示"""
return self._original_body_dict
diff --git a/scl/meta/skills_ref/errors.py b/scl/meta/skills_ref/errors.py
index 065b7db..e9b49b5 100644
--- a/scl/meta/skills_ref/errors.py
+++ b/scl/meta/skills_ref/errors.py
@@ -1,5 +1,5 @@
"""Skill-related exceptions."""
-from typing import List, Optional
+
class SkillError(Exception):
"""Base exception for all skill-related errors."""
@@ -20,6 +20,6 @@ class ValidationError(SkillError):
errors: List of validation error messages (may contain just one)
"""
- def __init__(self, message: str, errors: Optional[List[str]] = None):
+ def __init__(self, message: str, errors: list[str] | None = None):
super().__init__(message)
self.errors = errors if errors is not None else [message]
diff --git a/scl/meta/skills_ref/models.py b/scl/meta/skills_ref/models.py
index 77fa89e..7b7515d 100644
--- a/scl/meta/skills_ref/models.py
+++ b/scl/meta/skills_ref/models.py
@@ -1,7 +1,6 @@
"""Data models for Agent Skills."""
from dataclasses import dataclass, field
-from typing import Optional
@dataclass
@@ -20,9 +19,9 @@ class SkillProperties:
name: str
description: str
- license: Optional[str] = None
- compatibility: Optional[str] = None
- allowed_tools: Optional[str] = None
+ license: str | None = None
+ compatibility: str | None = None
+ allowed_tools: str | None = None
metadata: dict[str, str] = field(default_factory=dict)
def to_dict(self) -> dict:
diff --git a/scl/meta/skills_ref/parser.py b/scl/meta/skills_ref/parser.py
index 690c14e..4b4c0de 100644
--- a/scl/meta/skills_ref/parser.py
+++ b/scl/meta/skills_ref/parser.py
@@ -1,7 +1,6 @@
"""YAML frontmatter parsing for SKILL.md files."""
from pathlib import Path
-from typing import Optional
import strictyaml
@@ -9,7 +8,7 @@
from .models import SkillProperties
-def find_skill_md(skill_dir: Path) -> Optional[Path]:
+def find_skill_md(skill_dir: Path) -> Path | None:
"""Find the SKILL.md file in a skill directory.
Prefers SKILL.md (uppercase) but accepts skill.md (lowercase).
@@ -53,7 +52,7 @@ def parse_frontmatter(content: str) -> tuple[dict, str]:
parsed = strictyaml.load(frontmatter_str)
metadata = parsed.data
except strictyaml.YAMLError as e:
- raise ParseError(f"Invalid YAML in frontmatter: {e}")
+ raise ParseError(f"Invalid YAML in frontmatter: {e}") from e
if not isinstance(metadata, dict):
raise ParseError("SKILL.md frontmatter must be a YAML mapping")
diff --git a/scl/meta/task.py b/scl/meta/task.py
index 5b18763..5efa5c6 100644
--- a/scl/meta/task.py
+++ b/scl/meta/task.py
@@ -33,14 +33,14 @@
import hashlib
import json
import logging
-from datetime import datetime, timezone
-from typing import Any, Dict, List, Optional, Union
+from datetime import UTC, datetime
+from typing import Any
from opentelemetry import trace
-from scl.otel.otel import meter, tracer
# Import CapTask for the required cap_tasks list
from scl.meta.captask import CapTask
+from scl.otel.otel import meter, tracer
# Metrics
task_created_counter = meter.create_counter(
@@ -75,14 +75,14 @@ class Task:
def __init__(
self,
system_prompt: str,
- prompt_list: Optional[List[str]] = None,
- capacity: Optional[List[str]] = None,
+ prompt_list: list[str] | None = None,
+ capacity: list[str] | None = None,
status: str = "created",
approval: bool = True,
- additional: Optional[Dict[str, str]] = None,
- previous_hash: Optional[str] = None,
- sub_tasks: Optional[List["Task"]] = None,
- cap_tasks: Optional[List[CapTask]] = None,
+ additional: dict[str, str] | None = None,
+ previous_hash: str | None = None,
+ sub_tasks: list["Task"] | None = None,
+ cap_tasks: list[CapTask] | None = None,
) -> None:
"""
Initialize a Task instance.
@@ -104,18 +104,18 @@ def __init__(
span.set_attribute("task.approval", approval)
self._system_prompt: str = system_prompt
- self._prompt_list: List[str] = prompt_list.copy() if prompt_list else []
- self._capacity: List[str] = capacity.copy() if capacity else []
+ self._prompt_list: list[str] = prompt_list.copy() if prompt_list else []
+ self._capacity: list[str] = capacity.copy() if capacity else []
self._status: str = status if status in self.VALID_STATUSES else "created"
self._approval: bool = approval
- self._additional: Dict[str, str] = additional.copy() if additional else {}
- self._previous_hash: Optional[str] = previous_hash
- self._sub_tasks: List["Task"] = []
- self._cap_tasks: List[CapTask] = [] # New: list of CapTasks
- self._parent: Optional["Task"] = None
+ self._additional: dict[str, str] = additional.copy() if additional else {}
+ self._previous_hash: str | None = previous_hash
+ self._sub_tasks: list[Task] = []
+ self._cap_tasks: list[CapTask] = [] # New: list of CapTasks
+ self._parent: Task | None = None
# Timestamps for LRU ordering
- self._created_at: datetime = datetime.now(timezone.utc)
+ self._created_at: datetime = datetime.now(UTC)
self._updated_at: datetime = self._created_at
# Add initial subtasks if provided
@@ -146,7 +146,9 @@ def __init__(
)
logger.debug(
"Task details: system_prompt=%s, prompt_list=%s, capacity=%s",
- self._system_prompt[:50] + "..." if len(self._system_prompt) > 50 else self._system_prompt,
+ self._system_prompt[:50] + "..."
+ if len(self._system_prompt) > 50
+ else self._system_prompt,
self._prompt_list,
self._capacity,
)
@@ -169,7 +171,7 @@ def system_prompt(self, value: str) -> None:
self._touch()
@property
- def prompt_list(self) -> List[str]:
+ def prompt_list(self) -> list[str]:
"""List of prompt strings (history)."""
return self._prompt_list.copy() # return copy to prevent external mutation
@@ -182,12 +184,12 @@ def add_prompt(self, prompt: str) -> None:
logger.debug("Prompt added to task %s", self.hash)
@property
- def capacity(self) -> List[str]:
+ def capacity(self) -> list[str]:
"""List of capacity strings."""
return self._capacity.copy()
@capacity.setter
- def capacity(self, value: List[str]) -> None:
+ def capacity(self, value: list[str]) -> None:
"""Replace capacity list."""
with tracer.start_as_current_span("Task.capacity.setter") as span:
span.set_attribute("capacity_count", len(value))
@@ -212,9 +214,7 @@ def status(self, value: str) -> None:
old_status = self._status
self._status = value
self._touch()
- task_status_changed_counter.add(
- 1, {"old_status": old_status, "new_status": value}
- )
+ task_status_changed_counter.add(1, {"old_status": old_status, "new_status": value})
logger.info("Task %s status changed from %s to %s", self.hash, old_status, value)
@property
@@ -233,7 +233,7 @@ def approval(self, value: bool) -> None:
logger.info("Task %s approval set to %s", self.hash, value)
@property
- def additional(self) -> Dict[str, str]:
+ def additional(self) -> dict[str, str]:
"""Additional data dictionary (copy)."""
return self._additional.copy()
@@ -250,7 +250,7 @@ def set_additional(self, key: str, value: str) -> None:
# ----------------------------------------------------------------------
@property
- def cap_tasks(self) -> List[CapTask]:
+ def cap_tasks(self) -> list[CapTask]:
"""List of CapTasks associated with this task (copy)."""
return self._cap_tasks.copy()
@@ -307,12 +307,12 @@ def hash(self) -> str:
return hasher.hexdigest()
@property
- def previous_hash(self) -> Optional[str]:
+ def previous_hash(self) -> str | None:
"""Hash of the previous task in the chain."""
return self._previous_hash
@previous_hash.setter
- def previous_hash(self, value: Optional[str]) -> None:
+ def previous_hash(self, value: str | None) -> None:
"""Set the previous hash (for hash chain linking)."""
self._previous_hash = value
self._touch()
@@ -334,7 +334,7 @@ def verify_hash_chain(self) -> bool:
# ----------------------------------------------------------------------
@property
- def sub_tasks(self) -> List["Task"]:
+ def sub_tasks(self) -> list["Task"]:
"""List of subtasks (direct children)."""
return self._sub_tasks.copy()
@@ -360,7 +360,7 @@ def add_subtask(self, subtask: "Task") -> None:
subtask_added_counter.add(1)
logger.info("Subtask %s added to task %s", subtask.hash, self.hash)
- def get_siblings(self) -> List["Task"]:
+ def get_siblings(self) -> list["Task"]:
"""
Return a list of sibling tasks (other subtasks of the same parent).
If this task has no parent, returns an empty list.
@@ -369,7 +369,7 @@ def get_siblings(self) -> List["Task"]:
return []
return [t for t in self._parent._sub_tasks if t is not self]
- def get_all_descendants(self) -> List["Task"]:
+ def get_all_descendants(self) -> list["Task"]:
"""Return a flattened list of all subtasks recursively."""
descendants = []
for child in self._sub_tasks:
@@ -393,7 +393,7 @@ def updated_at(self) -> datetime:
def _touch(self) -> None:
"""Update the updated_at timestamp to now."""
- self._updated_at = datetime.now(timezone.utc)
+ self._updated_at = datetime.now(UTC)
@tracer.start_as_current_span("Task.get_latest_status")
def get_latest_status(self) -> str:
@@ -424,7 +424,7 @@ def get_latest_status(self) -> str:
# Serialization (JSON / YAML) - Updated to include CapTasks
# ----------------------------------------------------------------------
- def to_dict(self) -> Dict[str, Any]:
+ def to_dict(self) -> dict[str, Any]:
"""Convert task to a dictionary suitable for serialization."""
return {
"system_prompt": self._system_prompt,
@@ -435,13 +435,13 @@ def to_dict(self) -> Dict[str, Any]:
"additional": self._additional,
"previous_hash": self._previous_hash,
"sub_tasks": [st.to_dict() for st in self._sub_tasks],
- "cap_tasks": [ct.to_dict() for ct in self._cap_tasks], # NEW
+ "cap_tasks": [ct.to_dict() for ct in self._cap_tasks], # NEW
"created_at": self._created_at.isoformat(),
"updated_at": self._updated_at.isoformat(),
}
@classmethod
- def from_dict(cls, data: Dict[str, Any]) -> "Task":
+ def from_dict(cls, data: dict[str, Any]) -> "Task":
"""Create a Task instance from a dictionary."""
# Recursively create subtasks
sub_tasks = [cls.from_dict(st) for st in data.get("sub_tasks", [])]
@@ -468,7 +468,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Task":
task._updated_at = datetime.fromisoformat(data["updated_at"])
return task
- def to_json(self, indent: Optional[int] = None) -> str:
+ def to_json(self, indent: int | None = None) -> str:
"""Serialize task to JSON string."""
return json.dumps(self.to_dict(), indent=indent)
@@ -546,6 +546,7 @@ def __str__(self) -> str:
# Add a CapTask (requires actual CapTask class; here we simulate)
from scl.meta.captask import CapTask
+
cap = CapTask(cap_name="send_email", args=["user@example.com", "Subject"])
root.add_cap_task(cap)
@@ -590,4 +591,4 @@ def __str__(self) -> str:
# Check latest status (LRU view)
print(task.get_latest_status())
- """
\ No newline at end of file
+ """
diff --git a/scl/test/__init__.py b/scl/otel/__init__.py
similarity index 100%
rename from scl/test/__init__.py
rename to scl/otel/__init__.py
diff --git a/scl/otel/init.py b/scl/otel/init.py
index 8a25de1..e8d0482 100644
--- a/scl/otel/init.py
+++ b/scl/otel/init.py
@@ -1,16 +1,19 @@
"""
When running in test, this module should ensure test script can be run without otel features.
"""
+
import logging
+
from opentelemetry._logs import set_logger_provider
-from opentelemetry.sdk.resources import SERVICE_NAME, Resource
+from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
-from opentelemetry.instrumentation.logging import LoggingInstrumentor
+from opentelemetry.sdk.resources import SERVICE_NAME, Resource
# OTLP gRPC Log Exporter
try:
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
+
OTLP_GRPC_LOG_AVAILABLE = True
except ImportError:
OTLP_GRPC_LOG_AVAILABLE = False
@@ -22,21 +25,25 @@
# 服务名称(可从 config 获取,若无则使用默认值)
SERVICE_NAME_VALUE = getattr(config, "service_name", "SCL")
+
def get_otlp_endpoint() -> str:
"""直接从 config 获取 OTLP 端点地址"""
return config.otlp_endpoint
+
def setup_logs():
"""初始化 Logs"""
resource = Resource(attributes={SERVICE_NAME: SERVICE_NAME_VALUE})
otlp_endpoint = get_otlp_endpoint()
-
+
logger_provider = LoggerProvider(resource=resource)
set_logger_provider(logger_provider)
-
+
if OTLP_GRPC_LOG_AVAILABLE:
# gRPC 导出器通常使用 4317 端口,若配置的是 4318 则替换端口
- log_endpoint = otlp_endpoint.replace(":4318", ":4317") if ":4318" in otlp_endpoint else otlp_endpoint
+ log_endpoint = (
+ otlp_endpoint.replace(":4318", ":4317") if ":4318" in otlp_endpoint else otlp_endpoint
+ )
log_exporter = OTLPLogExporter(endpoint=log_endpoint, insecure=True)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
else:
@@ -46,8 +53,8 @@ def setup_logs():
LoggingInstrumentor().instrument(set_logging_format=True)
handler = LoggingHandler(logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
-
+
# 日志级别直接从 config 获取
logging.getLogger().setLevel(config.log_level)
-
- return logger_provider
\ No newline at end of file
+
+ return logger_provider
diff --git a/scl/otel/metric_decorator.py b/scl/otel/metric_decorator.py
index 373031d..208bf77 100644
--- a/scl/otel/metric_decorator.py
+++ b/scl/otel/metric_decorator.py
@@ -1,20 +1,24 @@
import functools
import time
-from typing import Callable, Any
+from collections.abc import Callable
+from typing import Any
+
from .otel import cap_counts
+
def record_latency(histogram, key=None):
"""记录函数执行时间的装饰器"""
+
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
# 获取直方图(假设在闭包或全局可访问)
# 或者可以传递histogram对象
start_time = time.perf_counter()
-
+
try:
result = func(*args, **kwargs)
- #print(f"Function {func.__name__} returned {result}")
+ # print(f"Function {func.__name__} returned {result}")
# todo
if key is not None:
count = len(result) if result else 0
@@ -24,6 +28,7 @@ def wrapper(*args, **kwargs) -> Any:
end_time = time.perf_counter()
duration = end_time - start_time
histogram.record(duration, {"function": func.__name__})
-
+
return wrapper
- return decorator
\ No newline at end of file
+
+ return decorator
diff --git a/scl/otel/metrics.py b/scl/otel/metrics.py
index c275013..546f607 100644
--- a/scl/otel/metrics.py
+++ b/scl/otel/metrics.py
@@ -1,21 +1,24 @@
-import os
import logging
-import time
+import os
import random
-from typing import Iterable
+import time
+from collections.abc import Iterable
from opentelemetry import metrics
from opentelemetry.metrics import CallbackOptions, Observation
-from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.metrics import MeterProvider
-from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader, ConsoleMetricExporter
+from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
+from opentelemetry.sdk.resources import SERVICE_NAME, Resource
# 配置基础日志
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logging.basicConfig(
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+)
# OTLP HTTP Metric Exporter
try:
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
+
OTLP_HTTP_AVAILABLE = True
except ImportError:
OTLP_HTTP_AVAILABLE = False
@@ -24,6 +27,7 @@
# 尝试导入配置对象 (如果存在)
try:
from scl.config import config
+
HAS_CONFIG = True
except ImportError:
HAS_CONFIG = False
@@ -32,12 +36,14 @@
# 服务名称
SERVICE_NAME_VALUE = "SCL"
+
def get_otlp_endpoint() -> str:
"""获取 OTLP 端点地址,优先从 config 读取,否则从环境变量读取"""
- if HAS_CONFIG and hasattr(config, 'otlp_metrics_endpoint') and config.otlp_metrics_endpoint:
+ if HAS_CONFIG and hasattr(config, "otlp_metrics_endpoint") and config.otlp_metrics_endpoint:
return config.otlp_metrics_endpoint
return os.getenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://localhost:4318")
+
def setup_metrics():
"""初始化 Metrics 并创建自定义指标"""
logging.info("init metrics for SCL")
@@ -47,34 +53,34 @@ def setup_metrics():
if OTLP_HTTP_AVAILABLE:
metric_exporter = OTLPMetricExporter(
endpoint=f"{otlp_endpoint}",
- #endpoint="http://localhost:9090/api/v1/otlp/v1/metrics",
- headers={}
+ # endpoint="http://localhost:9090/api/v1/otlp/v1/metrics",
+ headers={},
)
- #endpoint=f"{otlp_endpoint}/v1/metrics")
+ # endpoint=f"{otlp_endpoint}/v1/metrics")
else:
metric_exporter = ConsoleMetricExporter()
-
+
metric_reader = PeriodicExportingMetricReader(metric_exporter)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
-
+
# 返回 Meter
meter = metrics.get_meter(__name__)
-
+
# ========== 应用特定指标 ==========
# 直方图
search_time_histogram = meter.create_histogram(
name="cap_search_time",
description="Time taken for search operations",
explicit_bucket_boundaries_advisory=[1.0, 5.0, 10.0],
- unit="s"
+ unit="s",
)
-
+
tool_execute_time_histogram = meter.create_histogram(
name="cap_execute_time",
description="Time taken for cap execution",
explicit_bucket_boundaries_advisory=[1.0, 5.0, 10.0],
- unit="s"
+ unit="s",
)
# 可观测计数器 (Gauge)
@@ -93,7 +99,7 @@ def observable_cap_gauge_func(options: CallbackOptions) -> Iterable[Observation]
name="cap_gauge",
callbacks=[observable_cap_gauge_func],
description="gauge related with cap",
- unit="1"
+ unit="1",
)
# 返回指标对象
@@ -114,12 +120,12 @@ def main():
"""
logging.info("正在初始化 Metrics...")
metrics_dict = setup_metrics()
-
+
# 解包指标对象
search_hist = metrics_dict["search_time_histogram"]
execute_hist = metrics_dict["tool_execute_time_histogram"]
cap_counts = metrics_dict["cap_counts"]
-
+
logging.info("Metrics 初始化完成,开始模拟数据...")
logging.info(f"OTLP 端点: {get_otlp_endpoint()}")
logging.info("提示:程序将运行 60 秒,期间会持续更新指标。按 Ctrl+C 可提前终止。")
@@ -130,41 +136,41 @@ def main():
try:
while time.time() - start_time < 60:
iteration += 1
-
+
# 1. 模拟搜索耗时 (直方图)
search_duration = random.uniform(0.5, 8.0)
search_hist.record(search_duration)
-
+
# 2. 模拟执行耗时
execute_duration = random.uniform(0.2, 6.0)
execute_hist.record(execute_duration)
-
+
# 3. 更新 Gauge 字典 (cap_counts)
cap_counts["search"] = random.randint(0, 100)
cap_counts["total"] = cap_counts["search"] + random.randint(0, 50)
cap_counts["duplicate"] = random.randint(0, 20)
cap_counts["hit"] = random.randint(0, cap_counts["search"])
-
+
# 4. 计数器增量
- processed_ctr.add(random.randint(1, 10))
-
+ processed_ctr.add(random.randint(1, 10)) # noqa: F821 - TODO: processed_ctr undefined in this demo loop
+
if iteration % 10 == 0:
logging.info(
f"迭代 {iteration}: "
f"search={cap_counts['search']}, total={cap_counts['total']}, "
f"duplicate={cap_counts['duplicate']}, hit={cap_counts['hit']}"
)
-
+
# 模拟业务间隔
time.sleep(2)
-
+
except KeyboardInterrupt:
logging.info("调试被用户中断。")
-
+
logging.info("调试结束,Metrics 导出周期为 60 秒,请等待最后一个周期输出。")
# 给导出器一点时间完成最后一次导出
time.sleep(5)
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
diff --git a/scl/otel/otel.py b/scl/otel/otel.py
index cd38950..297967f 100644
--- a/scl/otel/otel.py
+++ b/scl/otel/otel.py
@@ -3,8 +3,10 @@
提供统一的入口来获取 Tracer、Meter 和预定义的指标对象。
真实遥测初始化请调用 init_telemetry()。
"""
+
import logging
-from opentelemetry import trace, metrics
+
+from opentelemetry import metrics, trace
# ---------- 获取全局代理 Tracer / Meter ----------
# 此时尚未设置真实的 Provider,获取到的是 No-op 代理对象
@@ -13,23 +15,13 @@
# ---------- 预创建常用指标对象(基于代理 Meter,安全且始终可用)----------
search_time_histogram = meter.create_histogram(
- name="search_time",
- description="Time taken for search operations",
- unit="ms"
+ name="search_time", description="Time taken for search operations", unit="ms"
)
tool_execute_time_histogram = meter.create_histogram(
- name="tool_execute_time",
- description="Time taken for tool execution",
- unit="ms"
-)
-cap_gauge = meter.create_gauge(
- name="cap_gauge",
- description="Current capacity gauge"
-)
-cap_counts = meter.create_counter(
- name="cap_counts",
- description="Count of capacity changes"
+ name="tool_execute_time", description="Time taken for tool execution", unit="ms"
)
+cap_gauge = meter.create_gauge(name="cap_gauge", description="Current capacity gauge")
+cap_counts = meter.create_counter(name="cap_counts", description="Count of capacity changes")
# 向后兼容:导出一个类似 metrics_result 的字典
metrics_result = {
@@ -40,6 +32,7 @@
"cap_counts": cap_counts,
}
+
# ---------- 显式初始化函数 ----------
def init_telemetry():
"""
@@ -47,17 +40,17 @@ def init_telemetry():
应在应用启动时调用(例如在 main 函数、Django AppConfig.ready 或环境变量控制下调用)。
测试环境中无需调用此函数。
"""
- from scl.otel.traces import setup_traces
- from scl.otel.metrics import setup_metrics
from scl.otel.init import setup_logs
+ from scl.otel.metrics import setup_metrics
+ from scl.otel.traces import setup_traces
# 注意:setup_traces 和 setup_metrics 内部应调用 trace.set_tracer_provider / metrics.set_meter_provider
# 如果它们原来返回了一些对象,我们可以忽略或仅用于内部配置
setup_traces()
- setup_metrics() # 假设该函数内部只负责设置 MeterProvider,不再重复创建指标
- setup_logs() # 你现有的日志初始化函数
+ setup_metrics() # 假设该函数内部只负责设置 MeterProvider,不再重复创建指标
+ setup_logs() # 你现有的日志初始化函数
# 可选:如果 setup_metrics 返回了新创建的指标对象,可以更新全局变量以保证完全一致
# 但由于全局变量已基于代理创建,Provider 切换后它们会自动生效,通常无需额外操作。
- logging.getLogger(__name__).info("OpenTelemetry initialized (real providers set).")
\ No newline at end of file
+ logging.getLogger(__name__).info("OpenTelemetry initialized (real providers set).")
diff --git a/scl/otel/traces.py b/scl/otel/traces.py
index 177b6f3..3f6b66a 100644
--- a/scl/otel/traces.py
+++ b/scl/otel/traces.py
@@ -1,5 +1,6 @@
-import os
import logging
+import os
+
from opentelemetry import trace
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider
@@ -8,6 +9,7 @@
# OTLP HTTP Trace Exporter
try:
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
+
OTLP_HTTP_AVAILABLE = True
except ImportError:
OTLP_HTTP_AVAILABLE = False
@@ -16,6 +18,7 @@
# 尝试导入配置对象 (如果存在)
try:
from scl.config import config
+
HAS_CONFIG = True
except ImportError:
HAS_CONFIG = False
@@ -24,25 +27,27 @@
# 服务名称
SERVICE_NAME_VALUE = "SCL"
+
def get_otlp_endpoint() -> str:
"""获取 OTLP 端点地址,优先从 config 读取,否则从环境变量读取"""
- if HAS_CONFIG and hasattr(config, 'otlp_endpoint') and config.otlp_endpoint:
+ if HAS_CONFIG and hasattr(config, "otlp_endpoint") and config.otlp_endpoint:
return config.otlp_endpoint
return os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318")
+
def setup_traces():
"""初始化 Traces"""
resource = Resource(attributes={SERVICE_NAME: SERVICE_NAME_VALUE})
otlp_endpoint = get_otlp_endpoint()
-
+
tracer_provider = TracerProvider(resource=resource)
if OTLP_HTTP_AVAILABLE:
span_exporter = OTLPSpanExporter(endpoint=f"{otlp_endpoint}/v1/traces")
else:
span_exporter = ConsoleSpanExporter()
-
+
tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
trace.set_tracer_provider(tracer_provider)
-
+
# 返回 Tracer
return trace.get_tracer(__name__)
diff --git a/scl/test/capabilities/__init__.py b/scl/processor/__init__.py
similarity index 100%
rename from scl/test/capabilities/__init__.py
rename to scl/processor/__init__.py
diff --git a/scl/processor/awaitCapTasksProcessor.py b/scl/processor/await_cap_tasks_processor.py
similarity index 86%
rename from scl/processor/awaitCapTasksProcessor.py
rename to scl/processor/await_cap_tasks_processor.py
index f96c0c2..31cc279 100644
--- a/scl/processor/awaitCapTasksProcessor.py
+++ b/scl/processor/await_cap_tasks_processor.py
@@ -1,5 +1,5 @@
"""
-awaitCapTasksProcessor module
+await_cap_tasks_processor module
Design Goals & Features:
- Inherits from BaseQueueProcessor for common loop/backoff/status/notify.
@@ -20,16 +20,16 @@
import logging
import os
import shutil
-from typing import Optional
from opentelemetry import trace
-from scl.otel.otel import meter, tracer
-# Project imports
-from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
from scl.meta.task import Task
+from scl.otel.otel import meter, tracer
from scl.processor.base_queue_processor import BaseQueueProcessor
+# Project imports
+from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
+
logger = logging.getLogger(__name__)
@@ -52,7 +52,7 @@ def __init__(
source_queue: AwaitingCapTasksQueue,
waiting_captask_dir: str,
file_watch_dir: str,
- name: Optional[str] = None
+ name: str | None = None,
):
"""
Initialize the processor.
@@ -75,22 +75,22 @@ def __init__(
# Additional metrics beyond the base class
self.files_moved_counter = meter.create_counter(
f"{self.name}.files_moved",
- description="Number of Task files moved from waitingCapTask to file_watch directory"
+ description="Number of Task files moved from waitingCapTask to file_watch directory",
)
self.file_move_errors_counter = meter.create_counter(
f"{self.name}.file_move_errors",
- description="Number of errors encountered while moving Task files"
+ description="Number of errors encountered while moving Task files",
)
self.tasks_requeued_counter = meter.create_counter(
f"{self.name}.tasks_requeued",
- description="Number of Task instances put back into the queue for retry"
+ description="Number of Task instances put back into the queue for retry",
)
logger.info("%s initialized with queue %r", self.name, source_queue)
# ------------------------------------------------------------------ Abstract method overrides
@tracer.start_as_current_span("AwaitCapTasksProcessor._get_item")
- def _get_item(self) -> Optional[Task]:
+ def _get_item(self) -> Task | None:
"""
Fetch one Task from the AwaitingCapTasksQueue.
Must return None if no item is available (empty queue).
@@ -135,15 +135,26 @@ def _process_item(self, item: Task) -> None:
)
current_span.set_attribute("task.requeued", True)
except Exception as e:
- logger.error("%s: failed to process Task %s: %s", self.name, item.hash, e, exc_info=True)
+ logger.error(
+ "%s: failed to process Task %s: %s", self.name, item.hash, e, exc_info=True
+ )
current_span.record_exception(e)
current_span.set_status(trace.Status(trace.StatusCode.ERROR, "Task processing failed"))
# Attempt to put back into source queue to avoid losing the task
try:
self.source_queue.push(item)
- logger.warning("%s: Task %s put back into source queue after processing error", self.name, item.hash)
+ logger.warning(
+ "%s: Task %s put back into source queue after processing error",
+ self.name,
+ item.hash,
+ )
except Exception as push_error:
- logger.critical("%s: failed to requeue Task %s after error: %s", self.name, item.hash, push_error)
+ logger.critical(
+ "%s: failed to requeue Task %s after error: %s",
+ self.name,
+ item.hash,
+ push_error,
+ )
# ------------------------------------------------------------------ Helper methods
def _all_captasks_completed(self, task: Task) -> bool:
@@ -172,7 +183,9 @@ def _move_completed_file(self, task_hash: str, span: trace.Span) -> None:
error_msg = f"Expected file {src_path} not found for completed Task {task_hash}"
logger.error("%s: %s", self.name, error_msg)
span.set_status(trace.Status(trace.StatusCode.ERROR, error_msg))
- self.file_move_errors_counter.add(1, {"processor.name": self.name, "error": "file_not_found"})
+ self.file_move_errors_counter.add(
+ 1, {"processor.name": self.name, "error": "file_not_found"}
+ )
# Task considered processed but file is missing
return
@@ -181,13 +194,18 @@ def _move_completed_file(self, task_hash: str, span: trace.Span) -> None:
self.files_moved_counter.add(1, {"processor.name": self.name})
logger.info(
"%s: moved completed Task file %s from %s to %s",
- self.name, filename, self.waiting_captask_dir, self.file_watch_dir
+ self.name,
+ filename,
+ self.waiting_captask_dir,
+ self.file_watch_dir,
)
span.set_attribute("file.moved", True)
except Exception as e:
logger.error("%s: failed to move file %s to %s: %s", self.name, src_path, dst_path, e)
span.record_exception(e)
- self.file_move_errors_counter.add(1, {"processor.name": self.name, "error": "move_failed"})
+ self.file_move_errors_counter.add(
+ 1, {"processor.name": self.name, "error": "move_failed"}
+ )
raise
@@ -200,8 +218,8 @@ def _move_completed_file(self, task_hash: str, span: trace.Span) -> None:
"""
Example usage:
- from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
- from scl.processor.awaitCapTasksProcessor import AwaitCapTasksProcessor
+ from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
+ from scl.processor.await_cap_tasks_processor import AwaitCapTasksProcessor
# Setup queue and folders
source_queue = AwaitingCapTasksQueue()
@@ -224,4 +242,4 @@ def _move_completed_file(self, task_hash: str, span: trace.Span) -> None:
# Graceful shutdown
processor.stop()
-"""
\ No newline at end of file
+"""
diff --git a/scl/processor/awaitingApproveProcessor.py b/scl/processor/awaiting_approve_processor.py
similarity index 81%
rename from scl/processor/awaitingApproveProcessor.py
rename to scl/processor/awaiting_approve_processor.py
index 6dffecf..f88b3eb 100644
--- a/scl/processor/awaitingApproveProcessor.py
+++ b/scl/processor/awaiting_approve_processor.py
@@ -1,5 +1,5 @@
"""
-awaitingApproveProcessor Module
+awaiting_approve_processor Module
Design Goals & Features:
- Inherits from BaseQueueProcessor for common loop/backoff/status/notify.
@@ -19,15 +19,14 @@
import logging
import os
import shutil
-from typing import Optional, Union
from opentelemetry import trace
-from scl.otel.otel import meter, tracer
-from scl.processor.base_queue_processor import BaseQueueProcessor
-from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
-from scl.meta.task import Task
from scl.meta.captask import CapTask
+from scl.meta.task import Task
+from scl.otel.otel import meter, tracer
+from scl.processor.base_queue_processor import BaseQueueProcessor
+from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
logger = logging.getLogger(__name__)
@@ -48,7 +47,7 @@ def __init__(
source_queue: AwaitingApproveQueue,
waiting_approval_dir: str,
file_watch_dir: str,
- name: Optional[str] = None
+ name: str | None = None,
):
"""
Initialize the processor.
@@ -71,26 +70,26 @@ def __init__(
# Processor‑specific metrics
self.items_fetched_by_type_counter = meter.create_counter(
f"{self.name}.items_fetched_by_type",
- description="Number of items fetched from AwaitingApproveQueue, broken down by item type"
+ description="Number of items fetched from AwaitingApproveQueue, broken down by item type",
)
self.items_requeued_counter = meter.create_counter(
f"{self.name}.items_requeued",
- description="Number of items returned to AwaitingApproveQueue (not yet approved)"
+ description="Number of items returned to AwaitingApproveQueue (not yet approved)",
)
self.files_moved_counter = meter.create_counter(
f"{self.name}.files_moved",
- description="Number of files moved from waitingapproval to file_watch directory"
+ description="Number of files moved from waitingapproval to file_watch directory",
)
self.file_move_errors_counter = meter.create_counter(
f"{self.name}.file_move_errors",
- description="Number of errors encountered while moving files"
+ description="Number of errors encountered while moving files",
)
self.logger.info("AwaitingApproveProcessor '%s' initialized", self.name)
# ------------------------------------------------------------------ Core logic overrides
@tracer.start_as_current_span("AwaitingApproveProcessor._get_item")
- def _get_item(self) -> Optional[Union[Task, CapTask]]:
+ def _get_item(self) -> Task | CapTask | None:
"""
Fetch one item from the source AwaitingApproveQueue.
Returns None if the queue is empty or an error occurs.
@@ -100,12 +99,14 @@ def _get_item(self) -> Optional[Union[Task, CapTask]]:
try:
item = self.source_queue.get()
if item:
- item_hash = getattr(item, 'hash', 'unknown')
+ item_hash = getattr(item, "hash", "unknown")
item_type = type(item).__name__
span.set_attribute("item.available", True)
span.set_attribute("item.type", item_type)
span.set_attribute("item.hash", item_hash)
- self.items_fetched_by_type_counter.add(1, {"processor.name": self.name, "item_type": item_type})
+ self.items_fetched_by_type_counter.add(
+ 1, {"processor.name": self.name, "item_type": item_type}
+ )
self.logger.debug("'%s' consumed %s %s", self.name, item_type, item_hash)
else:
span.set_attribute("item.available", False)
@@ -116,14 +117,14 @@ def _get_item(self) -> Optional[Union[Task, CapTask]]:
return None
@tracer.start_as_current_span("AwaitingApproveProcessor._process_item")
- def _process_item(self, item: Union[Task, CapTask]) -> None:
+ def _process_item(self, item: Task | CapTask) -> None:
"""
Process a consumed item.
If approval is True, move the corresponding file from waiting_approval_dir
to file_watch_dir; otherwise, return it to the source queue.
"""
span = trace.get_current_span()
- item_hash = getattr(item, 'hash', None)
+ item_hash = getattr(item, "hash", None)
if item_hash is None:
self.logger.error("Item missing 'hash' attribute, cannot process")
span.set_status(trace.Status(trace.StatusCode.ERROR, "Missing hash"))
@@ -139,8 +140,15 @@ def _process_item(self, item: Union[Task, CapTask]) -> None:
if not item.approval:
# Not yet approved: return to source queue
self.source_queue.add(item)
- self.items_requeued_counter.add(1, {"processor.name": self.name, "item_type": item_type})
- self.logger.debug("'%s' returned unapproved %s %s to source queue", self.name, item_type, item_hash)
+ self.items_requeued_counter.add(
+ 1, {"processor.name": self.name, "item_type": item_type}
+ )
+ self.logger.debug(
+ "'%s' returned unapproved %s %s to source queue",
+ self.name,
+ item_type,
+ item_hash,
+ )
span.set_attribute("item.routed_to", "source_queue")
else:
# Approved: move file to file_watch_dir
@@ -153,9 +161,13 @@ def _process_item(self, item: Union[Task, CapTask]) -> None:
# Attempt to put back into source queue to avoid losing the item
try:
self.source_queue.add(item)
- self.logger.warning("%s %s put back into source queue after processing error", item_type, item_hash)
+ self.logger.warning(
+ "%s %s put back into source queue after processing error", item_type, item_hash
+ )
except Exception as push_error:
- self.logger.critical("Failed to requeue %s %s after error: %s", item_type, item_hash, push_error)
+ self.logger.critical(
+ "Failed to requeue %s %s after error: %s", item_type, item_hash, push_error
+ )
# ------------------------------------------------------------------ File handling
def _move_approved_file(self, item_hash: str, item_type: str, span: trace.Span) -> None:
@@ -173,7 +185,9 @@ def _move_approved_file(self, item_hash: str, item_type: str, span: trace.Span)
error_msg = f"Expected file {src_path} not found for approved {item_type} {item_hash}"
self.logger.error(error_msg)
span.set_status(trace.Status(trace.StatusCode.ERROR, error_msg))
- self.file_move_errors_counter.add(1, {"processor.name": self.name, "error": "file_not_found"})
+ self.file_move_errors_counter.add(
+ 1, {"processor.name": self.name, "error": "file_not_found"}
+ )
# Still consider the item processed, but the file is missing.
# Could be a race condition or manual cleanup; log and continue.
return
@@ -183,21 +197,27 @@ def _move_approved_file(self, item_hash: str, item_type: str, span: trace.Span)
self.files_moved_counter.add(1, {"processor.name": self.name, "item_type": item_type})
self.logger.info(
"'%s' moved approved %s file %s from %s to %s",
- self.name, item_type, filename, self.waiting_approval_dir, self.file_watch_dir
+ self.name,
+ item_type,
+ filename,
+ self.waiting_approval_dir,
+ self.file_watch_dir,
)
span.set_attribute("file.moved", True)
except Exception as e:
self.logger.error("Failed to move file %s to %s: %s", src_path, dst_path, e)
span.record_exception(e)
- self.file_move_errors_counter.add(1, {"processor.name": self.name, "error": "move_failed"})
+ self.file_move_errors_counter.add(
+ 1, {"processor.name": self.name, "error": "move_failed"}
+ )
raise
# ------------------------------------------------------------------ Example usage
"""
Example usage:
- from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
- from scl.processor.awaitingApproveProcessor import AwaitingApproveProcessor
+ from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
+ from scl.processor.awaiting_approve_processor import AwaitingApproveProcessor
# Setup queue and folders
approve_queue = AwaitingApproveQueue()
@@ -227,4 +247,4 @@ def _move_approved_file(self, item_hash: str, item_type: str, span: trace.Span)
# - Configurable wait parameters (initial wait, max wait, idle threshold).
# - Dead-letter handling for files that repeatedly fail to move.
# - Batch processing support.
-# - Integration with external health checks.
\ No newline at end of file
+# - Integration with external health checks.
diff --git a/scl/processor/base_queue_processor.py b/scl/processor/base_queue_processor.py
index 6ea4a49..0f3f6f6 100644
--- a/scl/processor/base_queue_processor.py
+++ b/scl/processor/base_queue_processor.py
@@ -17,12 +17,12 @@
import abc
import logging
import threading
-import time
-from typing import Any, Optional
+from typing import Any
-from scl.otel.otel import tracer, meter
from opentelemetry import trace
+from scl.otel.otel import meter, tracer
+
class BaseQueueProcessor(abc.ABC):
"""
@@ -50,32 +50,31 @@ def _process_item(self, item):
def __init__(
self,
name: str,
- logger_name: Optional[str] = None,
+ logger_name: str | None = None,
):
self.name = name
self.logger = logging.getLogger(logger_name or __name__)
# Wait‑time / backoff parameters
- self._wait_time = 1.0 # seconds
- self._max_wait = 300.0 # 5 minutes
- self._idle_threshold = 16.0 # status becomes idle after this many seconds
+ self._wait_time = 1.0 # seconds
+ self._max_wait = 300.0 # 5 minutes
+ self._idle_threshold = 16.0 # status becomes idle after this many seconds
# Control flags
self._running = False
- self._thread: Optional[threading.Thread] = None
+ self._thread: threading.Thread | None = None
self._wakeup_event = threading.Event() # triggered by notify()
# ----------- Common metrics -----------
self.items_consumed_counter = meter.create_counter(
- f"{self.name}.items_consumed",
- description="Total items consumed from the queue"
+ f"{self.name}.items_consumed", description="Total items consumed from the queue"
)
# ObservableGauge reports the current idle status (1 = idle, 0 = normal).
self.idle_gauge = meter.create_observable_gauge(
f"{self.name}.idle",
description="Current idle status (1=idle, 0=normal)",
- callbacks=[self._idle_gauge_callback]
+ callbacks=[self._idle_gauge_callback],
)
self._idle_value = 0
@@ -105,7 +104,7 @@ def stop(self) -> None:
if not self._running:
return
self._running = False
- self._wakeup_event.set() # interrupt sleep
+ self._wakeup_event.set() # interrupt sleep
if self._thread:
self._thread.join(timeout=2.0)
self.logger.info("%s stopped.", self.name)
@@ -142,7 +141,7 @@ def _consume_loop(self) -> None:
# ------------------------------------------------------------------ Abstract methods
@abc.abstractmethod
- def _get_item(self) -> Optional[Any]:
+ def _get_item(self) -> Any | None:
"""
Fetch one item from the queue. Must return None if no item is available.
"""
@@ -210,4 +209,4 @@ def _process_item(self, item):
proc.notify()
# cleanup
proc.stop()
-"""
\ No newline at end of file
+"""
diff --git a/scl/processor/capTask_processor.py b/scl/processor/cap_task_processor.py
similarity index 93%
rename from scl/processor/capTask_processor.py
rename to scl/processor/cap_task_processor.py
index d05f27c..67959a3 100644
--- a/scl/processor/capTask_processor.py
+++ b/scl/processor/cap_task_processor.py
@@ -29,20 +29,19 @@
pip install opentelemetry-api opentelemetry-sdk
(optional) pip install scl-coretools
"""
+
import logging
import os
import shutil
from pathlib import Path
-from typing import Optional, Any
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
-from scl.processor.base_queue_processor import BaseQueueProcessor
-from scl.meta.capability import Capability
-from scl.meta.captask import CapTask
-from scl.queue.capTaskQueues import CapabilityTaskQueues
from scl.config import config
+from scl.meta.captask import CapTask
+from scl.otel.otel import meter, tracer
+from scl.processor.base_queue_processor import BaseQueueProcessor
+from scl.queue.cap_task_queues import CapabilityTaskQueues
# ----------------------------------------------------------------------
# Directory setup
@@ -57,16 +56,14 @@
# ----------------------------------------------------------------------
# Common metrics
tasks_processed_counter = meter.create_counter(
- "capability_processor.tasks_processed",
- description="Total CapTask instances processed"
+ "capability_processor.tasks_processed", description="Total CapTask instances processed"
)
tasks_succeeded_counter = meter.create_counter(
"capability_processor.tasks_succeeded",
- description="Tasks that completed successfully (status set to Processed)"
+ description="Tasks that completed successfully (status set to Processed)",
)
tasks_failed_counter = meter.create_counter(
- "capability_processor.tasks_failed",
- description="Tasks that failed with an error"
+ "capability_processor.tasks_failed", description="Tasks that failed with an error"
)
@@ -82,12 +79,7 @@ class CapabilityProcessor(BaseQueueProcessor):
# processor = CapabilityProcessor(name="greet", queue=queues, cap_registry=reg)
# processor.start()
- def __init__(
- self,
- name: str,
- queue: CapabilityTaskQueues,
- cap_registry: object
- ):
+ def __init__(self, name: str, queue: CapabilityTaskQueues, cap_registry: object):
"""
Args:
name: The capability name this processor handles.
@@ -123,7 +115,7 @@ def stop(self):
# ------------------------------------------------------------------ Abstract implementations
@tracer.start_as_current_span("CapabilityProcessor._get_item")
- def _get_item(self) -> Optional[CapTask]:
+ def _get_item(self) -> CapTask | None:
"""Fetch one CapTask from the queue. Returns None if no task is available."""
span = trace.get_current_span()
span.set_attribute("processor.name", self.name)
@@ -173,7 +165,9 @@ def _process_item(self, item: CapTask) -> None:
# Execute the capability (business logic)
result = capability.execute(item.args)
result_str = str(result) if result is not None else ""
- logger.debug("Capability executed for task %s, result length: %d", item.hash, len(result_str))
+ logger.debug(
+ "Capability executed for task %s, result length: %d", item.hash, len(result_str)
+ )
# Success path: update status and store result
# Note: CapTask uses "Processed" for successful completion, not "success"
@@ -232,7 +226,7 @@ def _safe_move(self, src: str, dst: str, context: str) -> None:
"""
Example usage (how other parts of the system invoke/reference this module):
- from scl.queue.capTaskQueues import CapabilityTaskQueues
+ from scl.queue.cap_task_queues import CapabilityTaskQueues
from scl.meta.captask import CapTask
from scl.cap_registry import CapRegistry
from scl.processor.capability_processor import CapabilityProcessor # this module
@@ -271,4 +265,4 @@ def execute(self, args):
# 6. (Optional) Stop the processor gracefully
processor.stop()
-"""
\ No newline at end of file
+"""
diff --git a/scl/processor/task_processor.py b/scl/processor/task_processor.py
index 89d72fc..31bb603 100644
--- a/scl/processor/task_processor.py
+++ b/scl/processor/task_processor.py
@@ -15,15 +15,12 @@
- The design goals are fully implemented; no missing features have been identified.
"""
-import logging
-from typing import Optional
-
from opentelemetry import trace
-from scl.processor.base_queue_processor import BaseQueueProcessor
-from scl.queue.taskQueue import TaskQueue
from scl.meta.task import Task # Task class definition
-from scl.otel.otel import tracer, meter
+from scl.otel.otel import meter, tracer
+from scl.processor.base_queue_processor import BaseQueueProcessor
+from scl.queue.task_queue import TaskQueue
class TaskProcessor(BaseQueueProcessor):
@@ -43,13 +40,13 @@ def __init__(self, input_queue: TaskQueue, name: str = "task_processor"):
# Metrics specific to task processing
self.processing_error_counter = meter.create_counter(
f"{self.name}.processing_errors",
- description="Number of errors while processing individual tasks"
+ description="Number of errors while processing individual tasks",
)
self.logger.info("TaskProcessor initialized and registered with queue")
# ------------------------------------------------------------------ Abstract method implementations
- def _get_item(self) -> Optional[Task]:
+ def _get_item(self) -> Task | None:
"""
Fetch one Task from the input queue without blocking.
Must return None if the queue is empty.
@@ -57,7 +54,7 @@ def _get_item(self) -> Optional[Task]:
try:
# Non‑blocking fetch so the base class can manage backoff/sleep
return self.input_queue.get(block=False)
- except Exception: # queue.Empty or equivalent
+ except Exception: # queue.Empty or equivalent
return None
@tracer.start_as_current_span("TaskProcessor._process_item")
@@ -68,8 +65,8 @@ def _process_item(self, item: Task) -> None:
handles exception logging / metric updates for completed items.
"""
current_span = trace.get_current_span()
- task_id = getattr(item, 'id', 'unknown')
- task_type = getattr(item, 'type', 'unknown')
+ task_id = getattr(item, "id", "unknown")
+ task_type = getattr(item, "type", "unknown")
current_span.set_attribute("task.id", str(task_id))
current_span.set_attribute("task.type", task_type)
@@ -80,7 +77,8 @@ def _process_item(self, item: Task) -> None:
# ---------- Actual task processing ----------
# Replace this placeholder with your business logic.
import time
- time.sleep(0.1) # simulate work
+
+ time.sleep(0.1) # simulate work
# -------------------------------------------
self.logger.debug("Task %s processed successfully", task_id)
except Exception as exc:
@@ -101,7 +99,7 @@ def notify(self) -> None:
with tracer.start_as_current_span("TaskProcessor.notify") as span:
status_before = self.status
span.set_attribute("processor.status_before", status_before)
- super().notify() # triggers wakeup if idle
+ super().notify() # triggers wakeup if idle
span.set_attribute("processor.status_after", self.status)
@@ -109,7 +107,7 @@ def notify(self) -> None:
"""
Example usage:
from scl.processor.task_processor import TaskProcessor
- from scl.queue.taskQueue import TaskQueue
+ from scl.queue.task_queue import TaskQueue
# Create a queue and a processor
queue = TaskQueue()
@@ -124,4 +122,4 @@ def notify(self) -> None:
# When done, stop the processor gracefully
processor.stop()
processor.join()
-"""
\ No newline at end of file
+"""
diff --git a/scl/test/listener/__init__.py b/scl/queue/__init__.py
similarity index 100%
rename from scl/test/listener/__init__.py
rename to scl/queue/__init__.py
diff --git a/scl/queue/awaitingApproveQueue.py b/scl/queue/awaiting_approve_queue.py
similarity index 93%
rename from scl/queue/awaitingApproveQueue.py
rename to scl/queue/awaiting_approve_queue.py
index 11289c9..5c0246a 100644
--- a/scl/queue/awaitingApproveQueue.py
+++ b/scl/queue/awaiting_approve_queue.py
@@ -23,36 +23,35 @@
import logging
import threading
from collections import deque
-from typing import Callable, Dict, List, Optional, Union
+from collections.abc import Callable
from opentelemetry import trace
-from scl.otel.otel import meter, tracer
-from scl.meta.task import Task
from scl.meta.captask import CapTask
+from scl.meta.task import Task
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Metrics
queue_add_counter = meter.create_counter(
"awaiting_approve_queue.added",
- description="Number of items added to the awaiting approve queue"
+ description="Number of items added to the awaiting approve queue",
)
queue_get_counter = meter.create_counter(
"awaiting_approve_queue.removed",
- description="Number of items removed from the awaiting approve queue via get()"
+ description="Number of items removed from the awaiting approve queue via get()",
)
queue_approve_counter = meter.create_counter(
- "awaiting_approve_queue.approved",
- description="Number of items approved (moved to front)"
+ "awaiting_approve_queue.approved", description="Number of items approved (moved to front)"
)
queue_size_gauge = meter.create_up_down_counter(
"awaiting_approve_queue.size",
- description="Current number of items in the awaiting approve queue"
+ description="Current number of items in the awaiting approve queue",
)
notifier_invoked_counter = meter.create_counter(
"awaiting_approve_queue.notifier_invoked",
- description="Number of times the registered notifier was invoked"
+ description="Number of times the registered notifier was invoked",
)
@@ -97,9 +96,9 @@ def on_approved_item_moved():
def __init__(self):
"""Initialize the queue and internal structures."""
self._deque: deque = deque()
- self._items_by_hash: Dict[str, Union[Task, CapTask]] = {}
+ self._items_by_hash: dict[str, Task | CapTask] = {}
self._lock = threading.RLock()
- self._notifier: Optional[Callable[[], None]] = None
+ self._notifier: Callable[[], None] | None = None
logger.info("AwaitingApproveQueue initialized")
@@ -127,7 +126,7 @@ def _invoke_notifier(self) -> None:
logger.error(f"Notifier raised an exception: {e}", exc_info=True)
@tracer.start_as_current_span("AwaitingApproveQueue.add")
- def add(self, item: Union[Task, CapTask]) -> bool:
+ def add(self, item: Task | CapTask) -> bool:
"""
Add a Task or CapTask to the queue if its approval is False.
Items with approval=True are ignored.
@@ -139,7 +138,7 @@ def add(self, item: Union[Task, CapTask]) -> bool:
True if the item was added, False otherwise.
"""
current_span = trace.get_current_span()
- item_hash = getattr(item, 'hash', None)
+ item_hash = getattr(item, "hash", None)
item_type = type(item).__name__
current_span.set_attribute("item.type", item_type)
current_span.set_attribute("item.hash", item_hash)
@@ -168,7 +167,7 @@ def add(self, item: Union[Task, CapTask]) -> bool:
return True
@tracer.start_as_current_span("AwaitingApproveQueue.get")
- def get(self) -> Optional[Union[Task, CapTask]]:
+ def get(self) -> Task | CapTask | None:
"""
Remove and return the item at the front of the queue.
Typically this will be an approved item waiting to be processed.
@@ -185,7 +184,7 @@ def get(self) -> Optional[Union[Task, CapTask]]:
return None
item = self._deque.popleft()
- item_hash = getattr(item, 'hash', None)
+ item_hash = getattr(item, "hash", None)
del self._items_by_hash[item_hash]
item_type = type(item).__name__
@@ -242,7 +241,7 @@ def approve(self, item_hash: str) -> bool:
self._invoke_notifier()
return True
- def get_pending_items(self) -> List[Union[Task, CapTask]]:
+ def get_pending_items(self) -> list[Task | CapTask]:
"""
Return a list of all items currently in the queue (pending approval).
Human operators can randomly pick from this list to approve.
@@ -268,4 +267,4 @@ def contains(self, item_hash: str) -> bool:
# - Support for batch approval.
# - Persistent storage of queue state.
# - Priority levels beyond the approved-to-front mechanism.
-# - Configurable notification throttling.
\ No newline at end of file
+# - Configurable notification throttling.
diff --git a/scl/queue/awaitingCapTasksQueue.py b/scl/queue/awaiting_cap_tasks_queue.py
similarity index 93%
rename from scl/queue/awaitingCapTasksQueue.py
rename to scl/queue/awaiting_cap_tasks_queue.py
index ef5eeac..88c33df 100644
--- a/scl/queue/awaitingCapTasksQueue.py
+++ b/scl/queue/awaiting_cap_tasks_queue.py
@@ -25,36 +25,32 @@
import heapq
import logging
import threading
-from typing import Callable, List, Optional, Set, Tuple
+from collections.abc import Callable
from opentelemetry import trace
-from scl.otel.otel import meter, tracer
from scl.meta.task import Task
-from scl.meta.captask import CapTask
+from scl.otel.otel import meter, tracer
logger = logging.getLogger(__name__)
# Metrics
heap_push_counter = meter.create_counter(
- "awaiting_queue.task_pushed",
- description="Number of tasks pushed to the awaiting heap"
+ "awaiting_queue.task_pushed", description="Number of tasks pushed to the awaiting heap"
)
heap_pop_counter = meter.create_counter(
- "awaiting_queue.task_popped",
- description="Number of tasks popped from the awaiting heap"
+ "awaiting_queue.task_popped", description="Number of tasks popped from the awaiting heap"
)
heap_remove_counter = meter.create_counter(
"awaiting_queue.task_removed",
- description="Number of tasks explicitly removed from the awaiting heap"
+ description="Number of tasks explicitly removed from the awaiting heap",
)
heap_size_gauge = meter.create_up_down_counter(
- "awaiting_queue.size",
- description="Current number of tasks in the awaiting heap"
+ "awaiting_queue.size", description="Current number of tasks in the awaiting heap"
)
notifier_invoked_counter = meter.create_counter(
"awaiting_queue.notifier_invoked",
- description="Number of times the registered notifier was invoked"
+ description="Number of times the registered notifier was invoked",
)
@@ -101,10 +97,10 @@ def my_notifier():
def __init__(self):
"""Initialize an empty heap with a lock for thread safety."""
- self._heap: List[Tuple[int, str, Task]] = []
- self._task_set: Set[str] = set() # Track task hashes for O(1) membership check
+ self._heap: list[tuple[int, str, Task]] = []
+ self._task_set: set[str] = set() # Track task hashes for O(1) membership check
self._lock = threading.RLock()
- self._notifier: Optional[Callable[[], None]] = None
+ self._notifier: Callable[[], None] | None = None
logger.info("AwaitingCapTasksQueue initialized")
@@ -183,7 +179,9 @@ def push(self, task: Task) -> bool:
self._task_set.add(task.hash)
heap_push_counter.add(1, {"priority": priority})
heap_size_gauge.add(1)
- logger.info(f"Task {task.hash} pushed to heap with priority {priority} (pending CapTasks)")
+ logger.info(
+ f"Task {task.hash} pushed to heap with priority {priority} (pending CapTasks)"
+ )
logger.debug(f"Heap size: {len(self._heap)}")
current_span.set_attribute("queue.pushed", True)
@@ -194,7 +192,7 @@ def push(self, task: Task) -> bool:
return True
@tracer.start_as_current_span("AwaitingCapTasksQueue.pop")
- def pop(self) -> Optional[Task]:
+ def pop(self) -> Task | None:
"""
Remove and return the Task with the smallest priority (fewest pending CapTasks).
If the heap is empty, returns None.
@@ -314,7 +312,7 @@ def update(self, task: Task) -> bool:
current_span.set_attribute("queue.updated", False)
return False
- def peek(self) -> Optional[Task]:
+ def peek(self) -> Task | None:
"""
Return the Task at the top of the heap without removing it.
@@ -341,4 +339,4 @@ def contains(self, task: Task) -> bool:
# - Persistent storage of heap state for crash recovery.
# - Batch operations for efficiency when updating many tasks at once.
# - Custom comparator support if Task priority model changes.
-# - Notification mechanism when a task becomes "all processed" (priority 0).
\ No newline at end of file
+# - Notification mechanism when a task becomes "all processed" (priority 0).
diff --git a/scl/queue/capTaskQueues.py b/scl/queue/cap_task_queues.py
similarity index 85%
rename from scl/queue/capTaskQueues.py
rename to scl/queue/cap_task_queues.py
index 75e569b..cee164f 100644
--- a/scl/queue/capTaskQueues.py
+++ b/scl/queue/cap_task_queues.py
@@ -20,37 +20,35 @@
Installation:
pip install opentelemetry-api opentelemetry-sdk
"""
+
import logging
from collections import defaultdict
+from collections.abc import Callable
from threading import Lock
-from typing import Optional, Any, Dict, List, Callable
# OpenTelemetry imports
from opentelemetry import trace
-from scl.otel.otel import tracer, meter
# Reference CapTask for type awareness
from scl.meta.captask import CapTask
+from scl.otel.otel import meter, tracer
# Setup logger
logger = logging.getLogger(__name__)
# Setup metrics
task_added_counter = meter.create_counter(
- "capability_task.added",
- description="Number of CapTasks added to CapabilityTaskQueues"
+ "capability_task.added", description="Number of CapTasks added to CapabilityTaskQueues"
)
task_consumed_counter = meter.create_counter(
- "capability_task.consumed",
- description="Number of CapTasks consumed from CapabilityTaskQueues"
+ "capability_task.consumed", description="Number of CapTasks consumed from CapabilityTaskQueues"
)
notifier_registered_counter = meter.create_counter(
- "capability_task.notifier.registered",
- description="Number of notifier functions registered"
+ "capability_task.notifier.registered", description="Number of notifier functions registered"
)
notifier_invoked_counter = meter.create_counter(
"capability_task.notifier.invoked",
- description="Number of times a notifier function was invoked"
+ description="Number of times a notifier function was invoked",
)
@@ -70,9 +68,9 @@ class CapabilityTaskQueues:
def __init__(self):
# Internal storage: dict of lists, each list holds CapTask objects for a capability name.
- self._queues: Dict[str, List[CapTask]] = defaultdict(list)
+ self._queues: dict[str, list[CapTask]] = defaultdict(list)
# Notifier functions per capability name: signature (name: str, task: CapTask) -> None
- self._notifiers: Dict[str, Callable[[str, CapTask], None]] = {}
+ self._notifiers: dict[str, Callable[[str, CapTask], None]] = {}
self._lock = Lock()
logger.info("CapabilityTaskQueues initialized")
@@ -140,7 +138,7 @@ def add(self, task: CapTask) -> None:
current_span.set_attribute("cap_task.hash", task.hash)
current_span.set_attribute("cap_task.args_count", len(task.args))
- notifier: Optional[Callable] = None
+ notifier: Callable | None = None
with self._lock:
self._queues[name].append(task)
task_added_counter.add(1, {"capability.name": name})
@@ -148,25 +146,31 @@ def add(self, task: CapTask) -> None:
# Retrieve notifier (if any) to invoke outside the lock to avoid deadlocks
notifier = self._notifiers.get(name)
- logger.debug(f"Added CapTask {task.hash} to queue for capability '{name}' (queue length: {queue_length})")
+ logger.debug(
+ f"Added CapTask {task.hash} to queue for capability '{name}' (queue length: {queue_length})"
+ )
# Invoke notifier if present, outside the lock for safety
if notifier:
try:
- with tracer.start_as_current_span("CapabilityTaskQueues.notifier.invoke") as notify_span:
+ with tracer.start_as_current_span(
+ "CapabilityTaskQueues.notifier.invoke"
+ ) as notify_span:
notify_span.set_attribute("capability.name", name)
notify_span.set_attribute("cap_task.hash", task.hash)
notifier_invoked_counter.add(1, {"capability.name": name})
logger.debug(f"Invoking notifier for capability '{name}' with task {task.hash}")
notifier(name, task)
except Exception as e:
- logger.error(f"Notifier for capability '{name}' raised an exception: {e}", exc_info=True)
+ logger.error(
+ f"Notifier for capability '{name}' raised an exception: {e}", exc_info=True
+ )
# Do not re-raise to avoid disrupting the add operation
current_span.record_exception(e)
current_span.set_attribute("capability.notifier.error", True)
@tracer.start_as_current_span("CapabilityTaskQueues.consume")
- def consume(self, name: str) -> Optional[CapTask]:
+ def consume(self, name: str) -> CapTask | None:
"""
Consume (pop) the oldest pending CapTask for the given capability name.
@@ -193,8 +197,10 @@ def consume(self, name: str) -> Optional[CapTask]:
if not queue:
del self._queues[name]
- logger.debug(f"Consumed CapTask {task.hash} for capability '{name}' (remaining: {remaining})")
+ logger.debug(
+ f"Consumed CapTask {task.hash} for capability '{name}' (remaining: {remaining})"
+ )
current_span.set_attribute("capability.task.available", True)
current_span.set_attribute("capability.task.remaining", remaining)
current_span.set_attribute("cap_task.hash", task.hash)
- return task
\ No newline at end of file
+ return task
diff --git a/scl/queue/taskQueue.py b/scl/queue/task_queue.py
similarity index 85%
rename from scl/queue/taskQueue.py
rename to scl/queue/task_queue.py
index b96fb3a..47e6e4f 100644
--- a/scl/queue/taskQueue.py
+++ b/scl/queue/task_queue.py
@@ -11,13 +11,13 @@
import logging
import queue
-from typing import Optional, TYPE_CHECKING
+from typing import TYPE_CHECKING
from opentelemetry import trace
from opentelemetry.metrics import Observation
-from scl.otel.otel import meter, tracer
from scl.meta.task import Task # Actual Task class import
+from scl.otel.otel import meter, tracer
if TYPE_CHECKING:
from scl.processor.task_processor import TaskProcessor
@@ -36,21 +36,19 @@ def __init__(self):
Initialize the queue, metrics, and processor reference.
"""
self._queue: queue.Queue[Task] = queue.Queue()
- self._processor: Optional["TaskProcessor"] = None
+ self._processor: TaskProcessor | None = None
# Metrics
self.task_enqueue_counter = meter.create_counter(
- "task_enqueue",
- description="Number of tasks added to the queue"
+ "task_enqueue", description="Number of tasks added to the queue"
)
self.task_dequeue_counter = meter.create_counter(
- "task_dequeue",
- description="Number of tasks removed from the queue"
+ "task_dequeue", description="Number of tasks removed from the queue"
)
self.queue_size_gauge = meter.create_observable_gauge(
"task_queue_size",
callbacks=[self._get_queue_size],
- description="Current number of tasks in the queue"
+ description="Current number of tasks in the queue",
)
logger.info("TaskQueue initialized")
@@ -80,8 +78,8 @@ def add(self, task: Task) -> None:
raise TypeError(f"Expected Task instance, got {type(task).__name__}")
current_span = trace.get_current_span()
- current_span.set_attribute("task.id", str(getattr(task, 'id', 'unknown')))
- current_span.set_attribute("task.type", getattr(task, 'type', 'unknown'))
+ current_span.set_attribute("task.id", str(getattr(task, "id", "unknown")))
+ current_span.set_attribute("task.type", getattr(task, "type", "unknown"))
self._queue.put(task)
self.task_enqueue_counter.add(1)
@@ -103,7 +101,7 @@ def add(self, task: Task) -> None:
logger.debug("No TaskProcessor registered; skipping notification")
@tracer.start_as_current_span("task_queue_get")
- def get(self, block: bool = True, timeout: Optional[float] = None) -> Optional[Task]:
+ def get(self, block: bool = True, timeout: float | None = None) -> Task | None:
"""
Retrieve a Task from the queue. Returns None if the queue is empty.
@@ -114,14 +112,16 @@ def get(self, block: bool = True, timeout: Optional[float] = None) -> Optional[T
current_span = trace.get_current_span()
try:
task = self._queue.get(block=block, timeout=timeout)
- current_span.set_attribute("task.id", str(getattr(task, 'id', 'unknown')))
- current_span.set_attribute("task.type", getattr(task, 'type', 'unknown'))
+ current_span.set_attribute("task.id", str(getattr(task, "id", "unknown")))
+ current_span.set_attribute("task.type", getattr(task, "type", "unknown"))
self.task_dequeue_counter.add(1)
queue_size = self._queue.qsize()
current_span.set_attribute("queue.size.after_get", queue_size)
- logger.debug(f"Task {getattr(task, 'id', '?')} retrieved from queue. Remaining size: {queue_size}")
+ logger.debug(
+ f"Task {getattr(task, 'id', '?')} retrieved from queue. Remaining size: {queue_size}"
+ )
return task
except queue.Empty:
@@ -131,4 +131,4 @@ def get(self, block: bool = True, timeout: Optional[float] = None) -> Optional[T
def qsize(self) -> int:
"""Return the current size of the queue (non-blocking)."""
- return self._queue.qsize()
\ No newline at end of file
+ return self._queue.qsize()
diff --git a/scl/storage/__init__.py b/scl/storage/__init__.py
index 44489ed..fec4c21 100644
--- a/scl/storage/__init__.py
+++ b/scl/storage/__init__.py
@@ -4,25 +4,28 @@
from .base import StoreBase
-__all__ = ['StoreBase']
+__all__ = ["StoreBase"]
# Import PgVectorStore (PostgreSQL with pgvector)
try:
from .pgstore import PgVectorStore
- __all__.append('PgVectorStore')
+
+ __all__.append("PgVectorStore")
except ImportError:
pass
# Import fsstore (File system storage)
try:
from .fsstore import fsstore
- __all__.append('fsstore')
+
+ __all__.append("fsstore")
except ImportError:
pass
# Import OceanBaseStore (OceanBase with pyobvector)
try:
from .oceanbasestore import OceanBaseStore
- __all__.append('OceanBaseStore')
+
+ __all__.append("OceanBaseStore")
except ImportError:
pass
diff --git a/scl/storage/base.py b/scl/storage/base.py
index ea110bd..4d7505c 100644
--- a/scl/storage/base.py
+++ b/scl/storage/base.py
@@ -14,7 +14,6 @@
"""
from abc import ABC, abstractmethod
-from typing import Dict
from scl.meta.capability import Capability
from scl.meta.msg import Msg
@@ -49,7 +48,7 @@ def get_cap_by_name(self, name: str) -> Capability:
@abstractmethod
def search_by_similarity(
self, msg: Msg, limit: int = 5, min_similarity: float = 0.5
- ) -> Dict[str, Capability]:
+ ) -> dict[str, Capability]:
"""
Semantic search for capabilities similar to the query embedding.
@@ -78,7 +77,7 @@ def record(self, msg: Msg, cap: Capability) -> None:
@abstractmethod
def getCapsByHistory(
self, msg: Msg, limit: int = 5, min_similarity: float = 0.5
- ) -> Dict[str, Capability]:
+ ) -> dict[str, Capability]:
"""
Retrieve capabilities based on usage history.
@@ -102,6 +101,7 @@ def insert_capability(self, cap: Capability) -> None:
"""
pass
+
"""
Example usage:
from scl.storage.fsstore import fsstore
@@ -128,4 +128,4 @@ def insert_capability(self, cap: Capability) -> None:
# History‑based recommendations (implementation dependent)
hist_results = store.getCapsByHistory(query, limit=2)
-"""
\ No newline at end of file
+"""
diff --git a/scl/storage/fsstore.py b/scl/storage/fsstore.py
index 5568487..26dcfd0 100644
--- a/scl/storage/fsstore.py
+++ b/scl/storage/fsstore.py
@@ -47,25 +47,23 @@
- Support for fuzzy name matching in get_cap_by_name.
"""
-from pathlib import Path
import logging
import pickle
-from typing import Dict, Optional, List, Tuple
+from pathlib import Path
import numpy as np
+from opentelemetry import trace
from rank_bm25 import BM25Okapi
+# Import the embedding service (singleton) and its global embed function
+from scl.embeddings.embedding import embed as generate_embedding
from scl.meta.capability import Capability
from scl.meta.functioncall import FunctionCall
+from scl.meta.msg import Msg
+from scl.meta.skill import Skill
from scl.meta.skills_ref.parser import read_properties
+from scl.otel.otel import meter, tracer
from scl.storage.base import StoreBase
-from scl.meta.skill import Skill
-from scl.otel.otel import tracer, meter
-from scl.meta.msg import Msg
-from opentelemetry import trace
-
-# Import the embedding service (singleton) and its global embed function
-from scl.embeddings.embedding import embed as generate_embedding
class fsstore(StoreBase):
@@ -73,9 +71,9 @@ def __init__(self, path: str, init: bool, embedding_service_on: bool = False):
super().__init__()
self.path = path
self.cache_file = Path(self.path) / ".Capability_cache.pkl"
- self._skill_embedding_cache: Dict[str, dict] = {}
+ self._skill_embedding_cache: dict[str, dict] = {}
self.embedding_service_on = embedding_service_on
- self.bm25: Optional[BM25Okapi] = None
+ self.bm25: BM25Okapi | None = None
# Instance logger (following the convention)
self.logger = logging.getLogger(__name__)
@@ -100,7 +98,7 @@ def __init__(self, path: str, init: bool, embedding_service_on: bool = False):
self._load_cache_from_disk()
self._rebuild_bm25()
- def cache(self) -> Dict[str, dict]:
+ def cache(self) -> dict[str, dict]:
"""Return the in‑memory skill embedding cache."""
return self._skill_embedding_cache
@@ -115,9 +113,7 @@ def load_skill(self, item: Path) -> None:
capability.embedding_description = generate_embedding(capability.description)
else:
capability.embedding_description = None
- self._skill_embedding_cache[str(item)] = {
- "Capability": capability
- }
+ self._skill_embedding_cache[str(item)] = {"Capability": capability}
except Exception as e:
self.logger.error("Error reading properties for %s: %s", item, e)
@@ -126,7 +122,9 @@ def _save_cache_to_disk(self) -> None:
try:
with open(self.cache_file, "wb") as f:
pickle.dump(self._skill_embedding_cache, f)
- self.logger.info("Cache saved to %s (entries: %d)", self.cache_file, len(self._skill_embedding_cache))
+ self.logger.info(
+ "Cache saved to %s (entries: %d)", self.cache_file, len(self._skill_embedding_cache)
+ )
except Exception as e:
self.logger.error("Error saving cache to disk: %s", e)
@@ -136,7 +134,11 @@ def _load_cache_from_disk(self) -> None:
try:
with open(self.cache_file, "rb") as f:
self._skill_embedding_cache = pickle.load(f)
- self.logger.info("Cache loaded from %s (entries: %d)", self.cache_file, len(self._skill_embedding_cache))
+ self.logger.info(
+ "Cache loaded from %s (entries: %d)",
+ self.cache_file,
+ len(self._skill_embedding_cache),
+ )
except Exception as e:
self.logger.error("Error loading cache from disk: %s", e)
@@ -177,12 +179,16 @@ def refresh_cache(self) -> None:
for item in dir_path.iterdir():
if item.is_dir():
self.load_skill(item)
- self.logger.info("Cache refresh completed, total skills: %d", len(self._skill_embedding_cache))
+ self.logger.info(
+ "Cache refresh completed, total skills: %d", len(self._skill_embedding_cache)
+ )
self._rebuild_bm25()
self._save_cache_to_disk()
@tracer.start_as_current_span("insert_capability")
- def insert_capability(self, capability: Capability, force: bool = False, similarity_threshold: float = 0.8) -> Capability:
+ def insert_capability(
+ self, capability: Capability, force: bool = False, similarity_threshold: float = 0.8
+ ) -> Capability:
"""
Add a new capability to the store.
Always checks uniqueness of name and description; raises ValueError if a duplicate exists.
@@ -208,9 +214,11 @@ def insert_capability(self, capability: Capability, force: bool = False, similar
self.insert_counter.add(1)
current_span = trace.get_current_span()
current_span.set_attribute("capability.name", capability.name)
- current_span.set_attribute("capability.description", capability.description[:100] if capability.description else "")
+ current_span.set_attribute(
+ "capability.description", capability.description[:100] if capability.description else ""
+ )
current_span.set_attribute("force", force)
-
+
# Uniqueness check on exact name and description
for data in self._skill_embedding_cache.values():
cur = data["Capability"]
@@ -241,13 +249,17 @@ def insert_capability(self, capability: Capability, force: bool = False, similar
for data in self._skill_embedding_cache.values():
cur = data["Capability"]
if cur.embedding_description is not None:
- sim = self.cosine_similarity(capability.embedding_description, cur.embedding_description)
+ sim = self.cosine_similarity(
+ capability.embedding_description, cur.embedding_description
+ )
if sim > max_sim:
max_sim = sim
most_similar_cap = cur
if max_sim >= similarity_threshold:
- msg = (f"Similar capability already exists (max cosine similarity {max_sim:.4f} >= threshold "
- f"{similarity_threshold}). Use force=True to insert anyway.")
+ msg = (
+ f"Similar capability already exists (max cosine similarity {max_sim:.4f} >= threshold "
+ f"{similarity_threshold}). Use force=True to insert anyway."
+ )
self.logger.error(msg)
current_span.set_attribute("error", True)
exc = ValueError(msg)
@@ -273,7 +285,7 @@ def insert_capability(self, capability: Capability, force: bool = False, similar
return capability
@tracer.start_as_current_span("get_cap_by_name")
- def get_cap_by_name(self, name: str) -> Optional[Capability]:
+ def get_cap_by_name(self, name: str) -> Capability | None:
"""Return a Capability by its exact name, or None if not found."""
self.get_name_counter.add(1)
current_span = trace.get_current_span()
@@ -312,8 +324,14 @@ def _tanh(scores: np.ndarray) -> np.ndarray:
return np.tanh(scores)
@tracer.start_as_current_span("search_by_similarity")
- def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float = 0.5,
- combine_method: Optional[str] = None, alpha: float = 0.5) -> Dict[str, Capability]:
+ def search_by_similarity(
+ self,
+ msg: Msg,
+ limit: int = 5,
+ min_similarity: float = 0.5,
+ combine_method: str | None = None,
+ alpha: float = 0.5,
+ ) -> dict[str, Capability]:
"""
Semantically search for capabilities.
@@ -343,10 +361,14 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
current_span.set_attribute("search.alpha", alpha)
# Collect all (path, Capability) pairs
- items: List[Tuple[str, Capability]] = [(path, data["Capability"]) for path, data in self._skill_embedding_cache.items()]
+ items: list[tuple[str, Capability]] = [
+ (path, data["Capability"]) for path, data in self._skill_embedding_cache.items()
+ ]
# Determine if embedding can be used
- use_embedding = self.embedding_service_on and hasattr(msg, "embed") and msg.embed is not None
+ use_embedding = (
+ self.embedding_service_on and hasattr(msg, "embed") and msg.embed is not None
+ )
# Precompute BM25 scores if they might be needed
bm25_raw = None
@@ -362,23 +384,33 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
emb_sims = None
if combine_method is not None and use_embedding:
query_embedding = msg.embed
- emb_sims = np.array([self.cosine_similarity(query_embedding, cap.embedding_description)
- for _, cap in items])
+ emb_sims = np.array(
+ [
+ self.cosine_similarity(query_embedding, cap.embedding_description)
+ for _, cap in items
+ ]
+ )
scored = []
if combine_method is None:
# Original behavior: embedding if available, else BM25 with min‑max
if use_embedding:
query_embedding = msg.embed
- for idx, (path, cap) in enumerate(items):
- sim = self.cosine_similarity(query_embedding, cap.embedding_description) if cap.embedding_description is not None else 0.0
+ for _idx, (path, cap) in enumerate(items):
+ sim = (
+ self.cosine_similarity(query_embedding, cap.embedding_description)
+ if cap.embedding_description is not None
+ else 0.0
+ )
self.logger.debug("Embedding similarity with '%s': %.4f", path, sim)
if sim >= min_similarity:
scored.append((sim, path, cap))
scored.sort(key=lambda x: x[0], reverse=True)
else:
if bm25_raw is None or len(bm25_raw) == 0:
- self.logger.warning("BM25 scores could not be computed, returning empty results")
+ self.logger.warning(
+ "BM25 scores could not be computed, returning empty results"
+ )
return {}
norm_scores = self._minmax_normalize(bm25_raw)
for idx, (path, cap) in enumerate(items):
@@ -399,7 +431,10 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
if combined >= min_similarity:
scored.append((combined, path, cap))
else:
- self.logger.warning("Embedding required for method %s but unavailable, falling back to min‑max BM25", combine_method)
+ self.logger.warning(
+ "Embedding required for method %s but unavailable, falling back to min‑max BM25",
+ combine_method,
+ )
for idx, (path, cap) in enumerate(items):
sim = float(norm_bm25[idx])
if sim >= min_similarity:
@@ -412,7 +447,10 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
if combined >= min_similarity:
scored.append((combined, path, cap))
else:
- self.logger.warning("Embedding required for method %s but unavailable, falling back to sigmoid BM25", combine_method)
+ self.logger.warning(
+ "Embedding required for method %s but unavailable, falling back to sigmoid BM25",
+ combine_method,
+ )
for idx, (path, cap) in enumerate(items):
sim = float(sig_bm25[idx])
if sim >= min_similarity:
@@ -425,7 +463,10 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
if combined >= min_similarity:
scored.append((combined, path, cap))
else:
- self.logger.warning("Embedding required for method %s but unavailable, falling back to tanh BM25", combine_method)
+ self.logger.warning(
+ "Embedding required for method %s but unavailable, falling back to tanh BM25",
+ combine_method,
+ )
for idx, (path, cap) in enumerate(items):
sim = float(tanh_bm25[idx])
if sim >= min_similarity:
@@ -445,7 +486,9 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
if combined >= min_similarity:
scored.append((combined, path, cap))
else:
- self.logger.error("Unknown combine method '%s', falling back to min‑max BM25", combine_method)
+ self.logger.error(
+ "Unknown combine method '%s', falling back to min‑max BM25", combine_method
+ )
norm_bm25 = self._minmax_normalize(bm25_raw)
for idx, (path, cap) in enumerate(items):
sim = float(norm_bm25[idx])
@@ -458,10 +501,8 @@ def search_by_similarity(self, msg: Msg, limit: int = 5, min_similarity: float =
current_span.set_attribute("search.total_matches", len(top))
result = {}
- for sim, path, cap in top:
- result[cap.name] = FunctionCall(
- name=cap.name, description=cap.description
- )
+ for _sim, _path, cap in top:
+ result[cap.name] = FunctionCall(name=cap.name, description=cap.description)
return result
@tracer.start_as_current_span("record_cap_history")
@@ -474,7 +515,9 @@ def record(self, msg: Msg, cap: Capability) -> None:
self.logger.debug("record() called but not implemented (msg=%s, cap=%s)", msg, cap)
@tracer.start_as_current_span("getCapsByHistory")
- def getCapsByHistory(self, msg: Msg, limit: int = 5, min_similarity: float = 0.5) -> Dict[str, Capability]:
+ def getCapsByHistory(
+ self, msg: Msg, limit: int = 5, min_similarity: float = 0.5
+ ) -> dict[str, Capability]:
"""
Retrieve capabilities based on usage history.
NOTE: Not implemented to avoid unbounded on‑disk storage.
@@ -497,6 +540,7 @@ def cosine_similarity(vec1, vec2) -> float:
return 0.0
return float(dot_product / (norm_vec1 * norm_vec2))
+
"""
Example usage:
from scl.storage.fsstore import fsstore
@@ -542,4 +586,4 @@ def cosine_similarity(vec1, vec2) -> float:
# Refresh cache after adding/removing skill folders
store.refresh_cache()
-"""
\ No newline at end of file
+"""
diff --git a/scl/storage/oceanbasestore.py b/scl/storage/oceanbasestore.py
index 4dadef8..f7ec9a9 100644
--- a/scl/storage/oceanbasestore.py
+++ b/scl/storage/oceanbasestore.py
@@ -1,49 +1,51 @@
-import sys
-import os
import json
import logging
-from typing import Optional, List, Dict
+import os
+import sys
# Add the StructuredContextLanguage directory to the path
scl_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(scl_root)
-from scl.otel.otel import tracer
-from scl.storage.base import StoreBase
+from scl.config import config
from scl.meta.capability import Capability
from scl.meta.msg import Msg
-from scl.config import config
+from scl.otel.otel import tracer
+from scl.storage.base import StoreBase
try:
from pyobvector import (
VECTOR,
ObVecClient,
- cosine_distance,
- inner_product,
l2_distance,
)
from pyobvector.schema import ReplaceStmt
- from sqlalchemy import JSON, Column, String, Table, BigInteger, text
+ from sqlalchemy import JSON, BigInteger, Column, String, Table, text
from sqlalchemy.dialects.mysql import LONGTEXT
+
logging.info("pyobvector imported successfully")
except ImportError as e:
- logging.error(f"Required dependencies not found: {e}. Please install pyobvector and sqlalchemy.")
+ logging.error(
+ f"Required dependencies not found: {e}. Please install pyobvector and sqlalchemy."
+ )
raise
class OceanBaseStore(StoreBase):
- def __init__(self,
- host="127.0.0.1",
- port="2881",
- user="root@test",
- password="",
- db_name="test",
- table_name="capabilities",
- embedding_model_dims=None,
- init=False):
+ def __init__(
+ self,
+ host="127.0.0.1",
+ port="2881",
+ user="root@test",
+ password="",
+ db_name="test",
+ table_name="capabilities",
+ embedding_model_dims=None,
+ init=False,
+ ):
"""
Initialize OceanBase database connection
-
+
Args:
host: OceanBase server address
port: OceanBase server port
@@ -63,25 +65,27 @@ def __init__(self,
}
self.table_name = table_name
self.embedding_model_dims = embedding_model_dims or int(config.embedding_model_dims)
-
+
# Initialize client
self._create_client()
-
+
if init:
self.create_table()
-
+
def _create_client(self):
"""Create and initialize OceanBase vector client"""
try:
- password = self.connection_args['password']
+ password = self.connection_args["password"]
if password is None or password == "":
- logging.warning("Password is empty. If authentication fails, please set OCEANBASE_PASSWORD environment variable.")
-
+ logging.warning(
+ "Password is empty. If authentication fails, please set OCEANBASE_PASSWORD environment variable."
+ )
+
self.obvector = ObVecClient(
uri=f"{self.connection_args['host']}:{self.connection_args['port']}",
- user=self.connection_args['user'],
+ user=self.connection_args["user"],
password=password or "", # Ensure password is at least an empty string
- db_name=self.connection_args['db_name'],
+ db_name=self.connection_args["db_name"],
)
logging.info("OceanBase connection successful!")
except Exception as e:
@@ -89,25 +93,31 @@ def _create_client(self):
logging.error(f"Connection failed: {e}")
if "Access denied" in error_msg or "password" in error_msg.lower():
logging.error("Authentication failed. Please check:")
- logging.error(f" 1. Username format: {self.connection_args['user']} (should be 'user@tenant')")
- logging.error(f" 2. Password is set: {'Yes' if self.connection_args.get('password') else 'No (empty or not set)'}")
+ logging.error(
+ f" 1. Username format: {self.connection_args['user']} (should be 'user@tenant')"
+ )
+ logging.error(
+ f" 2. Password is set: {'Yes' if self.connection_args.get('password') else 'No (empty or not set)'}"
+ )
logging.error(" 3. Set OCEANBASE_PASSWORD environment variable if needed")
- logging.error(" 4. For OceanBase Docker, you may need to set a password during initialization")
+ logging.error(
+ " 4. For OceanBase Docker, you may need to set a password during initialization"
+ )
else:
logging.error("Please ensure OceanBase is installed and running")
logging.error(f" Host: {self.connection_args['host']}")
logging.error(f" Port: {self.connection_args['port']}")
raise
-
+
def close(self):
"""Close database connection"""
- if hasattr(self, 'obvector') and self.obvector:
+ if hasattr(self, "obvector") and self.obvector:
# ObVecClient may not have an explicit close method
# but we can close the connection by disposing the engine
- if hasattr(self.obvector, 'engine'):
+ if hasattr(self.obvector, "engine"):
self.obvector.engine.dispose()
logging.info("Database connection closed")
-
+
def create_table(self):
"""Create capability storage table"""
try:
@@ -115,7 +125,7 @@ def create_table(self):
if self.obvector.check_table_exists(self.table_name):
logging.info(f"Table '{self.table_name}' already exists")
return
-
+
# Define table structure
cols = [
# Primary key - use BIGINT
@@ -135,7 +145,7 @@ def create_table(self):
# Function implementation
Column("function_impl", LONGTEXT, nullable=False),
]
-
+
# Create vector index parameters
vidx_params = self.obvector.prepare_index_params()
vidx_params.add_index(
@@ -145,7 +155,7 @@ def create_table(self):
metric_type="l2", # Use L2 distance
params={"M": 16, "efConstruction": 200}, # HNSW parameters
)
-
+
# Create table with vector index
self.obvector.create_table_with_index_params(
table_name=self.table_name,
@@ -154,29 +164,29 @@ def create_table(self):
vidxs=vidx_params,
partitions=None,
)
-
+
# Create index on name field
with self.obvector.engine.connect() as conn:
conn.execute(text(f"CREATE INDEX idx_name ON {self.table_name}(name)"))
conn.commit()
-
+
# Refresh metadata
self.obvector.refresh_metadata([self.table_name])
-
+
logging.info(f"Table '{self.table_name}' created successfully with indexes")
-
+
except Exception as e:
logging.error(f"Failed to create table: {e}")
raise
-
+
@tracer.start_as_current_span("insert_capability")
def insert_capability(self, cap: Capability):
"""
Insert a new capability
-
+
Args:
cap: Capability object
-
+
Returns:
ID of the inserted record, or None if failed
"""
@@ -191,7 +201,7 @@ def insert_capability(self, cap: Capability):
llm_desc = {"description": llm_desc}
elif llm_desc is None:
llm_desc = {}
-
+
# Ensure embedding_description is in list format
embedding = cap.embedding_description
if not isinstance(embedding, list):
@@ -199,9 +209,11 @@ def insert_capability(self, cap: Capability):
try:
embedding = list(embedding)
except (TypeError, ValueError):
- logging.error(f"Cannot convert embedding_description to list format: {type(embedding)}")
+ logging.error(
+ f"Cannot convert embedding_description to list format: {type(embedding)}"
+ )
return None
-
+
# Build insert record
record = {
"name": cap.name,
@@ -212,17 +224,19 @@ def insert_capability(self, cap: Capability):
"llm_description": llm_desc, # JSON type will serialize automatically
"function_impl": cap.function_impl or "",
}
-
+
# Use ReplaceStmt to implement upsert (update if name already exists)
- table = Table(self.table_name, self.obvector.metadata_obj, autoload_with=self.obvector.engine)
-
+ table = Table(
+ self.table_name, self.obvector.metadata_obj, autoload_with=self.obvector.engine
+ )
+
with self.obvector.engine.connect() as conn:
with conn.begin():
# Check if record with same name already exists
select_stmt = text(f"SELECT id FROM {self.table_name} WHERE name = :name")
result = conn.execute(select_stmt, {"name": cap.name})
existing = result.fetchone()
-
+
if existing:
# If exists, update record (REPLACE INTO will replace entire record)
record["id"] = existing[0]
@@ -235,17 +249,19 @@ def insert_capability(self, cap: Capability):
upsert_stmt = ReplaceStmt(table).values([record])
conn.execute(upsert_stmt)
# Get inserted ID
- select_id_stmt = text(f"SELECT id FROM {self.table_name} WHERE name = :name")
+ select_id_stmt = text(
+ f"SELECT id FROM {self.table_name} WHERE name = :name"
+ )
id_result = conn.execute(select_id_stmt, {"name": cap.name})
cap_id = id_result.fetchone()[0]
logging.info(f"Capability '{cap.name}' inserted successfully, ID: {cap_id}")
-
+
return cap_id
-
+
except Exception as e:
logging.error(f"Failed to insert capability: {e}", exc_info=True)
return None
-
+
@tracer.start_as_current_span("get_cap_by_name")
def get_cap_by_name(self, name) -> Capability:
"""Query capability by name"""
@@ -260,10 +276,10 @@ def get_cap_by_name(self, name) -> Capability:
FROM {self.table_name}
WHERE name = :name
""")
-
+
result = conn.execute(select_sql, {"name": name})
row = result.fetchone()
-
+
if row:
name_val, type_val, llm_desc, function_impl = row
# Parse llm_description JSON
@@ -274,32 +290,39 @@ def get_cap_by_name(self, name) -> Capability:
llm_desc = {}
except (json.JSONDecodeError, TypeError):
llm_desc = llm_desc if llm_desc else {}
-
+
logging.info(f"Found capability: {name_val}")
- return Capability(name=name_val, type=type_val, llm_description=llm_desc, function_impl=function_impl)
+ return Capability(
+ name=name_val,
+ type=type_val,
+ llm_description=llm_desc,
+ function_impl=function_impl,
+ )
else:
logging.info(f"Capability with name '{name}' not found")
return None
-
+
except Exception as e:
logging.error(f"Query failed: {e}")
return None
-
+
@tracer.start_as_current_span("search_by_similarity")
- def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> Dict[str,Capability]:
+ def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> dict[str, Capability]:
"""Query capabilities by description similarity"""
try:
# Get embedding from Msg object
query_embedding = msg.embed
-
+
# Ensure query_embedding is in list format
if not isinstance(query_embedding, list):
try:
query_embedding = list(query_embedding)
except (TypeError, ValueError):
- logging.error(f"Cannot convert query_embedding to list format: {type(query_embedding)}")
+ logging.error(
+ f"Cannot convert query_embedding to list format: {type(query_embedding)}"
+ )
return []
-
+
# Use pyobvector's ann_search for vector similarity search
results = self.obvector.ann_search(
table_name=self.table_name,
@@ -314,7 +337,7 @@ def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> Dict[st
"llm_description",
],
)
-
+
similar_functions = {}
for row in results.fetchall():
# Parse result row
@@ -325,7 +348,7 @@ def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> Dict[st
llm_desc = row[2]
# Distance is usually the last column
distance = row[-1] if len(row) > 3 else None
-
+
# Calculate similarity (smaller L2 distance means higher similarity)
# Convert distance to similarity: similarity = 1 / (1 + distance)
# Or use cosine similarity approximation: similarity = 1 - (distance / max_distance)
@@ -337,12 +360,14 @@ def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> Dict[st
similarity = 1.0 / (1.0 + distance_float)
else:
similarity = 0.0
-
+
# Check similarity threshold
if similarity < min_similarity:
- logging.info(f"{name_val} similarity {similarity:.4f} below threshold {min_similarity}")
+ logging.info(
+ f"{name_val} similarity {similarity:.4f} below threshold {min_similarity}"
+ )
continue
-
+
# Parse llm_description JSON
try:
if isinstance(llm_desc, str):
@@ -351,29 +376,31 @@ def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> Dict[st
llm_desc = {}
except (json.JSONDecodeError, TypeError):
llm_desc = llm_desc if llm_desc else {}
-
- similar_functions[name_val] = Capability(name=name_val, type=type_val, llm_description=llm_desc)
-
+
+ similar_functions[name_val] = Capability(
+ name=name_val, type=type_val, llm_description=llm_desc
+ )
+
# If we've found enough results meeting the threshold, return early
if len(similar_functions) >= limit:
break
-
+
logging.info(f"Found {len(similar_functions)} similar capabilities")
return similar_functions
-
+
except Exception as e:
logging.error(f"Similarity search failed: {e}", exc_info=True)
return {}
-
+
@tracer.start_as_current_span("record_cap_history_safe")
def record(self, msg: Msg, cap: Capability):
"""
Record a query embedding and its associated capability.
-
+
Args:
msg (Msg): The message object containing the embedding vector
cap (Capability): The capability associated with the embedding
-
+
Returns:
None
"""
@@ -381,16 +408,16 @@ def record(self, msg: Msg, cap: Capability):
# This method is provided for interface compatibility
return
- def getCapsByHistory(self, msg:Msg, limit=5, min_similarity=0.5) -> Dict[str,Capability]:
+ def getCapsByHistory(self, msg: Msg, limit=5, min_similarity=0.5) -> dict[str, Capability]:
"""
Search for similar items based on embedding similarity.
-
+
Args:
msg (Msg): The message object containing the embedding vector to search with
limit (int): Maximum number of results to return (default 5)
min_similarity (float): Minimum similarity threshold (default 0.5)
-
+
Returns:
List of similar items with their similarity scores
"""
- pass
\ No newline at end of file
+ pass
diff --git a/scl/storage/pgstore.py b/scl/storage/pgstore.py
index 1ba5136..c26728e 100644
--- a/scl/storage/pgstore.py
+++ b/scl/storage/pgstore.py
@@ -1,22 +1,26 @@
-import psycopg2
-import sys
-import os
import json
import logging
+import os
+import sys
+
+import psycopg2
+
# Add the StructuredContextLanguage directory to the path
scl_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(scl_root)
+
+from scl.config import config
+from scl.meta.capability import Capability
from scl.meta.msg import Msg
-from typing import Dict
from scl.otel.otel import tracer
-from scl.config import config
from scl.storage.base import StoreBase
-from scl.meta.capability import Capability
+
Vector = None
register_vector_info = None
try:
from pgvector import Vector
from pgvector.psycopg2 import register_vector_info
+
logging.info("pgvector imported successfully")
except ImportError as e:
logging.info(f"Warning: pgvector not installed or import failed: {e}")
@@ -25,8 +29,15 @@
class PgVectorStore(StoreBase):
- def __init__(self, dbname="postgres", user="postgres", password="your_password",
- host="localhost", port="5432", init=False):
+ def __init__(
+ self,
+ dbname="postgres",
+ user="postgres",
+ password="your_password",
+ host="localhost",
+ port="5432",
+ init=False,
+ ):
"""
初始化数据库连接
"""
@@ -35,9 +46,9 @@ def __init__(self, dbname="postgres", user="postgres", password="your_password",
"user": user,
"password": password,
"host": host,
- "port": port
+ "port": port,
}
-
+
self.conn = None
self.connect()
if init:
@@ -45,7 +56,7 @@ def __init__(self, dbname="postgres", user="postgres", password="your_password",
self.enable_vector_extension()
self.create_table()
self.create_history_table()
-
+
def connect(self):
"""连接到数据库"""
try:
@@ -54,7 +65,9 @@ def connect(self):
try:
# Try to register vector type
cursor = self.conn.cursor()
- cursor.execute("SELECT typname, oid, typarray FROM pg_type WHERE typname = 'vector'")
+ cursor.execute(
+ "SELECT typname, oid, typarray FROM pg_type WHERE typname = 'vector'"
+ )
result = cursor.fetchone()
cursor.close()
if result:
@@ -67,44 +80,45 @@ def connect(self):
logging.info("请确保PostgreSQL已安装并运行")
sys.exit(1)
-
def close(self):
"""关闭数据库连接"""
if self.conn:
self.conn.close()
logging.info("数据库连接已关闭")
-
+
def create_database(self):
"""从零开始创建数据库(需要先连接到默认数据库)"""
# 首先连接到默认的postgres数据库
default_params = self.db_params.copy()
default_params["dbname"] = "postgres"
-
+
try:
conn = psycopg2.connect(**default_params)
conn.autocommit = True
cursor = conn.cursor()
-
+
# 检查数据库是否存在
- cursor.execute("SELECT 1 FROM pg_database WHERE datname = %s", (self.db_params["dbname"],))
+ cursor.execute(
+ "SELECT 1 FROM pg_database WHERE datname = %s", (self.db_params["dbname"],)
+ )
exists = cursor.fetchone()
-
+
if not exists:
# 创建新数据库
cursor.execute(f"CREATE DATABASE {self.db_params['dbname']}")
logging.info(f"数据库 '{self.db_params['dbname']}' 创建成功!")
else:
logging.info(f"数据库 '{self.db_params['dbname']}' 已存在")
-
+
cursor.close()
conn.close()
-
+
# 重新连接到新创建的数据库
self.connect()
-
+
except Exception as e:
logging.info(f"创建数据库失败: {e}")
-
+
def enable_vector_extension(self):
"""启用pgvector扩展"""
try:
@@ -116,7 +130,7 @@ def enable_vector_extension(self):
except Exception as e:
logging.info(f"启用扩展失败: {e}")
self.conn.rollback()
-
+
def create_history_table(self):
"""创建历史记录表"""
try:
@@ -145,7 +159,7 @@ def create_table(self):
"""创建函数存储表"""
try:
cursor = self.conn.cursor()
-
+
# 获取嵌入模型的维度
embedding_dims = config.embedding_model_dims
create_table_sql = f"""
@@ -160,16 +174,18 @@ def create_table(self):
function_impl TEXT NOT NULL
);
"""
-
+
cursor.execute(create_table_sql)
-
+
# 创建索引以提高查询性能
# 为function_name创建索引
cursor.execute("CREATE INDEX IF NOT EXISTS idx_name ON capabilities(name);")
-
+
# 为llm_description创建GIN索引以加速JSON查询
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_llm_description ON capabilities USING GIN (llm_description);")
-
+ cursor.execute(
+ "CREATE INDEX IF NOT EXISTS idx_llm_description ON capabilities USING GIN (llm_description);"
+ )
+
# 为vector字段创建IVFFLAT索引以加速相似性搜索
cursor.execute("""
CREATE INDEX IF NOT EXISTS idx_embedding_description
@@ -177,43 +193,56 @@ def create_table(self):
USING ivfflat (embedding_description vector_l2_ops)
WITH (lists = 100);
""")
-
+
self.conn.commit()
cursor.close()
logging.info("表格创建成功,并已建立索引")
-
+
except Exception as e:
logging.info(f"创建表格失败: {e}")
self.conn.rollback()
-
+
@tracer.start_as_current_span("insert_capability")
- def insert_capability(self, cap:Capability):
+ def insert_capability(self, cap: Capability):
"""
插入新函数
-
+
Args:
cap: Capability
"""
- try:
+ try:
# 生成description的嵌入向量
- #embedding = Vector(cap.embedding_description)
+ # embedding = Vector(cap.embedding_description)
cursor = self.conn.cursor()
-
+
insert_sql = """
INSERT INTO capabilities (name, description, type, embedding_description, original_body, llm_description, function_impl)
VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s)
RETURNING id;
"""
- logging.info(f"Inserting function: {cap.name}, {cap.description}, {cap.type}, {cap.original_body}, {cap.function_impl}")
- cursor.execute(insert_sql, (cap.name, cap.description, cap.type, cap.embedding_description, cap.original_body, cap.llm_description, cap.function_impl))
+ logging.info(
+ f"Inserting function: {cap.name}, {cap.description}, {cap.type}, {cap.original_body}, {cap.function_impl}"
+ )
+ cursor.execute(
+ insert_sql,
+ (
+ cap.name,
+ cap.description,
+ cap.type,
+ cap.embedding_description,
+ cap.original_body,
+ cap.llm_description,
+ cap.function_impl,
+ ),
+ )
cap_id = cursor.fetchone()[0]
-
+
self.conn.commit()
cursor.close()
-
+
logging.info(f"函数 '{cap.name}' 插入成功,ID: {cap_id}")
return cap_id
-
+
except psycopg2.errors.UniqueViolation as e:
logging.info(f"函数名 '{cap.name}' 已存在(唯一约束违规){e}")
self.conn.rollback()
@@ -224,11 +253,11 @@ def insert_capability(self, cap:Capability):
return None
@tracer.start_as_current_span("get_cap_by_name")
- def get_cap_by_name(self, name)-> Capability:
+ def get_cap_by_name(self, name) -> Capability:
"""根据函数名查询"""
try:
cursor = self.conn.cursor()
-
+
select_sql = """
SELECT
name,
@@ -238,10 +267,10 @@ def get_cap_by_name(self, name)-> Capability:
FROM capabilities
WHERE name = %s;
"""
-
+
cursor.execute(select_sql, (name,))
result = cursor.fetchall()
-
+
cursor.close()
if result:
@@ -249,20 +278,22 @@ def get_cap_by_name(self, name)-> Capability:
logging.info(row)
try:
llm_desc = json.loads(row[2]) if row[2] else {}
- except:
+ except Exception:
llm_desc = row[2]
- return Capability(name=row[0], type=row[1], llm_description=llm_desc, function_impl=row[3])
- #[{"name":row[0],"type":row[1],"desc":llm_desc,"function_impl":row[3]}]
+ return Capability(
+ name=row[0], type=row[1], llm_description=llm_desc, function_impl=row[3]
+ )
+ # [{"name":row[0],"type":row[1],"desc":llm_desc,"function_impl":row[3]}]
else:
logging.info(f"未找到名为 '{name}' 的能力")
return None
-
+
except Exception as e:
logging.info(f"查询失败: {e}")
return None
-
+
@tracer.start_as_current_span("search_by_similarity")
- def search_by_similarity(self, msg:Msg, limit=5, min_similarity=0.5)-> Dict[str, Capability]:
+ def search_by_similarity(self, msg: Msg, limit=5, min_similarity=0.5) -> dict[str, Capability]:
"""根据描述相似度查询函数"""
try:
# 为查询文本生成嵌入向量)
@@ -278,38 +309,40 @@ def search_by_similarity(self, msg:Msg, limit=5, min_similarity=0.5)-> Dict[str,
ORDER BY embedding_description <=> %s::vector
LIMIT %s;
"""
-
+
cursor.execute(search_sql, (query_embedding, query_embedding, limit))
results = cursor.fetchall()
-
+
cursor.close()
logging.info("finish query db")
-
+
if results:
similar_functions = {}
for row in results:
try:
llm_desc = json.loads(row[2]) if row[2] else {}
- except:
+ except Exception:
llm_desc = row[2]
if float(row[3]) < min_similarity:
logging.info(f"{row[0]} 相似度 {row[3]} 低于阈值 {min_similarity}")
continue
- similar_functions[row[0]] = Capability(name=row[0], type=row[1], llm_description=llm_desc)
- #similar_functions.append({"name":row[0],"type":row[1],"desc":llm_desc})
-
+ similar_functions[row[0]] = Capability(
+ name=row[0], type=row[1], llm_description=llm_desc
+ )
+ # similar_functions.append({"name":row[0],"type":row[1],"desc":llm_desc})
+
logging.info(f"找到 {len(similar_functions)} 个相似函数")
return similar_functions
else:
logging.info("未找到相似函数")
return {}
-
+
except Exception as e:
logging.info(f"相似性搜索失败: {e}")
return {}
@tracer.start_as_current_span("record_cap_history")
- def record(self, msg:Msg, cap:Capability):
+ def record(self, msg: Msg, cap: Capability):
try:
cursor = self.conn.cursor()
insert_sql = """
@@ -327,7 +360,7 @@ def record(self, msg:Msg, cap:Capability):
return
@tracer.start_as_current_span("getCapsByHistory")
- def getCapsByHistory(self, msg:Msg, limit=5, min_similarity=0.5) -> Dict[str, Capability]:
+ def getCapsByHistory(self, msg: Msg, limit=5, min_similarity=0.5) -> dict[str, Capability]:
"""根据历史记录查询函数"""
try:
cursor = self.conn.cursor()
@@ -343,7 +376,7 @@ def getCapsByHistory(self, msg:Msg, limit=5, min_similarity=0.5) -> Dict[str, Ca
ORDER BY cih.embedding <=> %s::vector
LIMIT %s;
"""
-
+
cursor.execute(search_sql, (query_embedding, query_embedding, limit))
results = cursor.fetchall()
cursor.close()
@@ -352,16 +385,18 @@ def getCapsByHistory(self, msg:Msg, limit=5, min_similarity=0.5) -> Dict[str, Ca
for row in results:
try:
llm_desc = json.loads(row[2]) if row[2] else {}
- except:
+ except Exception:
llm_desc = row[2]
if float(row[3]) < min_similarity:
logging.info(f"{row[0]} 相似度 {row[3]} 低于阈值 {min_similarity}")
continue
- history_caps[row[0]] = Capability(name=row[0], type=row[1], llm_description=llm_desc)
- #similar_functions.append({"name":row[0],"type":row[1],"desc":llm_desc})
-
+ history_caps[row[0]] = Capability(
+ name=row[0], type=row[1], llm_description=llm_desc
+ )
+ # similar_functions.append({"name":row[0],"type":row[1],"desc":llm_desc})
+
logging.info(f"找到 {len(history_caps)} 个历史")
return history_caps
except Exception as e:
logging.info(f"根据历史记录查询函数失败: {e}")
- return {}
\ No newline at end of file
+ return {}
diff --git a/test.sh b/scripts/test.sh
similarity index 100%
rename from test.sh
rename to scripts/test.sh
diff --git a/test.py b/test.py
deleted file mode 100644
index 7dea26e..0000000
--- a/test.py
+++ /dev/null
@@ -1,119 +0,0 @@
-import time
-from typing import Iterable
-
-from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
- OTLPMetricExporter,
-)
-from opentelemetry.metrics import (
- CallbackOptions,
- Observation,
- get_meter_provider,
- set_meter_provider,
-)
-from opentelemetry.sdk.metrics import MeterProvider
-from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
-
-exporter = OTLPMetricExporter()
-reader = PeriodicExportingMetricReader(exporter)
-provider = MeterProvider(metric_readers=[reader])
-set_meter_provider(provider)
-
-test_value = 9
-
-def observable_counter_func(options: CallbackOptions) -> Iterable[Observation]:
- yield Observation(1, {})
-
-
-def observable_up_down_counter_func(
- options: CallbackOptions,
-) -> Iterable[Observation]:
- yield Observation(-10, {})
-
-
-def observable_gauge_func(options: CallbackOptions) -> Iterable[Observation]:
- yield Observation(test_value, {})
-
-
-meter = get_meter_provider().get_meter("getting-started", "0.1.2")
-
-# Counter
-counter = meter.create_counter("counter")
-counter.add(1)
-
-# Async Counter
-observable_counter = meter.create_observable_counter(
- "observable_counter",
- [observable_counter_func],
-)
-
-# UpDownCounter
-updown_counter = meter.create_up_down_counter("updown_counter")
-updown_counter.add(1)
-updown_counter.add(-5)
-
-# Async UpDownCounter
-observable_updown_counter = meter.create_observable_up_down_counter(
- "observable_updown_counter", [observable_up_down_counter_func]
-)
-
-# Histogram
-histogram = meter.create_histogram("histogram")
-histogram.record(99.9)
-
-
-# Histogram with explicit bucket boundaries advisory
-histogram = meter.create_histogram(
- "histogram_with_advisory",
- explicit_bucket_boundaries_advisory=[0.0, 1.0, 2.0],
-)
-histogram.record(99.9)
-histogram.record(99)
-histogram.record(98)
-
-# Async Gauge
-gauge = meter.create_observable_gauge("gauge", [observable_gauge_func])
-
-test_value = 100
-
-time.sleep(30)
-
-
-import functools
-from typing import Callable, Any
-import random
-
-def record_latency(histogram_name: str = "request_duration_seconds"):
- """记录函数执行时间的装饰器"""
- def decorator(func: Callable) -> Callable:
- @functools.wraps(func)
- def wrapper(*args, **kwargs) -> Any:
- # 获取直方图(假设在闭包或全局可访问)
- # 或者可以传递histogram对象
- start_time = time.perf_counter()
-
- try:
- result = func(*args, **kwargs)
- print(f"Function {func.__name__} returned {result}")
- return result
- finally:
- end_time = time.perf_counter()
- duration = end_time - start_time
-
- # 从meter获取或使用全局histogram
- hist = meter.create_histogram(
- histogram_name,
- explicit_bucket_boundaries_advisory=[1.0, 5.0, 10.0]
- )
- hist.record(duration, {"function": func.__name__})
-
- return wrapper
- return decorator
-
-# 使用装饰器
-@record_latency("api_call_duration")
-def call_external_api(api_name: str):
- time.sleep(random.uniform(0.5, 8.0))
- return f"Response from {api_name}"
-
-# 调用
-result = call_external_api("user_service")
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/capabilities/__init__.py b/tests/capabilities/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scl/test/capabilities/test_bash.py b/tests/capabilities/test_bash.py
similarity index 84%
rename from scl/test/capabilities/test_bash.py
rename to tests/capabilities/test_bash.py
index 3254906..a30545f 100644
--- a/scl/test/capabilities/test_bash.py
+++ b/tests/capabilities/test_bash.py
@@ -1,11 +1,12 @@
"""
Unit tests for BashFunctionCall.
"""
+
import logging
import os
import re
import subprocess
-from unittest.mock import MagicMock, patch, ANY
+from unittest.mock import ANY, MagicMock, patch
import pytest
@@ -19,7 +20,7 @@
"sudo",
"mkfs",
"dd if=",
- ":(){ :|:& };:", # fork bomb
+ ":(){ :|:& };:", # fork bomb
"chmod 777",
"wget",
"curl",
@@ -32,6 +33,7 @@
# Fixtures
# ---------------------------------------------------------------------------
+
@pytest.fixture(autouse=True)
def mock_telemetry(request):
"""
@@ -44,10 +46,12 @@ def mock_telemetry(request):
yield None
return
- with patch("scl.capabilities.bash.tracer") as mock_tracer, \
- patch("scl.capabilities.bash.meter") as mock_meter, \
- patch("scl.capabilities.bash.bash_execution_counter", MagicMock()) as mock_exec_counter, \
- patch("scl.capabilities.bash.trace.get_current_span") as mock_get_span:
+ with (
+ patch("scl.capabilities.bash.tracer") as mock_tracer,
+ patch("scl.capabilities.bash.meter") as mock_meter,
+ patch("scl.capabilities.bash.bash_execution_counter", MagicMock()) as mock_exec_counter,
+ patch("scl.capabilities.bash.trace.get_current_span") as mock_get_span,
+ ):
# --- mock span that will be returned by every context manager ---
mock_span = MagicMock()
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
@@ -73,6 +77,7 @@ def mock_telemetry(request):
def bash_class():
"""Import the ``BashFunctionCall`` class **after** the mocks are in place."""
from scl.capabilities.bash import BashFunctionCall
+
return BashFunctionCall
@@ -89,6 +94,7 @@ def fixed_cwd(tmp_path):
# Initialization tests
# ---------------------------------------------------------------------------
+
class TestInit:
def test_default_allowed_directories(self, bash_class, fixed_cwd, mock_telemetry):
cmd = bash_class(name="test", description="desc", original_body="echo hello")
@@ -96,14 +102,18 @@ def test_default_allowed_directories(self, bash_class, fixed_cwd, mock_telemetry
def test_explicit_allowed_directories(self, bash_class, mock_telemetry):
dirs = ["/home", "/tmp"]
- cmd = bash_class(name="test", description="desc", original_body="echo hello",
- allowed_directories=dirs)
+ cmd = bash_class(
+ name="test", description="desc", original_body="echo hello", allowed_directories=dirs
+ )
assert cmd.allowed_directories == dirs
def test_super_init_called(self, bash_class, mock_telemetry):
cmd = bash_class(
- name="my_name", description="my desc", original_body="my body",
- llm_description="llm desc", function_impl="impl",
+ name="my_name",
+ description="my desc",
+ original_body="my body",
+ llm_description="llm desc",
+ function_impl="impl",
)
assert cmd.type == "bash_function_call"
assert cmd.name == "my_name"
@@ -117,6 +127,7 @@ def test_super_init_called(self, bash_class, mock_telemetry):
# Danger detection
# ---------------------------------------------------------------------------
+
class TestIsDangerous:
@pytest.mark.parametrize("pattern", DANGEROUS_PATTERNS)
def test_dangerous_pattern_detected(self, bash_class, pattern):
@@ -141,6 +152,7 @@ def test_partial_match_blocked(self, bash_class):
# Execute – success cases
# ---------------------------------------------------------------------------
+
class TestExecuteSuccess:
def test_simple_echo(self, bash_class, mock_telemetry):
cmd = bash_class(name="echo_test", description="d", original_body="echo {name}")
@@ -149,14 +161,19 @@ def test_simple_echo(self, bash_class, mock_telemetry):
output = cmd.execute({"name": "Alice"})
assert output == "Hello Alice\n"
mock_run.assert_called_once_with(
- "echo Alice", shell=True, executable="/bin/bash",
- cwd=os.getcwd(), capture_output=True, text=True,
+ "echo Alice",
+ shell=True,
+ executable="/bin/bash",
+ cwd=os.getcwd(),
+ capture_output=True,
+ text=True,
)
def test_cwd_from_args(self, bash_class, mock_telemetry, tmp_path):
allowed = [str(tmp_path)]
- cmd = bash_class(name="ls", description="list", original_body="ls",
- allowed_directories=allowed)
+ cmd = bash_class(
+ name="ls", description="list", original_body="ls", allowed_directories=allowed
+ )
subdir = tmp_path / "sub"
subdir.mkdir()
with patch("subprocess.run") as mock_run:
@@ -164,14 +181,19 @@ def test_cwd_from_args(self, bash_class, mock_telemetry, tmp_path):
output = cmd.execute({"cwd": str(subdir)})
assert output == "file1\n"
mock_run.assert_called_once_with(
- "ls", shell=True, executable="/bin/bash",
- cwd=str(subdir), capture_output=True, text=True,
+ "ls",
+ shell=True,
+ executable="/bin/bash",
+ cwd=str(subdir),
+ capture_output=True,
+ text=True,
)
def test_allowed_directories_parent(self, bash_class, mock_telemetry):
dirs = ["/home"]
- cmd = bash_class(name="test", description="desc", original_body="pwd",
- allowed_directories=dirs)
+ cmd = bash_class(
+ name="test", description="desc", original_body="pwd", allowed_directories=dirs
+ )
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="/home\n", stderr="")
output = cmd.execute({"cwd": "/home"})
@@ -215,6 +237,7 @@ def test_logs_on_success(self, bash_class, mock_telemetry, caplog):
# Execute – error cases
# ---------------------------------------------------------------------------
+
class TestExecuteErrors:
def test_empty_original_body(self, bash_class, mock_telemetry):
cmd = bash_class(name="empty", description="d", original_body="")
@@ -235,8 +258,12 @@ def test_dangerous_command_blocked(self, bash_class, mock_telemetry):
mock_telemetry["span"].set_status.assert_called_once()
def test_cwd_not_allowed(self, bash_class, mock_telemetry):
- cmd = bash_class(name="cwd_block", description="d", original_body="echo",
- allowed_directories=["/allowed"])
+ cmd = bash_class(
+ name="cwd_block",
+ description="d",
+ original_body="echo",
+ allowed_directories=["/allowed"],
+ )
with patch("subprocess.run"):
with pytest.raises(ValueError, match="is not within allowed directories"):
cmd.execute({"cwd": "/forbidden"})
@@ -245,8 +272,12 @@ def test_cwd_not_allowed(self, bash_class, mock_telemetry):
def test_cwd_relative_path_resolved(self, bash_class, mock_telemetry, tmp_path):
allowed_dir = tmp_path / "safe"
allowed_dir.mkdir()
- cmd = bash_class(name="rel", description="d", original_body="echo",
- allowed_directories=[str(allowed_dir)])
+ cmd = bash_class(
+ name="rel",
+ description="d",
+ original_body="echo",
+ allowed_directories=[str(allowed_dir)],
+ )
os.chdir(str(allowed_dir))
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
@@ -264,13 +295,13 @@ def test_subprocess_exception_propagated(self, bash_class, mock_telemetry):
def test_nonzero_returncode(self, bash_class, mock_telemetry):
cmd = bash_class(name="fail", description="d", original_body="false")
with patch("subprocess.run") as mock_run:
- mock_run.return_value = MagicMock(
- returncode=1, stdout="", stderr="Permission denied"
- )
+ mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="Permission denied")
# The actual error message contains a newline between the colon and
# "Permission denied", so we must use dot-all mode.
- with pytest.raises(RuntimeError,
- match=re.compile(r"Command failed \(exit 1\):.*Permission denied", re.DOTALL)):
+ with pytest.raises(
+ RuntimeError,
+ match=re.compile(r"Command failed \(exit 1\):.*Permission denied", re.DOTALL),
+ ):
cmd.execute({})
mock_telemetry["span"].set_status.assert_called_once()
@@ -291,6 +322,7 @@ def test_no_command_text_logged_for_empty(self, bash_class, mock_telemetry, capl
# Additional safety edge cases
# ---------------------------------------------------------------------------
+
class TestSafetyEdgeCases:
def test_pattern_only_at_start(self, bash_class, mock_telemetry):
cmd = bash_class(name="d", description="d", original_body="sudo echo hi")
@@ -303,15 +335,18 @@ def test_pattern_in_middle_of_word(self, bash_class, mock_telemetry):
cmd.execute({})
def test_multiple_placeholders(self, bash_class, mock_telemetry):
- cmd = bash_class(name="multi", description="d",
- original_body="echo {greeting} {name}")
+ cmd = bash_class(name="multi", description="d", original_body="echo {greeting} {name}")
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="Hello World\n", stderr="")
output = cmd.execute({"greeting": "Hello", "name": "World"})
assert output == "Hello World\n"
mock_run.assert_called_once_with(
- "echo Hello World", shell=True, executable="/bin/bash",
- cwd=os.getcwd(), capture_output=True, text=True,
+ "echo Hello World",
+ shell=True,
+ executable="/bin/bash",
+ cwd=os.getcwd(),
+ capture_output=True,
+ text=True,
)
def test_stdout_stripped_not_modified(self, bash_class, mock_telemetry):
@@ -326,10 +361,12 @@ def test_stdout_stripped_not_modified(self, bash_class, mock_telemetry):
# Representation
# ---------------------------------------------------------------------------
+
class TestRepr:
def test_repr(self, bash_class, mock_telemetry):
- cmd = bash_class(name="x", description="d", original_body="echo",
- allowed_directories=["/a", "/b"])
+ cmd = bash_class(
+ name="x", description="d", original_body="echo", allowed_directories=["/a", "/b"]
+ )
r = repr(cmd)
assert "BashFunctionCall(name='x'" in r
assert "allowed_dirs=['/a', '/b']" in r
@@ -344,15 +381,17 @@ def test_repr_default_dirs(self, bash_class, mock_telemetry, fixed_cwd):
# Integration test – real Bash execution
# ---------------------------------------------------------------------------
+
@pytest.mark.integration
class TestRealExecution:
def test_real_echo(self):
"""Execute a simple echo command and check the output."""
from scl.capabilities.bash import BashFunctionCall
+
cmd = BashFunctionCall(
name="real_echo",
description="Real echo integration test",
original_body="echo hello",
)
output = cmd.execute({})
- assert output == "hello\n"
\ No newline at end of file
+ assert output == "hello\n"
diff --git a/scl/test/capabilities/test_fileread.py b/tests/capabilities/test_fileread.py
similarity index 80%
rename from scl/test/capabilities/test_fileread.py
rename to tests/capabilities/test_fileread.py
index 7f4db12..2de5324 100644
--- a/scl/test/capabilities/test_fileread.py
+++ b/tests/capabilities/test_fileread.py
@@ -18,15 +18,15 @@
import os
import sys
-from unittest.mock import MagicMock, patch, ANY
+from unittest.mock import ANY, MagicMock, patch
import pytest
-
# ---------------------------------------------------------------------------
# Fixture to mock OTEL dependencies *before* FileRead is imported.
# ---------------------------------------------------------------------------
+
@pytest.fixture
def mock_dependencies():
"""Set up mocked tracer, meter, trace and force‑reimport FileRead so that all
@@ -44,6 +44,7 @@ def mock_dependencies():
def start_as_current_span_side_effect(name):
"""Return a decorator that wraps the original function, entering the
mocked span context before calling it."""
+
def decorator(func):
def wrapper(*args, **kwargs):
# Use the already‑configured return_value as context manager.
@@ -52,7 +53,9 @@ def wrapper(*args, **kwargs):
ctx = mock_tracer.start_as_current_span.return_value
with ctx:
return func(*args, **kwargs)
+
return wrapper
+
return decorator
mock_tracer.start_as_current_span.side_effect = start_as_current_span_side_effect
@@ -70,13 +73,16 @@ def wrapper(*args, **kwargs):
mock_trace.Status.return_value = MagicMock(status_code=2)
# Force a fresh import of the module under test
- sys.modules.pop('scl.capabilities.fileread', None)
+ sys.modules.pop("scl.capabilities.fileread", None)
# Patch the *source* modules used by fileread.py
- with patch('scl.capabilities.fileread.tracer', mock_tracer), \
- patch('scl.capabilities.fileread.meter', mock_meter), \
- patch('scl.capabilities.fileread.trace', mock_trace):
+ with (
+ patch("scl.capabilities.fileread.tracer", mock_tracer),
+ patch("scl.capabilities.fileread.meter", mock_meter),
+ patch("scl.capabilities.fileread.trace", mock_trace),
+ ):
from scl.capabilities.fileread import FileRead
+
yield FileRead, mock_tracer, mock_span, mock_meter, mock_counter
@@ -84,6 +90,7 @@ def wrapper(*args, **kwargs):
# Helper fixtures for temporary files / directories
# ---------------------------------------------------------------------------
+
@pytest.fixture
def tmp_allowed_dir(tmp_path):
"""Create a temporary directory that will serve as an allowed path."""
@@ -112,6 +119,7 @@ def binary_file(tmp_allowed_dir):
# Initialization tests
# ---------------------------------------------------------------------------
+
class TestInitialization:
def test_default_allowed_directories_is_cwd(self, mock_dependencies):
FileRead, *_ = mock_dependencies
@@ -122,17 +130,17 @@ def test_default_allowed_directories_is_cwd(self, mock_dependencies):
def test_custom_absolute_directories_are_normalized(self, mock_dependencies):
FileRead, *_ = mock_dependencies
dirs = ["/tmp", "/var/run"]
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=dirs)
+ reader = FileRead(name="r", description="d", original_body="b", allowed_directories=dirs)
expected = [os.path.abspath(d) for d in dirs]
assert reader._allowed_dirs == expected
def test_relative_allowed_directories_become_absolute(self, mock_dependencies, tmp_path):
FileRead, *_ = mock_dependencies
rel_dir = "rel"
- with patch.object(os, 'getcwd', return_value=str(tmp_path)):
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[rel_dir])
+ with patch.object(os, "getcwd", return_value=str(tmp_path)):
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[rel_dir]
+ )
assert reader._allowed_dirs[0] == os.path.join(str(tmp_path), rel_dir)
def test_super_init_called_with_correct_args(self, mock_dependencies):
@@ -140,30 +148,30 @@ def test_super_init_called_with_correct_args(self, mock_dependencies):
the instance attributes set by the base Capability class."""
FileRead, *_ = mock_dependencies
reader = FileRead(
- name="test_name",
- description="test_desc",
- original_body="body",
- llm_description="llm"
+ name="test_name", description="test_desc", original_body="body", llm_description="llm"
)
assert reader.name == "test_name"
# base class may store type in 'type' or '_type'
- assert getattr(reader, 'type', getattr(reader, '_type', None)) == "file_read"
+ assert getattr(reader, "type", getattr(reader, "_type", None)) == "file_read"
assert reader.original_body == "body"
assert reader.llm_description == "llm"
def test_init_span_attributes(self, mock_dependencies):
FileRead, mock_tracer, mock_span, *_ = mock_dependencies
- reader = FileRead(name="reader1", description="d", original_body="o",
- allowed_directories=["/tmp"])
+ reader = FileRead(
+ name="reader1", description="d", original_body="o", allowed_directories=["/tmp"]
+ )
mock_span.set_attribute.assert_any_call("file_read.name", "reader1")
- mock_span.set_attribute.assert_any_call("file_read.allowed_directories",
- str(reader._allowed_dirs))
+ mock_span.set_attribute.assert_any_call(
+ "file_read.allowed_directories", str(reader._allowed_dirs)
+ )
# ---------------------------------------------------------------------------
# execute tests – error cases
# ---------------------------------------------------------------------------
+
class TestExecuteErrors:
def test_missing_path_argument(self, mock_dependencies):
FileRead, _, mock_span, *_ = mock_dependencies
@@ -176,23 +184,26 @@ def test_missing_path_argument(self, mock_dependencies):
def test_absolute_path_outside_allowed_directories(self, mock_dependencies, tmp_allowed_dir):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[tmp_allowed_dir])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[tmp_allowed_dir]
+ )
bad_path = "/outside/file.txt"
with pytest.raises(ValueError, match="is not within allowed directories"):
reader.execute({"path": bad_path})
def test_relative_path_not_found_in_any_allowed_dir(self, mock_dependencies, tmp_allowed_dir):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[tmp_allowed_dir])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[tmp_allowed_dir]
+ )
with pytest.raises(ValueError, match="does not exist in any allowed directory"):
reader.execute({"path": "nonexistent.txt"})
def test_path_is_directory_not_file(self, mock_dependencies, tmp_allowed_dir):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[tmp_allowed_dir])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[tmp_allowed_dir]
+ )
subdir = os.path.join(tmp_allowed_dir, "subdir")
os.makedirs(subdir, exist_ok=True)
with pytest.raises(ValueError, match="Path is not a regular file"):
@@ -201,25 +212,31 @@ def test_path_is_directory_not_file(self, mock_dependencies, tmp_allowed_dir):
def test_binary_extension_raises(self, mock_dependencies, binary_file):
FileRead, *_ = mock_dependencies
allowed_dir = os.path.dirname(binary_file)
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[allowed_dir])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[allowed_dir]
+ )
with pytest.raises(ValueError, match="Binary file extension '.png' is not allowed"):
reader.execute({"path": os.path.basename(binary_file)})
def test_unicode_decode_error_raises_valueerror(self, mock_dependencies, tmp_allowed_dir):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[tmp_allowed_dir])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[tmp_allowed_dir]
+ )
dummy_file = os.path.join(tmp_allowed_dir, "bad.txt")
with open(dummy_file, "wb") as f:
- f.write(b'\x80\x81')
+ f.write(b"\x80\x81")
with pytest.raises(ValueError, match="File could not be decoded as UTF-8"):
reader.execute({"path": os.path.basename(dummy_file)})
def test_os_error_during_read(self, mock_dependencies, text_file):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[os.path.dirname(text_file)])
+ reader = FileRead(
+ name="r",
+ description="d",
+ original_body="b",
+ allowed_directories=[os.path.dirname(text_file)],
+ )
with patch("builtins.open", side_effect=OSError("Permission denied")):
with pytest.raises(OSError, match="Permission denied"):
reader.execute({"path": os.path.basename(text_file)})
@@ -229,11 +246,16 @@ def test_os_error_during_read(self, mock_dependencies, text_file):
# execute tests – success cases
# ---------------------------------------------------------------------------
+
class TestExecuteSuccess:
def test_absolute_path_success(self, mock_dependencies, text_file):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[os.path.dirname(text_file)])
+ reader = FileRead(
+ name="r",
+ description="d",
+ original_body="b",
+ allowed_directories=[os.path.dirname(text_file)],
+ )
result = reader.execute({"path": text_file})
assert result == "Hello World"
@@ -245,15 +267,20 @@ def test_multiple_allowed_directories_finds_second(self, mock_dependencies, tmp_
dir2.mkdir()
test_file = dir2 / "findme.txt"
test_file.write_text("found me")
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[str(dir1), str(dir2)])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[str(dir1), str(dir2)]
+ )
result = reader.execute({"path": "findme.txt"})
assert result == "found me"
def test_execute_span_attributes(self, mock_dependencies, text_file):
FileRead, _, mock_span, *_ = mock_dependencies
- reader = FileRead(name="span_test", description="d", original_body="b",
- allowed_directories=[os.path.dirname(text_file)])
+ reader = FileRead(
+ name="span_test",
+ description="d",
+ original_body="b",
+ allowed_directories=[os.path.dirname(text_file)],
+ )
mock_span.reset_mock()
reader.execute({"path": os.path.basename(text_file)})
mock_span.set_attribute.assert_any_call("file_read.raw_path", os.path.basename(text_file))
@@ -263,13 +290,14 @@ def test_read_own_source_file(self, mock_dependencies):
"""Read the module's source code to verify it returns the expected class definition."""
FileRead, *_ = mock_dependencies
import inspect
+
source_file = inspect.getfile(FileRead)
source_dir = os.path.dirname(source_file)
reader = FileRead(
name="source_reader",
description="Read own source",
original_body="test",
- allowed_directories=[source_dir]
+ allowed_directories=[source_dir],
)
content = reader.execute({"path": os.path.basename(source_file)})
assert "class FileRead(Capability)" in content
@@ -279,10 +307,12 @@ def test_read_own_source_file(self, mock_dependencies):
# Representation
# ---------------------------------------------------------------------------
+
def test_repr(mock_dependencies):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=["/tmp", "/var"])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=["/tmp", "/var"]
+ )
r = repr(reader)
assert "FileRead(name='r'" in r
assert "/tmp" in r
@@ -293,11 +323,11 @@ def test_repr(mock_dependencies):
# Edge cases
# ---------------------------------------------------------------------------
+
class TestEdgeCases:
def test_empty_allowed_directories_list(self, mock_dependencies):
FileRead, *_ = mock_dependencies
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[])
+ reader = FileRead(name="r", description="d", original_body="b", allowed_directories=[])
with pytest.raises(ValueError, match="does not exist in any allowed directory"):
reader.execute({"path": "anything"})
@@ -309,7 +339,8 @@ def test_relative_path_traversal_currently_allowed(self, mock_dependencies, tmp_
secret = os.path.join(parent_dir, "secret.txt")
with open(secret, "w") as f:
f.write("traversed")
- reader = FileRead(name="r", description="d", original_body="b",
- allowed_directories=[tmp_allowed_dir])
+ reader = FileRead(
+ name="r", description="d", original_body="b", allowed_directories=[tmp_allowed_dir]
+ )
result = reader.execute({"path": "../secret.txt"})
- assert result == "traversed"
\ No newline at end of file
+ assert result == "traversed"
diff --git a/scl/test/capabilities/test_filwrite.py b/tests/capabilities/test_filewrite.py
similarity index 90%
rename from scl/test/capabilities/test_filwrite.py
rename to tests/capabilities/test_filewrite.py
index 88990fe..bbf2bf6 100644
--- a/scl/test/capabilities/test_filwrite.py
+++ b/tests/capabilities/test_filewrite.py
@@ -12,20 +12,21 @@
- Temporary file write under /tmp with cleanup
"""
-import os
import logging
+import os
import tempfile
from pathlib import Path
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock, patch
+
import pytest
from scl.capabilities.filewrite import FileWrite
-
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
+
@pytest.fixture
def temp_dir(tmp_path):
"""Provide a temporary directory as an allowed write target."""
@@ -75,6 +76,7 @@ def file_write_instance(allowed_dirs):
# Tests
# ---------------------------------------------------------------------------
+
class TestFileWriteInit:
"""Tests for FileWrite.__init__()."""
@@ -107,17 +109,15 @@ def test_overwrite_default_mode(self, file_write_instance, temp_dir):
result = file_write_instance.execute({"path": path, "content": content})
assert result == content
assert os.path.isfile(path)
- with open(path, "r", encoding="utf-8") as f:
+ with open(path, encoding="utf-8") as f:
assert f.read() == content
def test_append_mode(self, file_write_instance, temp_dir):
path = os.path.join(temp_dir, "append.txt")
file_write_instance.execute({"path": path, "content": "Line1\n"})
- result = file_write_instance.execute({
- "path": path, "content": "Line2\n", "mode": "a"
- })
+ result = file_write_instance.execute({"path": path, "content": "Line2\n", "mode": "a"})
assert result == "Line2\n"
- with open(path, "r", encoding="utf-8") as f:
+ with open(path, encoding="utf-8") as f:
assert f.readlines() == ["Line1\n", "Line2\n"]
def test_creates_parent_directories(self, file_write_instance, temp_dir):
@@ -125,7 +125,7 @@ def test_creates_parent_directories(self, file_write_instance, temp_dir):
content = "nested"
file_write_instance.execute({"path": nested_path, "content": content})
assert os.path.isfile(nested_path)
- with open(nested_path, "r", encoding="utf-8") as f:
+ with open(nested_path, encoding="utf-8") as f:
assert f.read() == content
def test_chmod_called_with_644(self, file_write_instance, temp_dir, monkeypatch):
@@ -149,7 +149,7 @@ def test_write_under_tmp_and_cleanup(self):
original_body="body",
allowed_dirs=["/tmp"],
)
- fd, path = tempfile.mkstemp(suffix='.txt', prefix='filewrite_test_', dir='/tmp')
+ fd, path = tempfile.mkstemp(suffix=".txt", prefix="filewrite_test_", dir="/tmp")
os.close(fd)
try:
os.unlink(path) # remove empty file created by mkstemp
@@ -161,7 +161,7 @@ def test_write_under_tmp_and_cleanup(self):
result = writer.execute({"path": path, "content": content})
assert result == content
assert os.path.isfile(path)
- with open(path, 'r', encoding='utf-8') as f:
+ with open(path, encoding="utf-8") as f:
assert f.read() == content
finally:
if os.path.isfile(path):
@@ -189,7 +189,9 @@ def test_path_traversal_blocked(self, file_write_instance, temp_dir):
with pytest.raises(PermissionError, match="outside allowed directories"):
file_write_instance.execute({"path": traversal, "content": "bad"})
- def test_symlink_attack_blocked_if_resolved_outside(self, file_write_instance, temp_dir, tmp_path):
+ def test_symlink_attack_blocked_if_resolved_outside(
+ self, file_write_instance, temp_dir, tmp_path
+ ):
# The current implementation does *not* resolve symlinks before the
# allowed‑directory check. This test reflects the actual behaviour:
# writing to a symlink inside the allowed directory succeeds (or fails
@@ -234,18 +236,13 @@ def test_missing_content_raises_value_error(self, file_write_instance, temp_dir)
def test_invalid_mode_raises_value_error(self, file_write_instance, temp_dir):
with pytest.raises(ValueError, match="Invalid mode"):
- file_write_instance.execute({
- "path": os.path.join(temp_dir, "f.txt"),
- "content": "c",
- "mode": "x"
- })
+ file_write_instance.execute(
+ {"path": os.path.join(temp_dir, "f.txt"), "content": "c", "mode": "x"}
+ )
def test_none_content_raises_value_error(self, file_write_instance, temp_dir):
with pytest.raises(ValueError, match="Missing 'content'"):
- file_write_instance.execute({
- "path": os.path.join(temp_dir, "f.txt"),
- "content": None
- })
+ file_write_instance.execute({"path": os.path.join(temp_dir, "f.txt"), "content": None})
class TestFileWriteOSErrorHandling:
@@ -269,7 +266,9 @@ def test_permission_error_on_existing_readonly_file(self, file_write_instance, t
class TestOpenTelemetryInstrumentation:
- def test_execute_sets_span_attributes_on_success(self, file_write_instance, temp_dir, mock_tracer):
+ def test_execute_sets_span_attributes_on_success(
+ self, file_write_instance, temp_dir, mock_tracer
+ ):
_, mock_span = mock_tracer
path = os.path.join(temp_dir, "otel_attrs.txt")
content = "trace me"
@@ -338,24 +337,25 @@ def test_example_from_docstring(self, tmp_path):
name="template_writer",
description="Writes templates to the output directory",
original_body="Writes file content safely",
- allowed_dirs=[output_dir, sandbox_dir]
+ allowed_dirs=[output_dir, sandbox_dir],
)
- result = writer.execute({
- "path": os.path.join(sandbox_dir, "hello.txt"),
- "content": "Hello, world!\n"
- })
+ result = writer.execute(
+ {"path": os.path.join(sandbox_dir, "hello.txt"), "content": "Hello, world!\n"}
+ )
assert result == "Hello, world!\n"
- with open(os.path.join(sandbox_dir, "hello.txt"), "r") as f:
+ with open(os.path.join(sandbox_dir, "hello.txt")) as f:
assert f.read() == "Hello, world!\n"
- result_append = writer.execute({
- "path": os.path.join(sandbox_dir, "hello.txt"),
- "content": "This line is appended.\n",
- "mode": "a"
- })
+ result_append = writer.execute(
+ {
+ "path": os.path.join(sandbox_dir, "hello.txt"),
+ "content": "This line is appended.\n",
+ "mode": "a",
+ }
+ )
assert result_append == "This line is appended.\n"
- with open(os.path.join(sandbox_dir, "hello.txt"), "r") as f:
+ with open(os.path.join(sandbox_dir, "hello.txt")) as f:
lines = f.readlines()
assert lines == ["Hello, world!\n", "This line is appended.\n"]
@@ -367,4 +367,4 @@ def test_repr(self, file_write_instance):
rep = repr(file_write_instance)
assert "FileWrite" in rep
assert file_write_instance.name in rep
- assert str(file_write_instance.allowed_dirs) in rep
\ No newline at end of file
+ assert str(file_write_instance.allowed_dirs) in rep
diff --git a/scl/test/capabilities/test_git.py b/tests/capabilities/test_git.py
similarity index 71%
rename from scl/test/capabilities/test_git.py
rename to tests/capabilities/test_git.py
index c9ffa3e..1986afe 100644
--- a/scl/test/capabilities/test_git.py
+++ b/tests/capabilities/test_git.py
@@ -13,15 +13,19 @@
- Basic git availability check via --version
"""
-import sys
import subprocess
-from unittest.mock import MagicMock, patch, call, ANY
+import sys
+from unittest.mock import ANY, MagicMock, call, patch
+
import pytest
# ---------------------------------------------------------------------------
-# Mock external dependencies before importing the module under test
+# Import the module under test with its OpenTelemetry hooks and Capability base
+# class replaced by mocks, so that the @tracer decorator is a no-op and the base
+# records constructor arguments. Only the two leaf submodules are overridden
+# (not the scl.meta / scl.otel packages), and patch.dict restores sys.modules on
+# exit so these mocks never leak into other test modules.
# ---------------------------------------------------------------------------
-# Mock scl.otel.otel
mock_otel = MagicMock()
mock_tracer = MagicMock()
mock_meter = MagicMock()
@@ -30,12 +34,11 @@
mock_tracer.start_as_current_span.side_effect = lambda name: lambda fn: fn
mock_otel.tracer = mock_tracer
mock_otel.meter = mock_meter
-sys.modules['scl.otel'] = MagicMock()
-sys.modules['scl.otel.otel'] = mock_otel
-# Mock scl.meta.capability to provide a Capability base class
+
class MockCapability:
"""Minimal base class that records constructor arguments."""
+
def __init__(self, name, type, description, original_body, llm_description, function_impl):
self.name = name
self.type = type
@@ -44,13 +47,20 @@ def __init__(self, name, type, description, original_body, llm_description, func
self.llm_description = llm_description
self.function_impl = function_impl
+
mock_capability_module = MagicMock()
mock_capability_module.Capability = MockCapability
-sys.modules['scl.meta'] = MagicMock()
-sys.modules['scl.meta.capability'] = mock_capability_module
-# Now safe to import the GitCapability class (decorators already applied with mock)
-from scl.capabilities.git import GitCapability
+# Initialize the real package tree first so importing scl does not overwrite the
+# overrides below, then import git fresh against the mocks.
+import scl.capabilities # noqa: F401
+
+with patch.dict(
+ sys.modules,
+ {"scl.otel.otel": mock_otel, "scl.meta.capability": mock_capability_module},
+):
+ sys.modules.pop("scl.capabilities.git", None)
+ from scl.capabilities.git import GitCapability
# ---------------------------------------------------------------------------
@@ -81,12 +91,12 @@ def git_capability(mock_span):
Keeps the get_current_span patch active for the whole test function
so that execute() sees the same mock_span.
"""
- with patch('opentelemetry.trace.get_current_span', return_value=mock_span):
+ with patch("opentelemetry.trace.get_current_span", return_value=mock_span):
cap = GitCapability(
name="test_git",
description="Test git capability",
original_body="test body",
- llm_description="LLM test desc"
+ llm_description="LLM test desc",
)
yield cap
@@ -96,26 +106,20 @@ def git_capability(mock_span):
# ---------------------------------------------------------------------------
class TestGitVersion:
"""Ensure git is available in the test environment by running git --version."""
+
def test_git_version_command(self):
"""Run git --version and verify it exits successfully."""
- result = subprocess.run(
- ['git', '--version'],
- capture_output=True,
- text=True
- )
+ result = subprocess.run(["git", "--version"], capture_output=True, text=True)
assert result.returncode == 0, f"git --version failed: {result.stderr}"
- assert 'git version' in result.stdout, f"Unexpected output: {result.stdout}"
+ assert "git version" in result.stdout, f"Unexpected output: {result.stdout}"
class TestGitCapabilityInit:
def test_init_sets_attributes(self, mock_span):
"""Verify that initialization calls the base class with correct arguments."""
- with patch('opentelemetry.trace.get_current_span', return_value=mock_span):
+ with patch("opentelemetry.trace.get_current_span", return_value=mock_span):
cap = GitCapability(
- name="mygit",
- description="desc",
- original_body="body",
- llm_description="llm_desc"
+ name="mygit", description="desc", original_body="body", llm_description="llm_desc"
)
assert cap.name == "mygit"
assert cap.type == "git"
@@ -147,7 +151,7 @@ def test_unsupported_action_raises_value_error(self, git_capability, mock_span):
class TestExecuteCommit:
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_commit_returns_hash(self, mock_run, git_capability, mock_span):
"""Happy path: commit stages, commits, and returns new HEAD hash."""
mock_run.side_effect = [
@@ -162,15 +166,20 @@ def test_commit_returns_hash(self, mock_run, git_capability, mock_span):
assert result == "abc123def"
expected_calls = [
- call(['git', 'rev-parse', '--is-inside-work-tree'], check=True, capture_output=True, text=True),
- call(['git', 'add', '.'], check=True, capture_output=True, text=True),
- call(['git', 'commit', '-m', message], check=True, capture_output=True, text=True),
- call(['git', 'rev-parse', 'HEAD'], check=True, capture_output=True, text=True),
+ call(
+ ["git", "rev-parse", "--is-inside-work-tree"],
+ check=True,
+ capture_output=True,
+ text=True,
+ ),
+ call(["git", "add", "."], check=True, capture_output=True, text=True),
+ call(["git", "commit", "-m", message], check=True, capture_output=True, text=True),
+ call(["git", "rev-parse", "HEAD"], check=True, capture_output=True, text=True),
]
mock_run.assert_has_calls(expected_calls)
mock_span.set_attribute.assert_any_call("git.commit.message", message)
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_commit_missing_message_raises_value_error(self, mock_run, git_capability, mock_span):
with pytest.raises(ValueError, match="Commit requires a 'message'"):
git_capability.execute({"action": "commit"})
@@ -179,28 +188,36 @@ def test_commit_missing_message_raises_value_error(self, mock_run, git_capabilit
with pytest.raises(ValueError, match="Commit requires a 'message'"):
git_capability.execute({"action": "commit", "message": ""})
- @patch('subprocess.run')
- def test_commit_when_not_in_git_repo_raises_runtime_error(self, mock_run, git_capability, mock_span):
- mock_run.side_effect = __import__('subprocess').CalledProcessError(
- returncode=128, cmd="git rev-parse --is-inside-work-tree", stderr="fatal: not a git repository"
+ @patch("subprocess.run")
+ def test_commit_when_not_in_git_repo_raises_runtime_error(
+ self, mock_run, git_capability, mock_span
+ ):
+ mock_run.side_effect = __import__("subprocess").CalledProcessError(
+ returncode=128,
+ cmd="git rev-parse --is-inside-work-tree",
+ stderr="fatal: not a git repository",
)
with pytest.raises(RuntimeError, match="Current directory is not a Git repository"):
git_capability.execute({"action": "commit", "message": "msg"})
- @patch('subprocess.run')
- def test_commit_git_command_failure_raises_runtime_error(self, mock_run, git_capability, mock_span):
+ @patch("subprocess.run")
+ def test_commit_git_command_failure_raises_runtime_error(
+ self, mock_run, git_capability, mock_span
+ ):
mock_run.side_effect = [
completed_process(), # repo verification ok
- __import__('subprocess').CalledProcessError(
+ __import__("subprocess").CalledProcessError(
returncode=1, cmd="git add", stderr="error: pathspec '.' did not match any files"
),
]
- with pytest.raises(RuntimeError, match="Git commit failed: error: pathspec '.' did not match any files"):
+ with pytest.raises(
+ RuntimeError, match="Git commit failed: error: pathspec '.' did not match any files"
+ ):
git_capability.execute({"action": "commit", "message": "msg"})
class TestExecuteCheckout:
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_checkout_returns_hash(self, mock_run, git_capability, mock_span):
mock_run.side_effect = [
completed_process(), # repo verification
@@ -212,44 +229,52 @@ def test_checkout_returns_hash(self, mock_run, git_capability, mock_span):
assert result == commit_hash
expected_calls = [
- call(['git', 'rev-parse', '--is-inside-work-tree'], check=True, capture_output=True, text=True),
- call(['git', 'checkout', commit_hash], check=True, capture_output=True, text=True),
+ call(
+ ["git", "rev-parse", "--is-inside-work-tree"],
+ check=True,
+ capture_output=True,
+ text=True,
+ ),
+ call(["git", "checkout", commit_hash], check=True, capture_output=True, text=True),
]
mock_run.assert_has_calls(expected_calls)
mock_span.set_attribute.assert_any_call("git.checkout.hash", commit_hash)
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_checkout_missing_hash_raises_value_error(self, mock_run, git_capability, mock_span):
with pytest.raises(ValueError, match="Checkout requires a 'commit_hash'"):
git_capability.execute({"action": "checkout"})
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_checkout_not_in_repo_raises_runtime_error(self, mock_run, git_capability):
- mock_run.side_effect = __import__('subprocess').CalledProcessError(
- returncode=128, cmd="git rev-parse --is-inside-work-tree", stderr="fatal: not a git repository"
+ mock_run.side_effect = __import__("subprocess").CalledProcessError(
+ returncode=128,
+ cmd="git rev-parse --is-inside-work-tree",
+ stderr="fatal: not a git repository",
)
with pytest.raises(RuntimeError, match="Current directory is not a Git repository"):
git_capability.execute({"action": "checkout", "commit_hash": "abc"})
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_checkout_failure_raises_runtime_error(self, mock_run, git_capability):
mock_run.side_effect = [
completed_process(),
- __import__('subprocess').CalledProcessError(
- returncode=1, cmd="git checkout", stderr="error: pathspec 'abc' did not match any file(s) known to git."
+ __import__("subprocess").CalledProcessError(
+ returncode=1,
+ cmd="git checkout",
+ stderr="error: pathspec 'abc' did not match any file(s) known to git.",
),
]
- with pytest.raises(RuntimeError, match="Git checkout failed: error: pathspec 'abc' did not match"):
+ with pytest.raises(
+ RuntimeError, match="Git checkout failed: error: pathspec 'abc' did not match"
+ ):
git_capability.execute({"action": "checkout", "commit_hash": "abc"})
class TestExecuteHistory:
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_history_returns_list_of_dicts(self, mock_run, git_capability, mock_span):
- log_output = (
- "hash1\tAlice\t2025-01-01\tFirst commit\n"
- "hash2\tBob\t2025-01-02\tSecond commit"
- )
+ log_output = "hash1\tAlice\t2025-01-01\tFirst commit\nhash2\tBob\t2025-01-02\tSecond commit"
mock_run.side_effect = [
completed_process(), # repo verification
completed_process(stdout=log_output),
@@ -258,12 +283,22 @@ def test_history_returns_list_of_dicts(self, mock_run, git_capability, mock_span
result = git_capability.execute({"action": "history"})
expected = [
- {"commit_hash": "hash1", "author": "Alice", "date": "2025-01-01", "message": "First commit"},
- {"commit_hash": "hash2", "author": "Bob", "date": "2025-01-02", "message": "Second commit"},
+ {
+ "commit_hash": "hash1",
+ "author": "Alice",
+ "date": "2025-01-01",
+ "message": "First commit",
+ },
+ {
+ "commit_hash": "hash2",
+ "author": "Bob",
+ "date": "2025-01-02",
+ "message": "Second commit",
+ },
]
assert result == expected
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_history_empty_output(self, mock_run, git_capability):
mock_run.side_effect = [
completed_process(),
@@ -272,12 +307,10 @@ def test_history_empty_output(self, mock_run, git_capability):
result = git_capability.execute({"action": "history"})
assert result == []
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_history_skips_lines_with_insufficient_fields(self, mock_run, git_capability):
log_output = (
- "hash1\tAlice\t2025-01-01\tMessage\n"
- "incomplete_line\n"
- "hash2\tBob\t2025-01-02\tSecond\n"
+ "hash1\tAlice\t2025-01-01\tMessage\nincomplete_line\nhash2\tBob\t2025-01-02\tSecond\n"
)
mock_run.side_effect = [
completed_process(),
@@ -288,7 +321,7 @@ def test_history_skips_lines_with_insufficient_fields(self, mock_run, git_capabi
assert result[0]["commit_hash"] == "hash1"
assert result[1]["commit_hash"] == "hash2"
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_history_with_single_commit(self, mock_run, git_capability):
log_output = "hash3\tCarol\t2025-03-10\tSingle commit"
mock_run.side_effect = [
@@ -296,19 +329,28 @@ def test_history_with_single_commit(self, mock_run, git_capability):
completed_process(stdout=log_output),
]
result = git_capability.execute({"action": "history"})
- assert result == [{"commit_hash": "hash3", "author": "Carol", "date": "2025-03-10", "message": "Single commit"}]
+ assert result == [
+ {
+ "commit_hash": "hash3",
+ "author": "Carol",
+ "date": "2025-03-10",
+ "message": "Single commit",
+ }
+ ]
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_history_not_in_repo_raises_runtime_error(self, mock_run, git_capability):
- mock_run.side_effect = __import__('subprocess').CalledProcessError(
- returncode=128, cmd="git rev-parse --is-inside-work-tree", stderr="fatal: not a git repository"
+ mock_run.side_effect = __import__("subprocess").CalledProcessError(
+ returncode=128,
+ cmd="git rev-parse --is-inside-work-tree",
+ stderr="fatal: not a git repository",
)
with pytest.raises(RuntimeError, match="Current directory is not a Git repository"):
git_capability.execute({"action": "history"})
class TestMetricsAndTracing:
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_counter_incremented_on_success(self, mock_run, git_capability, mock_span):
mock_run.side_effect = [
completed_process(),
@@ -323,13 +365,13 @@ def test_counter_incremented_on_success(self, mock_run, git_capability, mock_spa
counter = mock_meter.create_counter.return_value
counter.add.assert_called_once_with(1, {"action": "commit"})
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_span_status_error_on_failure(self, mock_run, git_capability, mock_span):
"""Verify that no span status is set on command failure (the implementation
currently only sets status for missing/invalid actions, not for subprocess errors)."""
mock_run.side_effect = [
completed_process(),
- __import__('subprocess').CalledProcessError(
+ __import__("subprocess").CalledProcessError(
returncode=1, cmd="git commit", stderr="error: something went wrong"
),
]
@@ -340,7 +382,7 @@ def test_span_status_error_on_failure(self, mock_run, git_capability, mock_span)
# so we assert it was not called.
mock_span.set_status.assert_not_called()
- @patch('subprocess.run')
+ @patch("subprocess.run")
def test_span_attributes_on_success(self, mock_run, git_capability, mock_span):
mock_run.side_effect = [
completed_process(),
@@ -349,4 +391,4 @@ def test_span_attributes_on_success(self, mock_run, git_capability, mock_span):
completed_process(stdout="hash123\n"),
]
git_capability.execute({"action": "commit", "message": "test"})
- mock_span.set_attribute.assert_any_call("git.result.success", True)
\ No newline at end of file
+ mock_span.set_attribute.assert_any_call("git.result.success", True)
diff --git a/scl/test/capabilities/test_grep.py b/tests/capabilities/test_grep.py
similarity index 85%
rename from scl/test/capabilities/test_grep.py
rename to tests/capabilities/test_grep.py
index bc339e1..72092e9 100644
--- a/scl/test/capabilities/test_grep.py
+++ b/tests/capabilities/test_grep.py
@@ -2,17 +2,19 @@
Tests for GrepFunctionCall (grep.py)
Uses pytest and mocking to avoid real subprocess and OpenTelemetry calls.
"""
-import pytest
+
import os
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock, patch
-from scl.capabilities.grep import GrepFunctionCall
+import pytest
+from scl.capabilities.grep import GrepFunctionCall
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
+
@pytest.fixture
def mock_span():
"""Return a mock span that records set_attribute and set_status."""
@@ -25,8 +27,10 @@ def mock_span():
@pytest.fixture
def mock_tracer(mock_span):
"""Patch the module-level tracer and trace.get_current_span to return mock spans."""
- with patch('scl.capabilities.grep.tracer') as tracer_mock, \
- patch('scl.capabilities.grep.trace.get_current_span', return_value=mock_span):
+ with (
+ patch("scl.capabilities.grep.tracer") as tracer_mock,
+ patch("scl.capabilities.grep.trace.get_current_span", return_value=mock_span),
+ ):
tracer_mock.start_as_current_span.return_value = mock_span
yield tracer_mock
@@ -34,10 +38,10 @@ def mock_tracer(mock_span):
@pytest.fixture
def mock_meter():
"""Patch the module-level meter and its counter."""
- with patch('scl.capabilities.grep.meter') as meter_mock:
+ with patch("scl.capabilities.grep.meter") as meter_mock:
counter_mock = MagicMock()
meter_mock.create_counter.return_value = counter_mock
- with patch('scl.capabilities.grep.grep_execution_counter', counter_mock):
+ with patch("scl.capabilities.grep.grep_execution_counter", counter_mock):
yield meter_mock, counter_mock
@@ -47,7 +51,7 @@ def mock_capability_init():
Prevent real Capability.__init__ from running. We manually set required
attributes in the instance fixtures afterwards.
"""
- with patch.object(GrepFunctionCall.__bases__[0], '__init__') as m:
+ with patch.object(GrepFunctionCall.__bases__[0], "__init__") as m:
m.return_value = None
yield m
@@ -59,10 +63,10 @@ def default_instance(mock_tracer, mock_meter, mock_capability_init):
name="test_grep",
description="test desc",
original_body="original",
- llm_description="llm desc"
+ llm_description="llm desc",
)
# Manually set attributes that Capability.__init__ would normally set
- inst.name = "test_grep" # Fix: use inst.name instead of inst._name
+ inst._name = "test_grep" # name is a read-only property; set the backing field
inst._description = "test desc"
inst._type = "grep_function_call"
inst._original_body = "original"
@@ -73,18 +77,14 @@ def default_instance(mock_tracer, mock_meter, mock_capability_init):
@pytest.fixture
def instance_with_params(mock_tracer, mock_meter, mock_capability_init):
"""Create instance with default search_params."""
- search_params = {
- "glob": "*.py",
- "output_mode": "content",
- "ignore_case": True
- }
+ search_params = {"glob": "*.py", "output_mode": "content", "ignore_case": True}
inst = GrepFunctionCall(
name="test_grep",
description="test desc",
original_body="original",
- search_params=search_params
+ search_params=search_params,
)
- inst.name = "test_grep" # Fix: use inst.name instead of inst._name
+ inst._name = "test_grep" # name is a read-only property; set the backing field
inst._description = "test desc"
inst._type = "grep_function_call"
inst._original_body = "original"
@@ -96,6 +96,7 @@ def instance_with_params(mock_tracer, mock_meter, mock_capability_init):
# Tests for __init__
# ---------------------------------------------------------------------------
+
def test_init_basic(default_instance, mock_capability_init):
"""Verify attributes are set correctly."""
assert default_instance.name == "test_grep"
@@ -112,6 +113,7 @@ def test_init_with_search_params(instance_with_params):
# Tests for execute method
# ---------------------------------------------------------------------------
+
def test_execute_pattern_missing(default_instance):
"""Raises ValueError when pattern is not provided."""
with pytest.raises(ValueError, match="No search pattern provided"):
@@ -139,6 +141,7 @@ def test_execute_success_no_pagination(default_instance, mock_span):
assert result == "file1.py:1:test\nfile2.py:5:test"
# Counter should be incremented
from scl.capabilities.grep import grep_execution_counter
+
grep_execution_counter.add.assert_called_once_with(1, {"grep.name": "test_grep"})
@@ -188,6 +191,7 @@ def test_execute_records_exception(mock_span, default_instance):
# Tests for _build_command
# ---------------------------------------------------------------------------
+
def test_build_command_minimal(default_instance):
"""Only pattern and default path."""
default_instance._get_grep_binary = MagicMock(return_value="igrep")
@@ -238,23 +242,23 @@ def test_build_command_output_content_default_line_numbers(default_instance):
def test_build_command_output_content_no_line_numbers(default_instance):
default_instance._get_grep_binary = MagicMock(return_value="igrep")
- cmd = default_instance._build_command({
- "pattern": "foo",
- "output_mode": "content",
- "line_numbers": False
- })
+ cmd = default_instance._build_command(
+ {"pattern": "foo", "output_mode": "content", "line_numbers": False}
+ )
assert "-n" not in cmd
def test_build_command_context_lines(default_instance):
default_instance._get_grep_binary = MagicMock(return_value="igrep")
- cmd = default_instance._build_command({
- "pattern": "foo",
- "output_mode": "content",
- "context_before": 2,
- "context_after": 1,
- "context_around": 3
- })
+ cmd = default_instance._build_command(
+ {
+ "pattern": "foo",
+ "output_mode": "content",
+ "context_before": 2,
+ "context_after": 1,
+ "context_around": 3,
+ }
+ )
assert cmd[cmd.index("-B") + 1] == "2"
assert cmd[cmd.index("-A") + 1] == "1"
assert cmd[cmd.index("-C") + 1] == "3"
@@ -285,8 +289,8 @@ def test_build_command_glob_brace_expansion_igrep(default_instance):
cmd = default_instance._build_command({"pattern": "foo", "glob": "*. {js,ts}"})
# tokens: ['*.', 'js', 'ts'] => three -g occurrences
assert cmd.count("-g") == 3
- g_values = [cmd[i+1] for i, val in enumerate(cmd) if val == "-g"]
- assert g_values == ['*.', 'js', 'ts']
+ g_values = [cmd[i + 1] for i, val in enumerate(cmd) if val == "-g"]
+ assert g_values == ["*.", "js", "ts"]
def test_build_command_glob_brace_expansion_grep(default_instance):
@@ -294,8 +298,8 @@ def test_build_command_glob_brace_expansion_grep(default_instance):
default_instance._get_grep_binary = MagicMock(return_value="grep")
cmd = default_instance._build_command({"pattern": "foo", "glob": "*. {js,ts}"})
assert cmd.count("--include") == 3
- includes = [cmd[i+1] for i, val in enumerate(cmd) if val == "--include"]
- assert includes == ['*.', 'js', 'ts']
+ includes = [cmd[i + 1] for i, val in enumerate(cmd) if val == "--include"]
+ assert includes == ["*.", "js", "ts"]
def test_build_command_type_filter_igrep(default_instance):
@@ -323,10 +327,7 @@ def test_build_command_explicit_path(default_instance):
def test_build_command_path_list(default_instance):
"""Multiple paths are all appended."""
default_instance._get_grep_binary = MagicMock(return_value="igrep")
- cmd = default_instance._build_command({
- "pattern": "foo",
- "path": ["/dir1", "/dir2"]
- })
+ cmd = default_instance._build_command({"pattern": "foo", "path": ["/dir1", "/dir2"]})
assert cmd[-2:] == ["/dir1", "/dir2"]
@@ -334,15 +335,19 @@ def test_build_command_path_list(default_instance):
# Tests for _parse_glob
# ---------------------------------------------------------------------------
-@pytest.mark.parametrize("input_glob,expected_list", [
- ("*.py", ["*.py"]),
- ("*.py,*.txt", ["*.py", "*.txt"]),
- ("*.py *.txt", ["*.py", "*.txt"]),
- ("*.py, *.txt", ["*.py", "*.txt"]),
- ("*. {js,ts}", ["*.", "js", "ts"]),
- ("{js,ts}", ["js", "ts"]),
- ("single", ["single"]),
-])
+
+@pytest.mark.parametrize(
+ "input_glob,expected_list",
+ [
+ ("*.py", ["*.py"]),
+ ("*.py,*.txt", ["*.py", "*.txt"]),
+ ("*.py *.txt", ["*.py", "*.txt"]),
+ ("*.py, *.txt", ["*.py", "*.txt"]),
+ ("*. {js,ts}", ["*.", "js", "ts"]),
+ ("{js,ts}", ["js", "ts"]),
+ ("single", ["single"]),
+ ],
+)
def test_parse_glob(default_instance, input_glob, expected_list):
result = default_instance._parse_glob(input_glob)
assert result == expected_list
@@ -357,7 +362,8 @@ def test_parse_glob_complex_mix(default_instance):
# Tests for _get_grep_binary
# ---------------------------------------------------------------------------
-@patch('scl.capabilities.grep.subprocess.run')
+
+@patch("scl.capabilities.grep.subprocess.run")
def test_get_grep_binary_igrep_available(mock_run, default_instance):
"""Preferred binary igrep is found."""
mock_run.side_effect = [MagicMock(returncode=0)]
@@ -366,7 +372,7 @@ def test_get_grep_binary_igrep_available(mock_run, default_instance):
assert mock_run.call_args_list[0][0][0] == ["igrep", "--version"]
-@patch('scl.capabilities.grep.subprocess.run')
+@patch("scl.capabilities.grep.subprocess.run")
def test_get_grep_binary_fallback_to_grep(mock_run, default_instance):
"""Fall back to grep when igrep not found."""
mock_run.side_effect = [FileNotFoundError, MagicMock(returncode=0)]
@@ -374,7 +380,7 @@ def test_get_grep_binary_fallback_to_grep(mock_run, default_instance):
assert bin_name == "grep"
-@patch('scl.capabilities.grep.subprocess.run')
+@patch("scl.capabilities.grep.subprocess.run")
def test_get_grep_binary_neither_available(mock_run, default_instance):
"""Raise FileNotFoundError when neither binary is available."""
mock_run.side_effect = FileNotFoundError
@@ -386,13 +392,14 @@ def test_get_grep_binary_neither_available(mock_run, default_instance):
# Tests for _is_binary_available
# ---------------------------------------------------------------------------
-@patch('scl.capabilities.grep.subprocess.run')
+
+@patch("scl.capabilities.grep.subprocess.run")
def test_is_binary_available_true(mock_run):
mock_run.return_value = MagicMock(returncode=0)
assert GrepFunctionCall._is_binary_available("igrep") is True
-@patch('scl.capabilities.grep.subprocess.run')
+@patch("scl.capabilities.grep.subprocess.run")
def test_is_binary_available_false_not_found(mock_run):
mock_run.side_effect = FileNotFoundError
assert GrepFunctionCall._is_binary_available("nosuch") is False
@@ -402,28 +409,29 @@ def test_is_binary_available_false_not_found(mock_run):
# Tests for _run_command
# ---------------------------------------------------------------------------
-@patch('scl.capabilities.grep.subprocess.run')
+
+@patch("scl.capabilities.grep.subprocess.run")
def test_run_command_success(mock_run, default_instance):
mock_run.return_value = MagicMock(returncode=0, stdout="output")
result = default_instance._run_command(["igrep", "test", "."])
assert result == "output"
-@patch('scl.capabilities.grep.subprocess.run')
+@patch("scl.capabilities.grep.subprocess.run")
def test_run_command_no_match(mock_run, default_instance):
mock_run.return_value = MagicMock(returncode=1, stdout="")
result = default_instance._run_command(["igrep", "test", "."])
assert result == ""
-@patch('scl.capabilities.grep.subprocess.run')
+@patch("scl.capabilities.grep.subprocess.run")
def test_run_command_error(mock_run, default_instance):
mock_run.return_value = MagicMock(returncode=2, stderr="permission denied")
with pytest.raises(RuntimeError, match="grep failed with code 2"):
default_instance._run_command(["igrep", "test", "."])
-@patch('scl.capabilities.grep.subprocess.run')
+@patch("scl.capabilities.grep.subprocess.run")
def test_run_command_binary_not_found(mock_run, default_instance):
mock_run.side_effect = FileNotFoundError
with pytest.raises(FileNotFoundError):
@@ -434,6 +442,7 @@ def test_run_command_binary_not_found(mock_run, default_instance):
# Test __repr__
# ---------------------------------------------------------------------------
+
def test_repr(default_instance):
default_instance.search_params["pattern"] = "hello"
rep = repr(default_instance)
@@ -445,27 +454,29 @@ def test_repr(default_instance):
# Integration test: real grep execution on grep.py source
# ---------------------------------------------------------------------------
+
def test_real_grep_on_source_file(default_instance, mock_tracer, mock_meter):
"""
Real grep execution on grep.py source using the standard `grep` binary.
Patches `_get_grep_binary` to force `grep` to avoid dependency on igrep.
"""
- with patch.object(default_instance, '_get_grep_binary', return_value='grep'):
- # Construct absolute path to grep.py (two levels up from test dir)
+ with patch.object(default_instance, "_get_grep_binary", return_value="grep"):
+ # Construct absolute path to scl/capabilities/grep.py (repo root is two
+ # levels up from this test dir: tests/capabilities/).
test_dir = os.path.dirname(os.path.abspath(__file__))
source_path = os.path.normpath(
- os.path.join(test_dir, "..", "..", "capabilities", "grep.py")
+ os.path.join(test_dir, "..", "..", "scl", "capabilities", "grep.py")
)
pattern = "Grep Function Call Module" # unique phrase in the docstring
- result = default_instance.execute({
- "pattern": pattern,
- "path": source_path,
- "output_mode": "content",
- "line_numbers": False,
- })
+ result = default_instance.execute(
+ {
+ "pattern": pattern,
+ "path": source_path,
+ "output_mode": "content",
+ "line_numbers": False,
+ }
+ )
- assert pattern in result, (
- f"Pattern {pattern!r} not found in output:\n{result}"
- )
\ No newline at end of file
+ assert pattern in result, f"Pattern {pattern!r} not found in output:\n{result}"
diff --git a/tests/listener/__init__.py b/tests/listener/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scl/test/listener/test_file_watcher.py b/tests/listener/test_file_watcher.py
similarity index 81%
rename from scl/test/listener/test_file_watcher.py
rename to tests/listener/test_file_watcher.py
index dbafc93..f3b5536 100644
--- a/scl/test/listener/test_file_watcher.py
+++ b/tests/listener/test_file_watcher.py
@@ -2,25 +2,25 @@
Tests for File Watcher module.
"""
-import os
import json
-import yaml
+import os
import shutil
from pathlib import Path
-from unittest.mock import MagicMock, patch, call, mock_open
+from unittest.mock import MagicMock, call, mock_open, patch
import pytest
+import yaml
+from watchdog.events import FileCreatedEvent
+from watchdog.observers import Observer
# Import the module under test
from scl.listener.file_watch import FileHandler
-from scl.queue.taskQueue import TaskQueue
-from scl.queue.capTaskQueues import CapabilityTaskQueues
-from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
-from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
-from scl.meta.task import Task
from scl.meta.captask import CapTask
-from watchdog.events import FileCreatedEvent
-from watchdog.observers import Observer
+from scl.meta.task import Task
+from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
+from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
+from scl.queue.cap_task_queues import CapabilityTaskQueues
+from scl.queue.task_queue import TaskQueue
@pytest.fixture
@@ -56,10 +56,18 @@ def temp_watch_dir(tmp_path):
@pytest.fixture
-def handler(mock_task_queue, mock_captask_queue, mock_waiting_approval_queue, mock_waiting_captask_queue, temp_watch_dir):
+def handler(
+ mock_task_queue,
+ mock_captask_queue,
+ mock_waiting_approval_queue,
+ mock_waiting_captask_queue,
+ temp_watch_dir,
+):
"""Fixture for a FileHandler instance with mocked dependencies."""
- with patch('scl.listener.file_watch.meter') as mock_meter, \
- patch('scl.listener.file_watch.tracer') as mock_tracer:
+ with (
+ patch("scl.listener.file_watch.meter") as mock_meter,
+ patch("scl.listener.file_watch.tracer") as mock_tracer,
+ ):
# Mock counter creation - create separate counters for each metric
mock_meter.create_counter.side_effect = lambda name, description: MagicMock()
@@ -68,7 +76,7 @@ def handler(mock_task_queue, mock_captask_queue, mock_waiting_approval_queue, mo
task_queue=mock_task_queue,
captask_queue=mock_captask_queue,
waiting_approval_queue=mock_waiting_approval_queue,
- waiting_captask_queue=mock_waiting_captask_queue
+ waiting_captask_queue=mock_waiting_captask_queue,
)
handler.logger = MagicMock()
@@ -80,7 +88,14 @@ def handler(mock_task_queue, mock_captask_queue, mock_waiting_approval_queue, mo
class TestFileHandler:
"""Test cases for FileHandler."""
- def test_init_creates_directories(self, mock_task_queue, mock_captask_queue, mock_waiting_approval_queue, mock_waiting_captask_queue, temp_watch_dir):
+ def test_init_creates_directories(
+ self,
+ mock_task_queue,
+ mock_captask_queue,
+ mock_waiting_approval_queue,
+ mock_waiting_captask_queue,
+ temp_watch_dir,
+ ):
"""Should create processed, processedCapTask, waitingapproval, waitingCapTask, and failed directories on initialization."""
processed_dir = Path(temp_watch_dir) / "processed"
processed_captask_dir = Path(temp_watch_dir) / "processedCapTask"
@@ -95,15 +110,17 @@ def test_init_creates_directories(self, mock_task_queue, mock_captask_queue, moc
assert not waiting_captask_dir.exists()
assert not failed_dir.exists()
- with patch('scl.listener.file_watch.meter') as mock_meter, \
- patch('scl.listener.file_watch.tracer'):
+ with (
+ patch("scl.listener.file_watch.meter") as mock_meter,
+ patch("scl.listener.file_watch.tracer"),
+ ):
mock_meter.create_counter.return_value = MagicMock()
FileHandler(
watch_path=temp_watch_dir,
task_queue=mock_task_queue,
captask_queue=mock_captask_queue,
waiting_approval_queue=mock_waiting_approval_queue,
- waiting_captask_queue=mock_waiting_captask_queue
+ waiting_captask_queue=mock_waiting_captask_queue,
)
assert processed_dir.exists()
@@ -136,9 +153,9 @@ def test_on_created_valid_json_task(self, handler, tmp_path):
mock_task.hash = "123"
mock_task.approval = True
mock_task.cap_tasks = []
- with patch.object(Task, 'from_dict', return_value=mock_task):
+ with patch.object(Task, "from_dict", return_value=mock_task):
# Mock shutil.move to avoid actual move
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
# Assertions
@@ -146,11 +163,12 @@ def test_on_created_valid_json_task(self, handler, tmp_path):
handler.task_file_approved_counter.add.assert_called_once_with(1)
handler.task_queue.add.assert_called_once_with(mock_task)
mock_move.assert_called_once_with(
- str(file_path),
- os.path.join(handler.processed_dir, "task.json")
+ str(file_path), os.path.join(handler.processed_dir, "task.json")
)
handler.logger.info.assert_any_call(f"New file detected: {file_path}")
- handler.logger.info.assert_any_call(f"Task file moved: {os.path.join(handler.processed_dir, 'task.json')}")
+ handler.logger.info.assert_any_call(
+ f"Task file moved: {os.path.join(handler.processed_dir, 'task.json')}"
+ )
def test_on_created_valid_yaml_task(self, handler, tmp_path):
"""Should process a valid YAML task file."""
@@ -165,15 +183,14 @@ def test_on_created_valid_yaml_task(self, handler, tmp_path):
mock_task.hash = "456"
mock_task.approval = True
mock_task.cap_tasks = []
- with patch.object(Task, 'from_dict', return_value=mock_task):
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch.object(Task, "from_dict", return_value=mock_task):
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
handler.task_file_approved_counter.add.assert_called_once_with(1)
handler.task_queue.add.assert_called_once_with(mock_task)
mock_move.assert_called_once_with(
- str(file_path),
- os.path.join(handler.processed_dir, "task.yaml")
+ str(file_path), os.path.join(handler.processed_dir, "task.yaml")
)
def test_on_created_valid_json_captask(self, handler, tmp_path):
@@ -188,8 +205,8 @@ def test_on_created_valid_json_captask(self, handler, tmp_path):
# Mock CapTask.from_dict
mock_captask = MagicMock()
mock_captask.hash = "abc123"
- with patch.object(CapTask, 'from_dict', return_value=mock_captask):
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch.object(CapTask, "from_dict", return_value=mock_captask):
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
# Assertions
@@ -197,11 +214,12 @@ def test_on_created_valid_json_captask(self, handler, tmp_path):
handler.captask_file_valid_counter.add.assert_called_once_with(1)
handler.captask_queue.add.assert_called_once_with(mock_captask)
mock_move.assert_called_once_with(
- str(file_path),
- os.path.join(handler.processed_captask_dir, "captask.json")
+ str(file_path), os.path.join(handler.processed_captask_dir, "captask.json")
)
handler.logger.info.assert_any_call(f"New file detected: {file_path}")
- handler.logger.info.assert_any_call(f"CapTask file moved to processedCapTask: {os.path.join(handler.processed_captask_dir, 'captask.json')}")
+ handler.logger.info.assert_any_call(
+ f"CapTask file moved to processedCapTask: {os.path.join(handler.processed_captask_dir, 'captask.json')}"
+ )
def test_on_created_valid_yaml_captask(self, handler, tmp_path):
"""Should process a valid YAML CapTask file."""
@@ -214,15 +232,14 @@ def test_on_created_valid_yaml_captask(self, handler, tmp_path):
mock_captask = MagicMock()
mock_captask.hash = "xyz789"
- with patch.object(CapTask, 'from_dict', return_value=mock_captask):
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch.object(CapTask, "from_dict", return_value=mock_captask):
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
handler.captask_file_valid_counter.add.assert_called_once_with(1)
handler.captask_queue.add.assert_called_once_with(mock_captask)
mock_move.assert_called_once_with(
- str(file_path),
- os.path.join(handler.processed_captask_dir, "captask.yaml")
+ str(file_path), os.path.join(handler.processed_captask_dir, "captask.yaml")
)
def test_on_created_unapproved_task(self, handler, tmp_path):
@@ -238,15 +255,14 @@ def test_on_created_unapproved_task(self, handler, tmp_path):
mock_task.hash = "789"
mock_task.approval = False
mock_task.cap_tasks = []
- with patch.object(Task, 'from_dict', return_value=mock_task):
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch.object(Task, "from_dict", return_value=mock_task):
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
handler.task_file_unapproved_counter.add.assert_called_once_with(1)
handler.waiting_approval_queue.add.assert_called_once_with(mock_task)
mock_move.assert_called_once_with(
- str(file_path),
- os.path.join(handler.waiting_approval_dir, "task.json")
+ str(file_path), os.path.join(handler.waiting_approval_dir, "task.json")
)
def test_on_created_task_pending_captasks(self, handler, tmp_path):
@@ -264,15 +280,14 @@ def test_on_created_task_pending_captasks(self, handler, tmp_path):
mock_task.hash = "101"
mock_task.approval = True
mock_task.cap_tasks = [mock_cap]
- with patch.object(Task, 'from_dict', return_value=mock_task):
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch.object(Task, "from_dict", return_value=mock_task):
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
handler.task_file_pending_captasks_counter.add.assert_called_once_with(1)
handler.waiting_captask_queue.push.assert_called_once_with(mock_task)
mock_move.assert_called_once_with(
- str(file_path),
- os.path.join(handler.waiting_captask_dir, "task.json")
+ str(file_path), os.path.join(handler.waiting_captask_dir, "task.json")
)
def test_on_created_unsupported_extension(self, handler, tmp_path):
@@ -287,7 +302,9 @@ def test_on_created_unsupported_extension(self, handler, tmp_path):
handler.on_created(event)
handler.file_invalid_counter.add.assert_called_once_with(1)
- handler._move_to_failed.assert_called_once_with(str(file_path), reason="unsupported_extension")
+ handler._move_to_failed.assert_called_once_with(
+ str(file_path), reason="unsupported_extension"
+ )
handler.task_queue.add.assert_not_called()
handler.captask_queue.add.assert_not_called()
@@ -319,7 +336,9 @@ def test_on_created_unrecognized_format(self, handler, tmp_path):
handler.on_created(event)
handler.file_invalid_counter.add.assert_called_once_with(1)
- handler._move_to_failed.assert_called_once_with(str(file_path), reason="unrecognized_format")
+ handler._move_to_failed.assert_called_once_with(
+ str(file_path), reason="unrecognized_format"
+ )
handler.task_queue.add.assert_not_called()
handler.captask_queue.add.assert_not_called()
@@ -333,15 +352,17 @@ def test_on_created_queue_error(self, handler, tmp_path):
event.src_path = str(file_path)
mock_task = MagicMock()
- with patch.object(Task, 'from_dict', return_value=mock_task):
+ with patch.object(Task, "from_dict", return_value=mock_task):
handler.task_queue.add.side_effect = Exception("Queue unavailable")
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
# Should not move to processed
mock_move.assert_not_called()
# FIXED: actual code uses reason="task_processing_error"
- handler._move_to_failed.assert_called_once_with(str(file_path), reason="task_processing_error")
+ handler._move_to_failed.assert_called_once_with(
+ str(file_path), reason="task_processing_error"
+ )
# Valid counter should NOT be incremented (because queue failed)
handler.task_file_approved_counter.add.assert_not_called()
@@ -392,7 +413,7 @@ def test_move_to_failed_success(self, handler, tmp_path):
# Use actual shutil for this test (or mock it)
handler._move_to_failed = FileHandler._move_to_failed.__get__(handler, FileHandler)
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler._move_to_failed(str(src), reason="parse_error")
expected_dest = str(failed_dir / "bad.parse_error.json")
@@ -403,10 +424,12 @@ def test_move_to_failed_exception_handling(self, handler, tmp_path):
src = tmp_path / "bad.json"
src.write_text("content")
- with patch.object(handler, 'logger') as mock_logger:
+ with patch.object(handler, "logger") as mock_logger:
# Restore the real _move_to_failed method
handler._move_to_failed = FileHandler._move_to_failed.__get__(handler, FileHandler)
- with patch('scl.listener.file_watch.shutil.move', side_effect=PermissionError("Access denied")):
+ with patch(
+ "scl.listener.file_watch.shutil.move", side_effect=PermissionError("Access denied")
+ ):
handler._move_to_failed(str(src), reason="parse_error")
mock_logger.error.assert_called_once()
@@ -414,7 +437,7 @@ def test_move_to_failed_exception_handling(self, handler, tmp_path):
def test_start_returns_observer(self, handler):
"""start() should create and start an Observer."""
- with patch('scl.listener.file_watch.Observer') as MockObserver:
+ with patch("scl.listener.file_watch.Observer") as MockObserver:
mock_observer_instance = MagicMock()
MockObserver.return_value = mock_observer_instance
@@ -437,18 +460,17 @@ def test_tracing_instrumentation(self, handler, tmp_path):
event.src_path = str(file_path)
mock_span = MagicMock()
- with patch('scl.listener.file_watch.trace.get_current_span', return_value=mock_span):
+ with patch("scl.listener.file_watch.trace.get_current_span", return_value=mock_span):
mock_task = MagicMock()
- with patch.object(Task, 'from_dict', return_value=mock_task):
- with patch('scl.listener.file_watch.shutil.move'):
+ with patch.object(Task, "from_dict", return_value=mock_task):
+ with patch("scl.listener.file_watch.shutil.move"):
handler.on_created(event)
mock_span.set_attribute.assert_any_call("file.path", str(file_path))
mock_span.set_attribute.assert_any_call("file.name", "task.json")
# Destination should also be set
mock_span.set_attribute.assert_any_call(
- "file.moved_to",
- os.path.join(handler.processed_dir, "task.json")
+ "file.moved_to", os.path.join(handler.processed_dir, "task.json")
)
mock_span.set_attribute.assert_any_call("file.type", "Task")
@@ -466,8 +488,8 @@ def test_metrics_counters_called(self, handler, tmp_path):
mock_task.hash = "1"
mock_task.approval = True
mock_task.cap_tasks = []
- with patch.object(Task, 'from_dict', return_value=mock_task):
- with patch('scl.listener.file_watch.shutil.move'):
+ with patch.object(Task, "from_dict", return_value=mock_task):
+ with patch("scl.listener.file_watch.shutil.move"):
handler.on_created(event)
handler.file_receive_counter.add.assert_called_once_with(1)
@@ -484,21 +506,23 @@ def test_on_created_captask_queue_error(self, handler, tmp_path):
event.src_path = str(file_path)
mock_captask = MagicMock()
- with patch.object(CapTask, 'from_dict', return_value=mock_captask):
+ with patch.object(CapTask, "from_dict", return_value=mock_captask):
handler.captask_queue.add.side_effect = Exception("CapTask Queue unavailable")
- with patch('scl.listener.file_watch.shutil.move') as mock_move:
+ with patch("scl.listener.file_watch.shutil.move") as mock_move:
handler.on_created(event)
# Should not move to processedCapTask
mock_move.assert_not_called()
- handler._move_to_failed.assert_called_once_with(str(file_path), reason="captask_queue_error")
+ handler._move_to_failed.assert_called_once_with(
+ str(file_path), reason="captask_queue_error"
+ )
# Valid counter should NOT be incremented (because queue failed)
handler.captask_file_valid_counter.add.assert_not_called()
def test_all_captasks_completed(self, handler):
"""_all_captasks_completed should check CapTask status."""
mock_task = MagicMock()
-
+
# All completed
cap1 = MagicMock()
cap1.status = "Processed"
@@ -506,13 +530,13 @@ def test_all_captasks_completed(self, handler):
cap2.status = "Error"
mock_task.cap_tasks = [cap1, cap2]
assert handler._all_captasks_completed(mock_task) is True
-
+
# One pending
cap3 = MagicMock()
cap3.status = "Pending"
mock_task.cap_tasks = [cap1, cap3]
assert handler._all_captasks_completed(mock_task) is False
-
+
# Empty list
mock_task.cap_tasks = []
- assert handler._all_captasks_completed(mock_task) is True
\ No newline at end of file
+ assert handler._all_captasks_completed(mock_task) is True
diff --git a/scl/test/listener/test_internal_watch.py b/tests/listener/test_internal_watch.py
similarity index 76%
rename from scl/test/listener/test_internal_watch.py
rename to tests/listener/test_internal_watch.py
index 9f51a37..7620ad3 100644
--- a/scl/test/listener/test_internal_watch.py
+++ b/tests/listener/test_internal_watch.py
@@ -2,16 +2,17 @@
Tests for InternalWatcher (updated file‑based implementation)
"""
-import os
import json
+import logging
+import os
+from unittest.mock import MagicMock, Mock, call, mock_open, patch
+
import pytest
-from unittest.mock import Mock, patch, MagicMock, mock_open, call
from opentelemetry import trace
-import logging
-from scl.listener.Interal_watch import InternalWatcher
-from scl.meta.task import Task
+from scl.listener.internal_watch import InternalWatcher
from scl.meta.captask import CapTask
+from scl.meta.task import Task
class TestInternalWatcher:
@@ -33,30 +34,36 @@ def mock_task(self):
"hash": "abc123hash",
"id": "task-456",
"type": "test-type",
- "data": "sample"
+ "data": "sample",
}
# Make to_dict return the actual dict when called
- task.to_dict = Mock(return_value={
- "hash": "abc123hash",
- "id": "task-456",
- "type": "test-type",
- "data": "sample"
- })
+ task.to_dict = Mock(
+ return_value={
+ "hash": "abc123hash",
+ "id": "task-456",
+ "type": "test-type",
+ "data": "sample",
+ }
+ )
return task
@pytest.fixture
def internal_watcher(self, watch_path):
"""Fixture providing an InternalWatcher with mocked OpenTelemetry components."""
- with patch('scl.listener.Interal_watch.meter') as mock_meter, \
- patch('scl.listener.Interal_watch.tracer') as mock_tracer:
+ with (
+ patch("scl.listener.internal_watch.meter") as mock_meter,
+ patch("scl.listener.internal_watch.tracer") as mock_tracer,
+ ):
mock_success_counter = Mock()
mock_error_counter = Mock()
mock_captask_success_counter = Mock()
mock_captask_error_counter = Mock()
# meter.create_counter called 4 times: Task success/error, CapTask success/error
mock_meter.create_counter.side_effect = [
- mock_success_counter, mock_error_counter,
- mock_captask_success_counter, mock_captask_error_counter
+ mock_success_counter,
+ mock_error_counter,
+ mock_captask_success_counter,
+ mock_captask_error_counter,
]
watcher = InternalWatcher(watch_path)
@@ -70,15 +77,19 @@ def internal_watcher(self, watch_path):
def test_init_creates_watch_directory_and_counters(self, watch_path):
"""Test that __init__ creates the watch directory, counters, and logs."""
- with patch('scl.listener.Interal_watch.meter') as mock_meter, \
- patch('scl.listener.Interal_watch.tracer'):
-
+ with (
+ patch("scl.listener.internal_watch.meter") as mock_meter,
+ patch("scl.listener.internal_watch.tracer"),
+ ):
mock_counter1 = Mock()
mock_counter2 = Mock()
mock_counter3 = Mock()
mock_counter4 = Mock()
mock_meter.create_counter.side_effect = [
- mock_counter1, mock_counter2, mock_counter3, mock_counter4
+ mock_counter1,
+ mock_counter2,
+ mock_counter3,
+ mock_counter4,
]
watcher = InternalWatcher(watch_path)
@@ -90,19 +101,19 @@ def test_init_creates_watch_directory_and_counters(self, watch_path):
assert mock_meter.create_counter.call_count == 4
mock_meter.create_counter.assert_any_call(
"internal_task_write",
- description="Number of internal Task instances written to file"
+ description="Number of internal Task instances written to file",
)
mock_meter.create_counter.assert_any_call(
"internal_task_error",
- description="Number of errors while writing internal Task instances to file"
+ description="Number of errors while writing internal Task instances to file",
)
mock_meter.create_counter.assert_any_call(
"internal_captask_write",
- description="Number of internal CapTask instances written to file"
+ description="Number of internal CapTask instances written to file",
)
mock_meter.create_counter.assert_any_call(
"internal_captask_error",
- description="Number of errors while writing internal CapTask instances to file"
+ description="Number of errors while writing internal CapTask instances to file",
)
assert watcher.internal_task_counter == mock_counter1
assert watcher.internal_task_error_counter == mock_counter2
@@ -114,11 +125,14 @@ def test_init_invalid_format_raises_valueerror(self, watch_path):
with pytest.raises(ValueError, match="output_format must be 'json' or 'yaml', got 'xml'"):
InternalWatcher(watch_path, output_format="xml")
- def test_add_valid_task_writes_file_and_returns_hash(self, internal_watcher, mock_task, watch_path):
+ def test_add_valid_task_writes_file_and_returns_hash(
+ self, internal_watcher, mock_task, watch_path
+ ):
"""Test adding a valid Task writes the correct JSON file and returns the hash."""
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span, \
- patch('builtins.open', mock_open()) as mock_file:
-
+ with (
+ patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span,
+ patch("builtins.open", mock_open()) as mock_file,
+ ):
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -130,12 +144,12 @@ def test_add_valid_task_writes_file_and_returns_hash(self, internal_watcher, moc
# Verify file open and write
expected_file_path = os.path.join(watch_path, "abc123hash.json")
- mock_file.assert_called_once_with(expected_file_path, 'w', encoding='utf-8')
+ mock_file.assert_called_once_with(expected_file_path, "w", encoding="utf-8")
handle = mock_file()
# Verify write was called (json.dump calls write multiple times)
assert handle.write.called
# Check that JSON contains expected data by collecting all write calls
- written_content = ''.join(str(call[0][0]) for call in handle.write.call_args_list)
+ written_content = "".join(str(call[0][0]) for call in handle.write.call_args_list)
written_json = json.loads(written_content)
assert written_json == mock_task.to_dict.return_value
@@ -158,8 +172,8 @@ def test_add_task_missing_hash_raises_valueerror(self, internal_watcher):
task_no_hash.to_dict.return_value = {}
# Explicitly set hash to None to simulate missing hash
type(task_no_hash).hash = property(lambda self: None)
-
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span:
+
+ with patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span:
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -168,7 +182,9 @@ def test_add_task_missing_hash_raises_valueerror(self, internal_watcher):
# Span error attributes set
mock_span.set_attribute.assert_any_call("error", True)
- mock_span.set_attribute.assert_any_call("error.message", "Task object missing 'hash' attribute")
+ mock_span.set_attribute.assert_any_call(
+ "error.message", "Task object missing 'hash' attribute"
+ )
# Error counter incremented
internal_watcher._mock_error_counter.add.assert_called_once_with(1)
@@ -177,7 +193,7 @@ def test_add_invalid_type_raises_typeerror(self, internal_watcher):
"""Test that passing a non-Task/CapTask object raises TypeError."""
not_a_task = {"foo": "bar"}
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span:
+ with patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span:
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -186,14 +202,18 @@ def test_add_invalid_type_raises_typeerror(self, internal_watcher):
# Span error attributes set
mock_span.set_attribute.assert_any_call("error", True)
- mock_span.set_attribute.assert_any_call("error.message", "Expected Task or CapTask instance, got dict")
+ mock_span.set_attribute.assert_any_call(
+ "error.message", "Expected Task or CapTask instance, got dict"
+ )
def test_add_file_write_failure_logs_and_raises(self, internal_watcher, mock_task):
"""Test that if file writing fails, the exception is logged and re-raised."""
test_error = OSError("Disk full")
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span, \
- patch('builtins.open', side_effect=test_error):
+ with (
+ patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span,
+ patch("builtins.open", side_effect=test_error),
+ ):
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -216,8 +236,10 @@ def test_add_task_with_missing_id_and_type_uses_unknown(self, internal_watcher,
task.to_dict.return_value = {"hash": "hash123"}
# No id or type attributes intentionally
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span, \
- patch('builtins.open', mock_open()):
+ with (
+ patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span,
+ patch("builtins.open", mock_open()),
+ ):
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -231,12 +253,13 @@ def test_add_task_with_missing_id_and_type_uses_unknown(self, internal_watcher,
# -------------------------------------------------------------------------
def test_logging_on_success_and_error(self, watch_path, mock_task):
"""Verify appropriate log messages are emitted for success and error cases."""
- with patch('scl.listener.Interal_watch.meter'), \
- patch('scl.listener.Interal_watch.tracer'), \
- patch('scl.listener.Interal_watch.trace.get_current_span'), \
- patch('builtins.open', mock_open()), \
- patch('scl.listener.Interal_watch.logger') as mock_logger:
-
+ with (
+ patch("scl.listener.internal_watch.meter"),
+ patch("scl.listener.internal_watch.tracer"),
+ patch("scl.listener.internal_watch.trace.get_current_span"),
+ patch("builtins.open", mock_open()),
+ patch("scl.listener.internal_watch.logger") as mock_logger,
+ ):
watcher = InternalWatcher(watch_path)
watcher.logger = mock_logger
@@ -244,12 +267,13 @@ def test_logging_on_success_and_error(self, watch_path, mock_task):
watcher.add(mock_task)
mock_logger.debug.assert_called_with(
"Internally generated Task received: id=%s, hash=%s, type=%s",
- mock_task.id, mock_task.hash, mock_task.type
+ mock_task.id,
+ mock_task.hash,
+ mock_task.type,
)
- expected_path = os.path.join(watch_path, mock_task.hash + '.json')
+ expected_path = os.path.join(watch_path, mock_task.hash + ".json")
mock_logger.info.assert_called_with(
- "Internal Task %s written to file: %s",
- mock_task.hash, expected_path
+ "Internal Task %s written to file: %s", mock_task.hash, expected_path
)
# Error: invalid type
@@ -261,13 +285,14 @@ def test_logging_on_success_and_error(self, watch_path, mock_task):
# Error: file write failure
mock_logger.reset_mock()
test_error = OSError("Permission denied")
- with patch('builtins.open', side_effect=test_error):
+ with patch("builtins.open", side_effect=test_error):
with pytest.raises(OSError):
watcher.add(mock_task)
mock_logger.error.assert_called_with(
"Failed to write internal Task %s to file: %s",
- mock_task.hash, test_error,
- exc_info=True
+ mock_task.hash,
+ test_error,
+ exc_info=True,
)
def test_add_valid_captask_writes_file_and_returns_hash(self, internal_watcher, watch_path):
@@ -278,12 +303,13 @@ def test_add_valid_captask_writes_file_and_returns_hash(self, internal_watcher,
mock_captask.to_dict.return_value = {
"hash": "captask789hash",
"cap_name": "email",
- "args": ["to@example.com"]
+ "args": ["to@example.com"],
}
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span, \
- patch('builtins.open', mock_open()) as mock_file:
-
+ with (
+ patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span,
+ patch("builtins.open", mock_open()) as mock_file,
+ ):
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -295,12 +321,12 @@ def test_add_valid_captask_writes_file_and_returns_hash(self, internal_watcher,
# Verify file open and write
expected_file_path = os.path.join(watch_path, "captask789hash.json")
- mock_file.assert_called_once_with(expected_file_path, 'w', encoding='utf-8')
+ mock_file.assert_called_once_with(expected_file_path, "w", encoding="utf-8")
handle = mock_file()
# Verify write was called
assert handle.write.called
# Check that JSON contains expected data
- written_content = ''.join(str(call[0][0]) for call in handle.write.call_args_list)
+ written_content = "".join(str(call[0][0]) for call in handle.write.call_args_list)
written_json = json.loads(written_content)
assert written_json == mock_captask.to_dict.return_value
@@ -322,8 +348,8 @@ def test_add_captask_missing_hash_raises_valueerror(self, internal_watcher):
captask_no_hash.to_dict.return_value = {}
# Explicitly set hash to None to simulate missing hash
type(captask_no_hash).hash = property(lambda self: None)
-
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span:
+
+ with patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span:
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -332,7 +358,9 @@ def test_add_captask_missing_hash_raises_valueerror(self, internal_watcher):
# Span error attributes set
mock_span.set_attribute.assert_any_call("error", True)
- mock_span.set_attribute.assert_any_call("error.message", "CapTask object missing 'hash' attribute")
+ mock_span.set_attribute.assert_any_call(
+ "error.message", "CapTask object missing 'hash' attribute"
+ )
# CapTask error counter incremented
internal_watcher._mock_captask_error_counter.add.assert_called_once_with(1)
@@ -344,8 +372,10 @@ def test_add_captask_with_missing_cap_name_uses_unknown(self, internal_watcher,
captask.to_dict.return_value = {"hash": "captaskhash456"}
# No cap_name attribute intentionally
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span, \
- patch('builtins.open', mock_open()):
+ with (
+ patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span,
+ patch("builtins.open", mock_open()),
+ ):
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -359,11 +389,13 @@ def test_captask_file_write_failure_logs_and_raises(self, internal_watcher, watc
mock_captask.hash = "captask123"
mock_captask.cap_name = "test"
mock_captask.to_dict.return_value = {"hash": "captask123", "cap_name": "test"}
-
+
test_error = OSError("Permission denied")
- with patch('scl.listener.Interal_watch.trace.get_current_span') as mock_get_span, \
- patch('builtins.open', side_effect=test_error):
+ with (
+ patch("scl.listener.internal_watch.trace.get_current_span") as mock_get_span,
+ patch("builtins.open", side_effect=test_error),
+ ):
mock_span = Mock()
mock_get_span.return_value = mock_span
@@ -389,12 +421,13 @@ def test_captask_logging_on_success_and_error(self, watch_path):
mock_captask.cap_name = "notification"
mock_captask.to_dict.return_value = {"hash": "captask999", "cap_name": "notification"}
- with patch('scl.listener.Interal_watch.meter'), \
- patch('scl.listener.Interal_watch.tracer'), \
- patch('scl.listener.Interal_watch.trace.get_current_span'), \
- patch('builtins.open', mock_open()), \
- patch('scl.listener.Interal_watch.logger') as mock_logger:
-
+ with (
+ patch("scl.listener.internal_watch.meter"),
+ patch("scl.listener.internal_watch.tracer"),
+ patch("scl.listener.internal_watch.trace.get_current_span"),
+ patch("builtins.open", mock_open()),
+ patch("scl.listener.internal_watch.logger") as mock_logger,
+ ):
watcher = InternalWatcher(watch_path)
watcher.logger = mock_logger
@@ -402,24 +435,25 @@ def test_captask_logging_on_success_and_error(self, watch_path):
watcher.add(mock_captask)
mock_logger.debug.assert_called_with(
"Internally generated CapTask received: cap_name=%s, hash=%s",
- mock_captask.cap_name, mock_captask.hash
+ mock_captask.cap_name,
+ mock_captask.hash,
)
- expected_path = os.path.join(watch_path, mock_captask.hash + '.json')
+ expected_path = os.path.join(watch_path, mock_captask.hash + ".json")
mock_logger.info.assert_called_with(
- "Internal CapTask %s written to file: %s",
- mock_captask.hash, expected_path
+ "Internal CapTask %s written to file: %s", mock_captask.hash, expected_path
)
# Error: file write failure
mock_logger.reset_mock()
test_error = OSError("Disk full")
- with patch('builtins.open', side_effect=test_error):
+ with patch("builtins.open", side_effect=test_error):
with pytest.raises(OSError):
watcher.add(mock_captask)
mock_logger.error.assert_called_with(
"Failed to write internal CapTask %s to file: %s",
- mock_captask.hash, test_error,
- exc_info=True
+ mock_captask.hash,
+ test_error,
+ exc_info=True,
)
# -------------------------------------------------------------------------
@@ -429,18 +463,20 @@ def test_add_task_yaml_format(self, watch_path, mock_task):
"""Test that a Task is written as YAML when output_format is 'yaml'."""
import yaml
- with patch('scl.listener.Interal_watch.meter'), \
- patch('scl.listener.Interal_watch.tracer'), \
- patch('scl.listener.Interal_watch.trace.get_current_span'), \
- patch('builtins.open', mock_open()) as mock_file:
+ with (
+ patch("scl.listener.internal_watch.meter"),
+ patch("scl.listener.internal_watch.tracer"),
+ patch("scl.listener.internal_watch.trace.get_current_span"),
+ patch("builtins.open", mock_open()) as mock_file,
+ ):
watcher = InternalWatcher(watch_path, output_format="yaml")
# ensure logger stays mock-free; not checked
watcher.logger = Mock()
watcher.add(mock_task)
- expected_path = os.path.join(watch_path, mock_task.hash + '.yaml')
- mock_file.assert_called_once_with(expected_path, 'w', encoding='utf-8')
+ expected_path = os.path.join(watch_path, mock_task.hash + ".yaml")
+ mock_file.assert_called_once_with(expected_path, "w", encoding="utf-8")
handle = mock_file()
# yaml.dump should have been called; we cannot easily inspect the written data,
# but we can verify that the file was opened with the correct name.
@@ -456,16 +492,18 @@ def test_add_captask_yaml_format(self, watch_path):
mock_captask.cap_name = "test"
mock_captask.to_dict.return_value = {"hash": "captask789", "cap_name": "test"}
- with patch('scl.listener.Interal_watch.meter'), \
- patch('scl.listener.Interal_watch.tracer'), \
- patch('scl.listener.Interal_watch.trace.get_current_span'), \
- patch('builtins.open', mock_open()) as mock_file:
+ with (
+ patch("scl.listener.internal_watch.meter"),
+ patch("scl.listener.internal_watch.tracer"),
+ patch("scl.listener.internal_watch.trace.get_current_span"),
+ patch("builtins.open", mock_open()) as mock_file,
+ ):
watcher = InternalWatcher(watch_path, output_format="yaml")
watcher.logger = Mock()
watcher.add(mock_captask)
- expected_path = os.path.join(watch_path, mock_captask.hash + '.yaml')
- mock_file.assert_called_once_with(expected_path, 'w', encoding='utf-8')
+ expected_path = os.path.join(watch_path, mock_captask.hash + ".yaml")
+ mock_file.assert_called_once_with(expected_path, "w", encoding="utf-8")
handle = mock_file()
- assert handle.write.called
\ No newline at end of file
+ assert handle.write.called
diff --git a/scl/test/listener/test_restful_handler.py b/tests/listener/test_restful_handler.py
similarity index 89%
rename from scl/test/listener/test_restful_handler.py
rename to tests/listener/test_restful_handler.py
index e4ec607..f0bb224 100644
--- a/scl/test/listener/test_restful_handler.py
+++ b/tests/listener/test_restful_handler.py
@@ -2,24 +2,24 @@
Unit tests for scl.listener.restful_watch.RestFulHandler
"""
-import pytest
import json
import os
from pathlib import Path
-from unittest.mock import AsyncMock, MagicMock, patch, ANY, mock_open
+from unittest.mock import ANY, AsyncMock, MagicMock, mock_open, patch
+import pytest
from fastapi import HTTPException
from opentelemetry import trace
from scl.listener.restful_watch import RestFulHandler
-from scl.meta.task import Task
from scl.meta.captask import CapTask
+from scl.meta.task import Task
@pytest.fixture
def mock_tracer():
"""Mock OpenTelemetry tracer."""
- with patch('scl.otel.otel.tracer') as mock_tracer:
+ with patch("scl.otel.otel.tracer") as mock_tracer:
# Configure start_as_current_span to act as a no-op decorator that returns a mock span
mock_span = MagicMock()
mock_span.__enter__ = MagicMock(return_value=mock_span)
@@ -29,7 +29,9 @@ def decorator_factory(name):
def decorator(func):
async def wrapper(*args, **kwargs):
return await func(*args, **kwargs)
+
return wrapper
+
return decorator
mock_tracer.start_as_current_span = MagicMock(side_effect=decorator_factory)
@@ -39,12 +41,14 @@ async def wrapper(*args, **kwargs):
@pytest.fixture
def mock_meter():
"""Mock OpenTelemetry meter."""
- with patch('scl.listener.restful_watch.meter') as mock_meter:
+ with patch("scl.listener.restful_watch.meter") as mock_meter:
mock_counters = {}
+
def create_counter_side_effect(name, *args, **kwargs):
if name not in mock_counters:
mock_counters[name] = MagicMock()
return mock_counters[name]
+
mock_meter.create_counter = MagicMock(side_effect=create_counter_side_effect)
yield mock_meter, mock_counters
@@ -55,11 +59,7 @@ def handler(tmp_path, mock_tracer, mock_meter):
_, mock_counters = mock_meter
watch_path = str(tmp_path / "file_watch")
# waiting_approval_dir is automatically created as watch_path/waitingapproval
- handler = RestFulHandler(
- watch_path=watch_path,
- host="127.0.0.1",
- port=8001
- )
+ handler = RestFulHandler(watch_path=watch_path, host="127.0.0.1", port=8001)
# Attach counter mocks to handler for easier access in tests
handler._mock_counters = mock_counters
return handler
@@ -79,6 +79,7 @@ def mock_request():
# Tests for POST /tasks (receive_task)
# -----------------------------------------------------------------------------
+
@pytest.mark.asyncio
async def test_receive_task_success(handler, mock_request, tmp_path):
"""Test successful POST /tasks with valid task data."""
@@ -90,9 +91,9 @@ async def test_receive_task_success(handler, mock_request, tmp_path):
mock_task_instance.hash = "abc123"
mock_task_instance.to_dict.return_value = valid_payload
- with patch('scl.meta.task.Task.from_dict', return_value=mock_task_instance) as mock_from_dict:
+ with patch("scl.meta.task.Task.from_dict", return_value=mock_task_instance) as mock_from_dict:
m_open = mock_open()
- with patch('builtins.open', m_open):
+ with patch("builtins.open", m_open):
response = await handler._receive_task(mock_request)
assert response == {"status": "accepted", "hash": "abc123"}
@@ -100,10 +101,12 @@ async def test_receive_task_success(handler, mock_request, tmp_path):
mock_from_dict.assert_called_once_with(valid_payload)
expected_file_path = os.path.join(handler.watch_path, "abc123.json")
- m_open.assert_called_once_with(expected_file_path, 'w', encoding='utf-8')
+ m_open.assert_called_once_with(expected_file_path, "w", encoding="utf-8")
handler._mock_counters["restful_task_received"].add.assert_called_once_with(1)
- handler._mock_counters["restful_item_valid"].add.assert_called_once_with(1, {"item.type": "Task"})
+ handler._mock_counters["restful_item_valid"].add.assert_called_once_with(
+ 1, {"item.type": "Task"}
+ )
handler._mock_counters["restful_item_invalid"].add.assert_not_called()
@@ -127,7 +130,7 @@ async def test_receive_task_conversion_failure(handler, mock_request):
invalid_task_data = {"bad": "format"}
mock_request.json.return_value = invalid_task_data
- with patch('scl.meta.task.Task.from_dict', side_effect=ValueError("Missing required field")):
+ with patch("scl.meta.task.Task.from_dict", side_effect=ValueError("Missing required field")):
with pytest.raises(HTTPException) as exc_info:
await handler._receive_task(mock_request)
@@ -135,7 +138,9 @@ async def test_receive_task_conversion_failure(handler, mock_request):
assert "Invalid task format" in exc_info.value.detail
handler._mock_counters["restful_task_received"].add.assert_called_once_with(1)
- handler._mock_counters["restful_item_invalid"].add.assert_called_once_with(1, {"item.type": "Task"})
+ handler._mock_counters["restful_item_invalid"].add.assert_called_once_with(
+ 1, {"item.type": "Task"}
+ )
handler._mock_counters["restful_item_valid"].add.assert_not_called()
@@ -148,7 +153,7 @@ async def test_receive_task_missing_hash(handler, mock_request):
mock_task_instance = MagicMock(spec=Task)
del mock_task_instance.hash
- with patch('scl.meta.task.Task.from_dict', return_value=mock_task_instance):
+ with patch("scl.meta.task.Task.from_dict", return_value=mock_task_instance):
with pytest.raises(HTTPException) as exc_info:
await handler._receive_task(mock_request)
@@ -156,13 +161,16 @@ async def test_receive_task_missing_hash(handler, mock_request):
assert "no hash identifier" in exc_info.value.detail
handler._mock_counters["restful_task_received"].add.assert_called_once_with(1)
- handler._mock_counters["restful_item_invalid"].add.assert_called_once_with(1, {"item.type": "Task"})
+ handler._mock_counters["restful_item_invalid"].add.assert_called_once_with(
+ 1, {"item.type": "Task"}
+ )
# -----------------------------------------------------------------------------
# Tests for POST /captasks (receive_captask)
# -----------------------------------------------------------------------------
+
@pytest.mark.asyncio
async def test_receive_captask_success(handler, mock_request):
"""Test successful POST /captasks with valid CapTask data."""
@@ -173,9 +181,11 @@ async def test_receive_captask_success(handler, mock_request):
mock_captask_instance.hash = "cap123"
mock_captask_instance.to_dict.return_value = valid_payload
- with patch('scl.meta.captask.CapTask.from_dict', return_value=mock_captask_instance) as mock_from_dict:
+ with patch(
+ "scl.meta.captask.CapTask.from_dict", return_value=mock_captask_instance
+ ) as mock_from_dict:
m_open = mock_open()
- with patch('builtins.open', m_open):
+ with patch("builtins.open", m_open):
response = await handler._receive_captask(mock_request)
assert response == {"status": "accepted", "hash": "cap123"}
@@ -183,10 +193,12 @@ async def test_receive_captask_success(handler, mock_request):
mock_from_dict.assert_called_once_with(valid_payload)
expected_file_path = os.path.join(handler.watch_path, "cap123.json")
- m_open.assert_called_once_with(expected_file_path, 'w', encoding='utf-8')
+ m_open.assert_called_once_with(expected_file_path, "w", encoding="utf-8")
handler._mock_counters["restful_captask_received"].add.assert_called_once_with(1)
- handler._mock_counters["restful_item_valid"].add.assert_called_once_with(1, {"item.type": "CapTask"})
+ handler._mock_counters["restful_item_valid"].add.assert_called_once_with(
+ 1, {"item.type": "CapTask"}
+ )
@pytest.mark.asyncio
@@ -195,7 +207,7 @@ async def test_receive_captask_conversion_failure(handler, mock_request):
invalid_data = {"wrong": "field"}
mock_request.json.return_value = invalid_data
- with patch('scl.meta.captask.CapTask.from_dict', side_effect=ValueError("Invalid")):
+ with patch("scl.meta.captask.CapTask.from_dict", side_effect=ValueError("Invalid")):
with pytest.raises(HTTPException) as exc_info:
await handler._receive_captask(mock_request)
@@ -203,13 +215,16 @@ async def test_receive_captask_conversion_failure(handler, mock_request):
assert "Invalid captask format" in exc_info.value.detail
handler._mock_counters["restful_captask_received"].add.assert_called_once_with(1)
- handler._mock_counters["restful_item_invalid"].add.assert_called_once_with(1, {"item.type": "CapTask"})
+ handler._mock_counters["restful_item_invalid"].add.assert_called_once_with(
+ 1, {"item.type": "CapTask"}
+ )
# -----------------------------------------------------------------------------
# Tests for GET /items/{item_hash} (check_status)
# -----------------------------------------------------------------------------
+
@pytest.mark.asyncio
async def test_check_status_pending(handler, tmp_path):
"""Test status check when file exists in watch_path."""
@@ -301,6 +316,7 @@ async def test_check_status_not_found(handler):
# Tests for GET /tasks/waiting (list_waiting)
# -----------------------------------------------------------------------------
+
@pytest.mark.asyncio
async def test_list_waiting_empty(handler):
"""Test listing waiting items when directory is empty."""
@@ -315,7 +331,12 @@ async def test_list_waiting_with_items(handler):
waiting_dir = Path(handler.waiting_approval_dir)
waiting_dir.mkdir(parents=True, exist_ok=True)
- task_data = {"system_prompt": "You are a bot", "prompt_list": [], "hash": "task1", "approval": False}
+ task_data = {
+ "system_prompt": "You are a bot",
+ "prompt_list": [],
+ "hash": "task1",
+ "approval": False,
+ }
captask_data = {"cap_name": "email", "args": ["x"], "hash": "cap1", "approval": False}
with open(waiting_dir / "task1.json", "w") as f:
@@ -358,6 +379,7 @@ async def test_list_waiting_handles_invalid_json(handler, caplog):
# Tests for POST /items/{item_hash}/approve (approve_item)
# -----------------------------------------------------------------------------
+
@pytest.mark.asyncio
async def test_approve_item_success(handler):
"""Test successful approval of an item."""
@@ -379,7 +401,7 @@ async def test_approve_item_success(handler):
assert not src_path.exists()
# Check updated approval flag in destination file
- with open(dest_path, "r") as f:
+ with open(dest_path) as f:
moved_data = json.load(f)
assert moved_data["approval"] is True
@@ -397,6 +419,7 @@ async def test_approve_item_not_found(handler):
assert exc_info.value.status_code == 404
assert "not found" in exc_info.value.detail
+
@pytest.mark.asyncio
async def test_approve_item_supports_yaml(handler):
"""Test approval works with YAML file."""
@@ -416,11 +439,11 @@ async def test_approve_item_supports_yaml(handler):
def isfile_side_effect(path):
return path == yaml_file_path
- with patch.dict('sys.modules', {'yaml': mock_yaml_module}):
- with patch('os.path.isfile', side_effect=isfile_side_effect):
+ with patch.dict("sys.modules", {"yaml": mock_yaml_module}):
+ with patch("os.path.isfile", side_effect=isfile_side_effect):
m_open = mock_open(read_data="hash: yaml-approve\napproval: false")
- with patch('builtins.open', m_open):
- with patch('os.remove') as mock_remove:
+ with patch("builtins.open", m_open):
+ with patch("os.remove") as mock_remove:
response = await handler._approve_item(item_hash)
assert response == {"hash": item_hash, "status": "approved"}
@@ -428,19 +451,19 @@ def isfile_side_effect(path):
handler._mock_counters["restful_approve"].add.assert_called_once_with(1)
mock_yaml_module.dump.assert_called_once()
# 确认目标文件被正确写入
- m_open.assert_any_call(dest_file_path, 'w', encoding='utf-8')
+ m_open.assert_any_call(dest_file_path, "w", encoding="utf-8")
+
+
# -----------------------------------------------------------------------------
# Initialization and Utility Tests
# -----------------------------------------------------------------------------
+
def test_handler_initialization(tmp_path):
"""Test RestFulHandler initialization creates required directories."""
watch_path = tmp_path / "custom_watch"
handler = RestFulHandler(
- watch_path=str(watch_path),
- host="0.0.0.0",
- port=8080,
- log_level="debug"
+ watch_path=str(watch_path), host="0.0.0.0", port=8080, log_level="debug"
)
assert handler.watch_path == str(watch_path)
# waiting_approval_dir is a fixed subdirectory
@@ -453,15 +476,12 @@ def test_handler_initialization(tmp_path):
assert os.path.exists(expected_waiting)
-@patch('scl.listener.restful_watch.uvicorn')
+@patch("scl.listener.restful_watch.uvicorn")
def test_start_method(mock_uvicorn, handler):
"""Test start() calls uvicorn.run with correct parameters."""
handler.start()
mock_uvicorn.run.assert_called_once_with(
- handler.app,
- host=handler.host,
- port=handler.port,
- log_level=handler.log_level
+ handler.app, host=handler.host, port=handler.port, log_level=handler.log_level
)
@@ -472,16 +492,16 @@ def test_write_item_file(handler):
mock_item = MagicMock()
mock_item.to_dict.return_value = item_dict
- with patch('builtins.open', mock_open()) as m_open:
+ with patch("builtins.open", mock_open()) as m_open:
file_path = handler._write_item_file(mock_item, item_hash, "Task")
expected_path = os.path.join(handler.watch_path, f"{item_hash}.json")
assert file_path == expected_path
- m_open.assert_called_once_with(expected_path, 'w', encoding='utf-8')
+ m_open.assert_called_once_with(expected_path, "w", encoding="utf-8")
def test_guess_type(handler):
"""Test _guess_type correctly identifies Task vs CapTask."""
assert handler._guess_type({"cap_name": "x", "args": []}, "") == "CapTask"
assert handler._guess_type({"system_prompt": "hi"}, "") == "Task"
- assert handler._guess_type({}, "") == "Unknown"
\ No newline at end of file
+ assert handler._guess_type({}, "") == "Unknown"
diff --git a/scl/test/processor/test_awaitCapTasksProcessor.py b/tests/processor/test_await_cap_tasks_processor.py
similarity index 81%
rename from scl/test/processor/test_awaitCapTasksProcessor.py
rename to tests/processor/test_await_cap_tasks_processor.py
index 0f7fb97..69dcdba 100644
--- a/scl/test/processor/test_awaitCapTasksProcessor.py
+++ b/tests/processor/test_await_cap_tasks_processor.py
@@ -1,31 +1,32 @@
-# scl/test/processor/test_awaitCapTasksProcessor.py
+# scl/test/processor/test_await_cap_tasks_processor.py
"""
Unit tests for the AwaitCapTasksProcessor class.
"""
-import pytest
from contextlib import contextmanager
-from unittest.mock import MagicMock, patch, ANY
+from unittest.mock import ANY, MagicMock, patch
+
+import pytest
from opentelemetry import trace as trace_api
from opentelemetry.trace import StatusCode
-from scl.processor.awaitCapTasksProcessor import AwaitCapTasksProcessor
-from scl.queue.awaitingCapTasksQueue import AwaitingCapTasksQueue
from scl.meta.task import Task
# Import the module itself to patch trace.get_current_span precisely
-from scl.processor import awaitCapTasksProcessor as processor_module
-
+from scl.processor import await_cap_tasks_processor as processor_module
+from scl.processor.await_cap_tasks_processor import AwaitCapTasksProcessor
+from scl.queue.awaiting_cap_tasks_queue import AwaitingCapTasksQueue
# ----------------------------------------------------------------------
# Utilities
# ----------------------------------------------------------------------
+
@contextmanager
def mock_get_current_span(span):
"""Patch trace.get_current_span to return a specific mock span."""
- with patch.object(processor_module.trace, 'get_current_span', return_value=span):
+ with patch.object(processor_module.trace, "get_current_span", return_value=span):
yield
@@ -33,6 +34,7 @@ def mock_get_current_span(span):
# Autouse fixtures to avoid side effects from real code
# ----------------------------------------------------------------------
+
@pytest.fixture(autouse=True)
def patch_os_makedirs():
"""Prevent real directory creation."""
@@ -43,23 +45,23 @@ def patch_os_makedirs():
@pytest.fixture(autouse=True)
def patch_module_logger():
"""Replace the module‑level logger."""
- with patch("scl.processor.awaitCapTasksProcessor.logger") as mock_logger:
+ with patch("scl.processor.await_cap_tasks_processor.logger") as mock_logger:
yield mock_logger
@pytest.fixture(autouse=True)
def patch_module_meter():
"""Replace the meter and provide mocks for the three custom counters."""
- with patch("scl.processor.awaitCapTasksProcessor.meter") as mock_meter:
+ with patch("scl.processor.await_cap_tasks_processor.meter") as mock_meter:
counter_moved = MagicMock()
counter_errors = MagicMock()
counter_requeued = MagicMock()
mock_meter.create_counter.side_effect = [
- counter_moved, # files_moved
- counter_errors, # file_move_errors
- counter_requeued # tasks_requeued
+ counter_moved, # files_moved
+ counter_errors, # file_move_errors
+ counter_requeued, # tasks_requeued
]
- with patch("scl.processor.awaitCapTasksProcessor.tracer") as mock_tracer:
+ with patch("scl.processor.await_cap_tasks_processor.tracer") as mock_tracer:
yield mock_meter, counter_moved, counter_errors, counter_requeued, mock_tracer
@@ -67,6 +69,7 @@ def patch_module_meter():
# Basic test fixtures
# ----------------------------------------------------------------------
+
@pytest.fixture
def mock_queue():
"""Return a mocked AwaitingCapTasksQueue."""
@@ -88,12 +91,14 @@ class ProcessorFactory:
def __init__(self, meter_info):
_, self.counter_moved, self.counter_errors, self.counter_requeued, _ = meter_info
- def create(self, source_queue, waiting_dir="/tmp/waiting", file_watch_dir="/tmp/watch", name=None):
+ def create(
+ self, source_queue, waiting_dir="/tmp/waiting", file_watch_dir="/tmp/watch", name=None
+ ):
proc = AwaitCapTasksProcessor(
source_queue=source_queue,
waiting_captask_dir=waiting_dir,
file_watch_dir=file_watch_dir,
- name=name
+ name=name,
)
proc.files_moved_counter = self.counter_moved
proc.file_move_errors_counter = self.counter_errors
@@ -111,13 +116,16 @@ def factory(patch_module_meter):
# Tests
# ----------------------------------------------------------------------
+
class TestInit:
def test_initializes_directories_and_metrics(self, factory, patch_module_logger):
# Override the autouse patch just in this test to assert calls
with patch("os.makedirs") as mock_makedirs:
proc = factory(
MagicMock(spec=AwaitingCapTasksQueue),
- "/custom/wait", "/custom/watch", name="testproc"
+ "/custom/wait",
+ "/custom/watch",
+ name="testproc",
)
mock_makedirs.assert_any_call("/custom/wait", exist_ok=True)
mock_makedirs.assert_any_call("/custom/watch", exist_ok=True)
@@ -129,8 +137,9 @@ def test_initializes_directories_and_metrics(self, factory, patch_module_logger)
class TestGetItem:
- def test_returns_task_when_available(self, factory, mock_queue, mock_task,
- patch_module_logger, patch_module_meter):
+ def test_returns_task_when_available(
+ self, factory, mock_queue, mock_task, patch_module_logger, patch_module_meter
+ ):
mock_queue.pop.return_value = mock_task
proc = factory(mock_queue)
mock_tracer = patch_module_meter[4]
@@ -148,8 +157,9 @@ def test_returns_task_when_available(self, factory, mock_queue, mock_task,
"%s: consumed Task %s from source queue", proc.name, mock_task.hash
)
- def test_returns_none_when_queue_empty(self, factory, mock_queue,
- patch_module_logger, patch_module_meter):
+ def test_returns_none_when_queue_empty(
+ self, factory, mock_queue, patch_module_logger, patch_module_meter
+ ):
mock_queue.pop.return_value = None
proc = factory(mock_queue)
mock_tracer = patch_module_meter[4]
@@ -163,8 +173,9 @@ def test_returns_none_when_queue_empty(self, factory, mock_queue,
mock_span.set_attribute.assert_any_call("task.available", False)
patch_module_logger.debug.assert_not_called()
- def test_handles_pop_exception(self, factory, mock_queue,
- patch_module_logger, patch_module_meter):
+ def test_handles_pop_exception(
+ self, factory, mock_queue, patch_module_logger, patch_module_meter
+ ):
mock_queue.pop.side_effect = Exception("queue down")
proc = factory(mock_queue)
mock_tracer = patch_module_meter[4]
@@ -176,16 +187,15 @@ def test_handles_pop_exception(self, factory, mock_queue,
assert result is None
patch_module_logger.error.assert_called_with(
- "%s: error consuming task from source queue: %s",
- proc.name,
- mock_queue.pop.side_effect
+ "%s: error consuming task from source queue: %s", proc.name, mock_queue.pop.side_effect
)
mock_span.record_exception.assert_called_once_with(mock_queue.pop.side_effect)
class TestProcessItem:
- def test_all_captasks_completed_triggers_move(self, factory, mock_queue, mock_task,
- patch_module_logger, patch_module_meter):
+ def test_all_captasks_completed_triggers_move(
+ self, factory, mock_queue, mock_task, patch_module_logger, patch_module_meter
+ ):
cap1 = MagicMock(status="Processed")
cap2 = MagicMock(status="Error")
mock_task.cap_tasks = [cap1, cap2]
@@ -194,8 +204,10 @@ def test_all_captasks_completed_triggers_move(self, factory, mock_queue, mock_ta
mock_span = MagicMock()
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
- with mock_get_current_span(mock_span), \
- patch.object(proc, "_move_completed_file") as mock_move:
+ with (
+ mock_get_current_span(mock_span),
+ patch.object(proc, "_move_completed_file") as mock_move,
+ ):
proc._process_item(mock_task)
mock_move.assert_called_once_with(mock_task.hash, mock_span)
@@ -203,8 +215,9 @@ def test_all_captasks_completed_triggers_move(self, factory, mock_queue, mock_ta
mock_queue.push.assert_not_called()
proc.tasks_requeued_counter.add.assert_not_called()
- def test_not_all_captasks_completed_requeues(self, factory, mock_queue, mock_task,
- patch_module_logger, patch_module_meter):
+ def test_not_all_captasks_completed_requeues(
+ self, factory, mock_queue, mock_task, patch_module_logger, patch_module_meter
+ ):
cap1 = MagicMock(status="created")
mock_task.cap_tasks = [cap1]
proc = factory(mock_queue)
@@ -216,17 +229,15 @@ def test_not_all_captasks_completed_requeues(self, factory, mock_queue, mock_tas
proc._process_item(mock_task)
mock_queue.push.assert_called_once_with(mock_task)
- proc.tasks_requeued_counter.add.assert_called_once_with(
- 1, {"processor.name": proc.name}
- )
+ proc.tasks_requeued_counter.add.assert_called_once_with(1, {"processor.name": proc.name})
patch_module_logger.debug.assert_called_with(
- "%s: requeued Task %s (CapTasks not all completed)",
- proc.name, mock_task.hash
+ "%s: requeued Task %s (CapTasks not all completed)", proc.name, mock_task.hash
)
mock_span.set_attribute.assert_any_call("task.requeued", True)
- def test_all_captasks_completed_exception_pushes_back(self, factory, mock_queue, mock_task,
- patch_module_logger, patch_module_meter):
+ def test_all_captasks_completed_exception_pushes_back(
+ self, factory, mock_queue, mock_task, patch_module_logger, patch_module_meter
+ ):
cap1 = MagicMock(status="Processed")
mock_task.cap_tasks = [cap1]
proc = factory(mock_queue)
@@ -244,7 +255,10 @@ def test_all_captasks_completed_exception_pushes_back(self, factory, mock_queue,
mock_queue.push.assert_called_with(mock_task)
patch_module_logger.error.assert_called_with(
"%s: failed to process Task %s: %s",
- proc.name, mock_task.hash, original_error, exc_info=True
+ proc.name,
+ mock_task.hash,
+ original_error,
+ exc_info=True,
)
mock_span.record_exception.assert_called_once()
@@ -256,8 +270,9 @@ def test_all_captasks_completed_exception_pushes_back(self, factory, mock_queue,
assert call_args.status_code == trace_api.StatusCode.ERROR
assert call_args.description == "Task processing failed"
- def test_push_back_after_error_fails(self, factory, mock_queue, mock_task,
- patch_module_logger, patch_module_meter):
+ def test_push_back_after_error_fails(
+ self, factory, mock_queue, mock_task, patch_module_logger, patch_module_meter
+ ):
cap1 = MagicMock(status="Processed")
mock_task.cap_tasks = [cap1]
proc = factory(mock_queue)
@@ -276,7 +291,9 @@ def test_push_back_after_error_fails(self, factory, mock_queue, mock_task,
mock_queue.push.assert_called_with(mock_task)
patch_module_logger.critical.assert_called_with(
"%s: failed to requeue Task %s after error: %s",
- proc.name, mock_task.hash, requeue_error
+ proc.name,
+ mock_task.hash,
+ requeue_error,
)
@@ -317,12 +334,13 @@ def test_successful_move(self, factory, mock_queue, patch_module_logger, patch_m
proc._move_completed_file(task_hash, mock_span)
mock_shutil_move.assert_called_once_with(src, dst)
- proc.files_moved_counter.add.assert_called_once_with(
- 1, {"processor.name": "mover"}
- )
+ proc.files_moved_counter.add.assert_called_once_with(1, {"processor.name": "mover"})
patch_module_logger.info.assert_called_with(
"%s: moved completed Task file %s from %s to %s",
- "mover", "abc123.json", "/custom/wait", "/custom/watch"
+ "mover",
+ "abc123.json",
+ "/custom/wait",
+ "/custom/watch",
)
mock_span.set_attribute.assert_any_call("file.moved", True)
@@ -342,9 +360,7 @@ def test_file_not_found(self, factory, mock_queue, patch_module_logger, patch_mo
)
# The original code uses: logger.error("%s: %s", self.name, error_msg)
expected_error_msg = f"Expected file {src} not found for completed Task {task_hash}"
- patch_module_logger.error.assert_called_with(
- "%s: %s", "mover", expected_error_msg
- )
+ patch_module_logger.error.assert_called_with("%s: %s", "mover", expected_error_msg)
# Check set_status call
mock_span.set_status.assert_called_once()
call_args = mock_span.set_status.call_args[0][0]
@@ -369,5 +385,8 @@ def test_move_failure(self, factory, mock_queue, patch_module_logger, patch_modu
mock_span.record_exception.assert_called_once_with(move_error)
patch_module_logger.error.assert_called_with(
"%s: failed to move file %s to %s: %s",
- "mover", src, "/custom/watch/failhash.json", move_error
- )
\ No newline at end of file
+ "mover",
+ src,
+ "/custom/watch/failhash.json",
+ move_error,
+ )
diff --git a/scl/test/processor/test_awaitingApproveProcessor.py b/tests/processor/test_awaiting_approve_processor.py
similarity index 80%
rename from scl/test/processor/test_awaitingApproveProcessor.py
rename to tests/processor/test_awaiting_approve_processor.py
index 5db13c5..eb21050 100644
--- a/scl/test/processor/test_awaitingApproveProcessor.py
+++ b/tests/processor/test_awaiting_approve_processor.py
@@ -9,25 +9,28 @@
- _move_approved_file: success, missing file, move error.
"""
-import pytest
-from unittest.mock import patch, MagicMock, call
+import logging
import os
import tempfile
-import logging
+from unittest.mock import MagicMock, call, patch
+
+import pytest
-from scl.processor.awaitingApproveProcessor import AwaitingApproveProcessor
-from scl.queue.awaitingApproveQueue import AwaitingApproveQueue
-from scl.meta.task import Task
from scl.meta.captask import CapTask
+from scl.meta.task import Task
+from scl.processor.awaiting_approve_processor import AwaitingApproveProcessor
+from scl.queue.awaiting_approve_queue import AwaitingApproveQueue
# ------------------------------------------------------------------ Fixtures
@pytest.fixture
def otel_mock():
"""Mock OpenTelemetry tracer, meter, and trace.get_current_span."""
- with patch('scl.processor.awaitingApproveProcessor.tracer') as mock_tracer, \
- patch('scl.processor.awaitingApproveProcessor.meter') as mock_meter, \
- patch('scl.processor.awaitingApproveProcessor.trace') as mock_trace:
+ with (
+ patch("scl.processor.awaiting_approve_processor.tracer") as mock_tracer,
+ patch("scl.processor.awaiting_approve_processor.meter") as mock_meter,
+ patch("scl.processor.awaiting_approve_processor.trace") as mock_trace,
+ ):
mock_span = MagicMock()
mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
mock_tracer.start_as_current_span.return_value.__exit__.return_value = None
@@ -38,6 +41,7 @@ def create_counter_side_effect(name, description=None):
cnt = MagicMock()
cnt.name = name # helps in debugging
return cnt
+
mock_meter.create_counter.side_effect = create_counter_side_effect
yield mock_tracer, mock_meter, mock_trace, mock_span
@@ -46,11 +50,14 @@ def create_counter_side_effect(name, description=None):
@pytest.fixture
def mock_base_init():
"""Prevent BaseQueueProcessor.__init__ from executing any real logic."""
- with patch('scl.processor.awaitingApproveProcessor.BaseQueueProcessor.__init__',
- autospec=True) as mock_init:
+ with patch(
+ "scl.processor.awaiting_approve_processor.BaseQueueProcessor.__init__", autospec=True
+ ) as mock_init:
+
def init_side_effect(self, name=None, logger_name=None):
- self.name = name or 'mock'
+ self.name = name or "mock"
self.logger = MagicMock()
+
mock_init.side_effect = init_side_effect
yield mock_init
@@ -67,10 +74,10 @@ def processor_fixture(otel_mock, mock_base_init, tmp_path):
source_queue=mock_queue,
waiting_approval_dir=str(waiting_dir),
file_watch_dir=str(file_watch_dir),
- name="test_processor"
+ name="test_processor",
)
# Logger is set by the mocked base init, but ensure it's present.
- if not hasattr(proc, 'logger'):
+ if not hasattr(proc, "logger"):
proc.logger = MagicMock()
return proc, mock_queue, mock_tracer, mock_span, mock_meter, waiting_dir, file_watch_dir
@@ -88,8 +95,8 @@ def test_init_creates_directories(self, processor_fixture):
def test_init_calls_base_queue_processor(self, mock_base_init, processor_fixture):
mock_base_init.assert_called_once()
call_kwargs = mock_base_init.call_args[1]
- assert call_kwargs.get('name') == 'test_processor'
- assert 'logger_name' in call_kwargs
+ assert call_kwargs.get("name") == "test_processor"
+ assert "logger_name" in call_kwargs
def test_init_creates_counters(self, otel_mock, processor_fixture):
mock_meter = otel_mock[1] # the mocked meter
@@ -97,10 +104,10 @@ def test_init_creates_counters(self, otel_mock, processor_fixture):
# Four counters should be created during __init__
assert mock_meter.create_counter.call_count == 4
counter_names = [c.args[0] for c in mock_meter.create_counter.call_args_list]
- assert 'test_processor.items_fetched_by_type' in counter_names
- assert 'test_processor.items_requeued' in counter_names
- assert 'test_processor.files_moved' in counter_names
- assert 'test_processor.file_move_errors' in counter_names
+ assert "test_processor.items_fetched_by_type" in counter_names
+ assert "test_processor.items_requeued" in counter_names
+ assert "test_processor.files_moved" in counter_names
+ assert "test_processor.file_move_errors" in counter_names
# ------ _get_item tests ------
def test_get_item_when_queue_empty(self, processor_fixture):
@@ -115,14 +122,14 @@ def test_get_item_returns_task(self, processor_fixture):
proc, mock_queue, _, mock_span, _, _, _ = processor_fixture
# Use a plain MagicMock without spec to avoid type='Task' expectation
mock_item = MagicMock()
- mock_item.hash = 'abc123'
+ mock_item.hash = "abc123"
mock_queue.get.return_value = mock_item
item = proc._get_item()
assert item is mock_item
mock_span.set_attribute.assert_any_call("item.available", True)
expected_type = type(mock_item).__name__
mock_span.set_attribute.assert_any_call("item.type", expected_type)
- mock_span.set_attribute.assert_any_call("item.hash", 'abc123')
+ mock_span.set_attribute.assert_any_call("item.hash", "abc123")
proc.items_fetched_by_type_counter.add.assert_called_once_with(
1, {"processor.name": "test_processor", "item_type": expected_type}
)
@@ -146,7 +153,7 @@ def test_process_item_missing_hash_logs_error(self, processor_fixture):
def test_process_item_unapproved_requeues(self, processor_fixture):
proc, mock_queue, _, mock_span, _, _, _ = processor_fixture
- item = MagicMock(hash='u1', approval=False)
+ item = MagicMock(hash="u1", approval=False)
proc._process_item(item)
mock_queue.add.assert_called_once_with(item)
proc.items_requeued_counter.add.assert_called_once_with(
@@ -156,17 +163,17 @@ def test_process_item_unapproved_requeues(self, processor_fixture):
def test_process_item_approved_calls_move_file(self, processor_fixture):
proc, mock_queue, _, mock_span, _, _, _ = processor_fixture
- item = MagicMock(hash='a1', approval=True)
- with patch.object(proc, '_move_approved_file') as mock_move:
+ item = MagicMock(hash="a1", approval=True)
+ with patch.object(proc, "_move_approved_file") as mock_move:
proc._process_item(item)
- mock_move.assert_called_once_with('a1', type(item).__name__, mock_span)
+ mock_move.assert_called_once_with("a1", type(item).__name__, mock_span)
mock_queue.add.assert_not_called()
mock_span.set_attribute.assert_any_call("item.routed_to", "file_watch_dir")
def test_process_item_exception_requeues_and_logs(self, processor_fixture):
proc, mock_queue, _, mock_span, _, _, _ = processor_fixture
- item = MagicMock(hash='e1', approval=True)
- with patch.object(proc, '_move_approved_file', side_effect=RuntimeError("move fail")):
+ item = MagicMock(hash="e1", approval=True)
+ with patch.object(proc, "_move_approved_file", side_effect=RuntimeError("move fail")):
proc._process_item(item)
mock_queue.add.assert_called_with(item)
proc.logger.error.assert_called()
@@ -175,8 +182,8 @@ def test_process_item_exception_requeues_and_logs(self, processor_fixture):
def test_process_item_requeue_failure_logs_critical(self, processor_fixture):
proc, mock_queue, _, mock_span, _, _, _ = processor_fixture
- item = MagicMock(hash='e2', approval=True)
- with patch.object(proc, '_move_approved_file', side_effect=RuntimeError("move fail")):
+ item = MagicMock(hash="e2", approval=True)
+ with patch.object(proc, "_move_approved_file", side_effect=RuntimeError("move fail")):
mock_queue.add.side_effect = Exception("add fail")
proc._process_item(item)
proc.logger.critical.assert_called()
@@ -188,7 +195,7 @@ def test_move_approved_file_success(self, processor_fixture):
src.write_text("payload")
dst = file_watch_dir / "hash1.json"
- with patch('scl.processor.awaitingApproveProcessor.shutil.move') as mock_move:
+ with patch("scl.processor.awaiting_approve_processor.shutil.move") as mock_move:
proc._move_approved_file("hash1", "Task", mock_span)
mock_move.assert_called_once_with(str(src), str(dst))
@@ -200,7 +207,7 @@ def test_move_approved_file_success(self, processor_fixture):
def test_move_approved_file_not_found(self, processor_fixture):
proc, _, _, mock_span, _, waiting_dir, _ = processor_fixture
# Do not create the file
- with patch('scl.processor.awaitingApproveProcessor.shutil.move') as mock_move:
+ with patch("scl.processor.awaiting_approve_processor.shutil.move") as mock_move:
proc._move_approved_file("nohash", "CapTask", mock_span)
proc.logger.error.assert_called()
@@ -215,12 +222,13 @@ def test_move_approved_file_move_exception(self, processor_fixture):
src = waiting_dir / "errhash.json"
src.write_text("data")
- with patch('scl.processor.awaitingApproveProcessor.shutil.move',
- side_effect=OSError("disk full")):
+ with patch(
+ "scl.processor.awaiting_approve_processor.shutil.move", side_effect=OSError("disk full")
+ ):
with pytest.raises(OSError):
proc._move_approved_file("errhash", "Task", mock_span)
proc.file_move_errors_counter.add.assert_called_once_with(
1, {"processor.name": "test_processor", "error": "move_failed"}
)
- mock_span.record_exception.assert_called()
\ No newline at end of file
+ mock_span.record_exception.assert_called()
diff --git a/scl/test/processor/test_base_queue_processor.py b/tests/processor/test_base_queue_processor.py
similarity index 93%
rename from scl/test/processor/test_base_queue_processor.py
rename to tests/processor/test_base_queue_processor.py
index 08ed62f..14252ad 100644
--- a/scl/test/processor/test_base_queue_processor.py
+++ b/tests/processor/test_base_queue_processor.py
@@ -2,19 +2,20 @@
Tests for base_queue_processor.py
"""
+import threading
import time
import unittest
from unittest.mock import (
- Mock,
MagicMock,
+ Mock,
PropertyMock,
call,
patch,
)
-import threading
from scl.processor.base_queue_processor import BaseQueueProcessor
+
# ---------------------------------------------------------------------------
# Concrete minimal implementations for testing
# ---------------------------------------------------------------------------
@@ -41,7 +42,7 @@ class StoppingProcessor(TestProcessor):
def _process_item(self, item):
super()._process_item(item)
- if not self.items: # no more items left
+ if not self.items: # no more items left
self._running = False
@@ -54,12 +55,8 @@ def setUp(self):
patcher_tracer = patch(
"scl.processor.base_queue_processor.tracer", MagicMock(name="tracer")
)
- patcher_meter = patch(
- "scl.processor.base_queue_processor.meter", MagicMock(name="meter")
- )
- patcher_trace = patch(
- "scl.processor.base_queue_processor.trace", MagicMock(name="trace")
- )
+ patcher_meter = patch("scl.processor.base_queue_processor.meter", MagicMock(name="meter"))
+ patcher_trace = patch("scl.processor.base_queue_processor.trace", MagicMock(name="trace"))
self.addCleanup(patcher_tracer.stop)
self.addCleanup(patcher_meter.stop)
self.addCleanup(patcher_trace.stop)
@@ -69,9 +66,7 @@ def setUp(self):
# Mock the span and context manager interface
self.mock_span = MagicMock(name="span")
- self.mock_tracer.start_as_current_span.return_value.__enter__.return_value = (
- self.mock_span
- )
+ self.mock_tracer.start_as_current_span.return_value.__enter__.return_value = self.mock_span
self.mock_tracer.start_as_current_span.return_value.__exit__ = Mock()
# mock_meter.create_counter returns a mock counter
@@ -103,8 +98,7 @@ def test_init_defaults(self):
self.assertEqual(proc.logger.name, "test")
# Metrics creation calls
self.mock_meter.create_counter.assert_called_with(
- "test.items_consumed",
- description="Total items consumed from the queue"
+ "test.items_consumed", description="Total items consumed from the queue"
)
self.mock_meter.create_observable_gauge.assert_called()
self.assertEqual(proc._idle_value, 0)
@@ -135,7 +129,7 @@ def test_start(self):
proc.start()
self.assertTrue(proc._running)
self.assertIsNotNone(proc._thread)
- proc.stop() # clean shutdown
+ proc.stop() # clean shutdown
def test_start_already_running(self):
proc = TestProcessor(name="start_test", logger_name="start_test")
@@ -163,7 +157,7 @@ def test_consume_loop_processes_items(self):
items = ["a", "b", "c"]
proc = StoppingProcessor(name="test", logger_name="test", items=items)
proc._running = True
- proc._wakeup_event = Mock() # prevent actual blocking
+ proc._wakeup_event = Mock() # prevent actual blocking
proc._consume_loop()
# All items processed
@@ -180,9 +174,11 @@ def test_consume_loop_empty_queue_backoff(self):
# Stop after one iteration to observe backoff
original_wait = proc._wakeup_event.wait
+
def stop_after_first_wait(timeout):
proc._running = False
return original_wait(timeout)
+
proc._wakeup_event.wait = stop_after_first_wait
proc._consume_loop()
@@ -218,6 +214,7 @@ def test_consume_loop_backoff_capped_at_max(self):
def stop_loop(timeout):
proc._running = False
+
proc._wakeup_event.wait = stop_loop
proc._consume_loop()
@@ -253,6 +250,4 @@ def test_idle_gauge_callback(self):
proc._idle_value = 1
mock_observer = Mock()
proc._idle_gauge_callback(mock_observer)
- mock_observer.observe.assert_called_once_with(
- 1, {"processor.name": "gauge"}
- )
\ No newline at end of file
+ mock_observer.observe.assert_called_once_with(1, {"processor.name": "gauge"})
diff --git a/scl/test/processor/test_capTask_processor.py b/tests/processor/test_cap_task_processor.py
similarity index 86%
rename from scl/test/processor/test_capTask_processor.py
rename to tests/processor/test_cap_task_processor.py
index 6d23573..9eba797 100644
--- a/scl/test/processor/test_capTask_processor.py
+++ b/tests/processor/test_cap_task_processor.py
@@ -1,9 +1,9 @@
-# File: scl/test/processor/test_capTask_processor.py
+# File: scl/test/processor/test_cap_task_processor.py
import os
import shutil
from pathlib import Path
-from unittest.mock import MagicMock, patch, call
+from unittest.mock import MagicMock, call, patch
import pytest
@@ -32,7 +32,8 @@ def processor_module(todo_watch_dir):
def _decorator_pass_through(span_name):
def decorator(func):
- return func # no wrapping – ABC will see a real method
+ return func # no wrapping – ABC will see a real method
+
return decorator
mock_tracer.start_as_current_span = _decorator_pass_through
@@ -45,11 +46,14 @@ def decorator(func):
mock_trace.get_current_span.return_value = mock_span
# Apply all patches and then import the module under test
- with patch('scl.config.config.todo_watch_dir', new=todo_watch_dir), \
- patch('scl.otel.otel.tracer', mock_tracer), \
- patch('scl.otel.otel.meter', mock_meter), \
- patch('opentelemetry.trace', mock_trace):
- from scl.processor import capTask_processor as mod
+ with (
+ patch("scl.config.config.todo_watch_dir", new=todo_watch_dir),
+ patch("scl.otel.otel.tracer", mock_tracer),
+ patch("scl.otel.otel.meter", mock_meter),
+ patch("opentelemetry.trace", mock_trace),
+ ):
+ from scl.processor import cap_task_processor as mod
+
yield mod
@@ -69,9 +73,7 @@ def mock_cap_registry():
def processor(processor_module, mock_queue, mock_cap_registry):
"""Instantiate a real CapabilityProcessor (the class is now concrete)."""
proc = processor_module.CapabilityProcessor(
- name="test_cap",
- queue=mock_queue,
- cap_registry=mock_cap_registry
+ name="test_cap", queue=mock_queue, cap_registry=mock_cap_registry
)
return proc
@@ -81,12 +83,16 @@ def reset_counter_mocks(processor_module):
"""
Reset counter mocks and the span mock before each test to avoid call count leaks.
"""
- for counter_name in ["tasks_processed_counter", "tasks_succeeded_counter", "tasks_failed_counter"]:
+ for counter_name in [
+ "tasks_processed_counter",
+ "tasks_succeeded_counter",
+ "tasks_failed_counter",
+ ]:
counter = getattr(processor_module, counter_name, None)
if counter:
counter.reset_mock()
# Also reset the add method, which is a separate child mock
- if hasattr(counter, 'add'):
+ if hasattr(counter, "add"):
counter.add.reset_mock()
processor_module.trace.get_current_span.return_value.reset_mock()
@@ -95,11 +101,12 @@ def reset_counter_mocks(processor_module):
# Tests
# ======================================================================
+
class TestCapabilityProcessorInit:
def test_attributes(self, processor, mock_queue):
assert processor.name == "test_cap"
assert processor.queue is mock_queue
- assert processor.cap_registry is processor.cap_registry # same object
+ assert processor.cap_registry is processor.cap_registry # same object
def test_registers_notifier(self, processor, mock_queue):
"""Verify that a notify callback is registered and that it calls notify()."""
@@ -109,7 +116,7 @@ def test_registers_notifier(self, processor, mock_queue):
callback = args[1]
# Patch notify on the instance so we can assert it was called
- with patch.object(processor, 'notify'):
+ with patch.object(processor, "notify"):
callback("test_cap", MagicMock())
processor.notify.assert_called_once()
@@ -124,11 +131,13 @@ def test_returns_task(self, processor, mock_queue, processor_module):
mock_queue.consume.assert_called_once_with("test_cap")
span = processor_module.trace.get_current_span.return_value
- span.set_attribute.assert_has_calls([
- call("processor.name", "test_cap"),
- call("task.available", True),
- call("task.hash", "abc"),
- ])
+ span.set_attribute.assert_has_calls(
+ [
+ call("processor.name", "test_cap"),
+ call("task.available", True),
+ call("task.hash", "abc"),
+ ]
+ )
def test_no_task(self, processor, mock_queue, processor_module):
mock_queue.consume.return_value = None
@@ -178,7 +187,9 @@ def test_success(self, processor, processor_module, mock_cap_registry, todo_watc
# The real code sets "task.result_length", not "task.result"
span.set_attribute.assert_any_call("task.result_length", len("result-ok"))
- def test_failure_execute_raises(self, processor, processor_module, mock_cap_registry, todo_watch_dir):
+ def test_failure_execute_raises(
+ self, processor, processor_module, mock_cap_registry, todo_watch_dir
+ ):
cap = MagicMock()
cap.execute.side_effect = ValueError("bad input")
mock_cap_registry.get_capability.return_value = cap
@@ -205,7 +216,9 @@ def test_failure_execute_raises(self, processor, processor_module, mock_cap_regi
span.record_exception.assert_called()
span.set_status.assert_called()
- def test_missing_capability(self, processor, processor_module, mock_cap_registry, todo_watch_dir, caplog):
+ def test_missing_capability(
+ self, processor, processor_module, mock_cap_registry, todo_watch_dir, caplog
+ ):
mock_cap_registry.get_capability.return_value = None
task = MagicMock(hash="hash3", cap_name="unknown", args={})
@@ -250,7 +263,8 @@ def test_move_error(self, processor, todo_watch_dir, caplog, monkeypatch):
def mock_move(*args, **kwargs):
raise OSError("permission denied")
+
monkeypatch.setattr(shutil, "move", mock_move)
processor._safe_move(src, dst, "error")
- assert "Failed to move" in caplog.text
\ No newline at end of file
+ assert "Failed to move" in caplog.text
diff --git a/scl/test/processor/test_task_processor.py b/tests/processor/test_task_processor.py
similarity index 92%
rename from scl/test/processor/test_task_processor.py
rename to tests/processor/test_task_processor.py
index 520b14d..7b0f208 100644
--- a/scl/test/processor/test_task_processor.py
+++ b/tests/processor/test_task_processor.py
@@ -7,6 +7,7 @@
- Task processing with tracing, logging and error metrics
- Notification tracing override
"""
+
import logging
import queue
from unittest.mock import ANY, MagicMock, PropertyMock, call, patch
@@ -74,8 +75,7 @@ def test_init_creates_error_counter(mock_input_queue, mock_tracer, mock_meter):
"""Should create a counter metric for processing errors."""
processor = TaskProcessor(input_queue=mock_input_queue, name="myproc")
mock_meter.create_counter.assert_called_once_with(
- "myproc.processing_errors",
- description="Number of errors while processing individual tasks"
+ "myproc.processing_errors", description="Number of errors while processing individual tasks"
)
@@ -116,10 +116,13 @@ def test_process_item_sets_span_attributes(processor, mock_task, mock_tracer):
with patch("scl.processor.task_processor.trace.get_current_span", return_value=mock_span):
processor._process_item(mock_task)
- mock_span.set_attribute.assert_has_calls([
- call("task.id", "task-123"),
- call("task.type", "test_type"),
- ], any_order=True)
+ mock_span.set_attribute.assert_has_calls(
+ [
+ call("task.id", "task-123"),
+ call("task.type", "test_type"),
+ ],
+ any_order=True,
+ )
def test_process_item_logs_info_and_debug_on_success(processor, mock_task, caplog):
@@ -175,9 +178,11 @@ def test_notify_opens_span_and_delegates(processor, mock_tracer):
# Verify tracer created a span for notify
mock_tracer.start_as_current_span.assert_called_with("TaskProcessor.notify")
span_instance = mock_tracer.start_as_current_span.return_value.__enter__.return_value
- span_instance.set_attribute.assert_has_calls([
- call("processor.status_before", "idle"),
- call("processor.status_after", "idle"),
- ])
+ span_instance.set_attribute.assert_has_calls(
+ [
+ call("processor.status_before", "idle"),
+ call("processor.status_after", "idle"),
+ ]
+ )
# super().notify() must be invoked
- mock_super_notify.assert_called_once()
\ No newline at end of file
+ mock_super_notify.assert_called_once()
diff --git a/scl/test/prompt.md b/tests/prompt.md
similarity index 100%
rename from scl/test/prompt.md
rename to tests/prompt.md
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..7fbe6ce
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,2668 @@
+version = 1
+revision = 2
+requires-python = ">=3.11"
+resolution-markers = [
+ "python_full_version >= '3.15'",
+ "python_full_version == '3.14.*'",
+ "python_full_version == '3.13.*'",
+ "python_full_version < '3.13'",
+]
+
+[[package]]
+name = "aiomysql"
+version = "0.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pymysql" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
+]
+
+[[package]]
+name = "ast-serialize"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" },
+ { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" },
+ { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" },
+ { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" },
+ { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" },
+ { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" },
+ { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" },
+ { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" },
+ { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" },
+ { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" },
+ { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" },
+ { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" },
+ { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" },
+ { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.5.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
+ { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
+ { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
+ { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
+ { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
+ { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
+ { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
+ { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
+ { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
+ { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
+ { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
+ { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
+ { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
+ { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
+ { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
+ { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
+ { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
+ { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
+ { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
+]
+
+[[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, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.14.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" },
+ { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" },
+ { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" },
+ { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" },
+ { url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" },
+ { url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" },
+ { url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" },
+ { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" },
+ { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" },
+ { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" },
+ { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" },
+ { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" },
+ { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" },
+ { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" },
+ { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" },
+ { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" },
+ { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" },
+ { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" },
+ { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" },
+ { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" },
+ { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" },
+ { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" },
+ { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" },
+ { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" },
+ { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cuda-bindings"
+version = "13.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-pathfinder" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
+ { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" },
+ { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" },
+ { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" },
+ { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" },
+ { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" },
+]
+
+[[package]]
+name = "cuda-pathfinder"
+version = "1.5.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" },
+]
+
+[[package]]
+name = "cuda-toolkit"
+version = "13.0.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" },
+]
+
+[package.optional-dependencies]
+cudart = [
+ { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+cufft = [
+ { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+cufile = [
+ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
+]
+cupti = [
+ { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+curand = [
+ { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+cusolver = [
+ { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+cusparse = [
+ { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+nvjitlink = [
+ { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+nvrtc = [
+ { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+nvtx = [
+ { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.136.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.29.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d5/8d/1c51c094345df128ca4a990d633fe1a0ff28726c9e6b3c41ba65087bba1d/fsspec-2026.4.0.tar.gz", hash = "sha256:301d8ac70ae90ef3ad05dcf94d6c3754a097f9b5fe4667d2787aa359ec7df7e4", size = 312760, upload-time = "2026-04-29T20:42:38.635Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl", hash = "sha256:11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2", size = 203402, upload-time = "2026-04-29T20:42:36.842Z" },
+]
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.75.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" },
+]
+
+[[package]]
+name = "greenlet"
+version = "3.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6d/6e/802acd792aebb2256fbbee8cacf2727faaeb6f240ac11008f09eae4414bc/greenlet-3.5.1.tar.gz", hash = "sha256:5a56aeb7d5d9cc4b3a735efb5095bd4b4f6f0e4f93e5ca876d0e2315137b7829", size = 197356, upload-time = "2026-05-20T15:05:03.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/3c/ff890b466eaba2b0f5e6bdfff025f8c75f41b8ffdc3dbc3d24ad261e764a/greenlet-3.5.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:73f78f9b9f0a5c06e5c946ba1e8e36f5114923b6be109ee618c54f079c3ea14f", size = 284764, upload-time = "2026-05-20T13:09:10.204Z" },
+ { url = "https://files.pythonhosted.org/packages/81/0e/5e5457be3d256918f6a4756f073548a3f0190836e2cc94aa6d0d617a940b/greenlet-3.5.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0cbed8bb44e23c5b199f888f4e4ce096b45ad9f25ff74a7ad0213875e936bb2", size = 603479, upload-time = "2026-05-20T14:00:04.757Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e1/f89a21d58d308298e6f275f13a1b472ed96c680b601a371b08be6a725989/greenlet-3.5.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a203a8bd0acb0701653d3bbb26e404854a68674139ed5cbb778830f42b09bb33", size = 615495, upload-time = "2026-05-20T14:05:40.87Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/f2/8fd452fd81adb9ec79c8275c1375702ab0fd6bee4952da12eaa09b9508d8/greenlet-3.5.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ebeb75c81211f5c702576cf81f315e77e23cfdb2c7c6fcb9dd143e6de35c360", size = 623515, upload-time = "2026-05-20T14:09:07.853Z" },
+ { url = "https://files.pythonhosted.org/packages/75/de/af6cef182862d2ccd6975440d21c9058a77c3f9b469abf94e322dfd2e0e3/greenlet-3.5.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a271fcd66c74615cda6a964fda3f304267a12e50a084472218a39bb0376f563", size = 614754, upload-time = "2026-05-20T13:14:24.947Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bc/c318aa9f3ffc77320fddcee3d892be957b42e2ff947198d9450b004f3a38/greenlet-3.5.1-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:017a544f0385d441e88714160d089d6900ef46c9eff9d99b6715a5ef2d127747", size = 418439, upload-time = "2026-05-20T14:01:38.446Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/c6/50e520283a9f19388a7326b05f9e8637e566003475eacaadad04f558c68d/greenlet-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ded7b068c7c31c1a8657d4fd42d886b3e051ae29f88b80c5ff9d502257b0f071", size = 1574097, upload-time = "2026-05-20T14:02:24.003Z" },
+ { url = "https://files.pythonhosted.org/packages/21/1c/13abd1f4860d987fa5e1170a01930d6e6cd40d328de487a3c9fdaff0ffd0/greenlet-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0932b81d72f552ded9d810d00021b64d89f2195a91ce115b893f943b7a4ab3c", size = 1641058, upload-time = "2026-05-20T13:14:31.83Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/56/5f332b7705545eac2dc01b4e9254d24a793f2656d55d5cc6b94ee59d22ae/greenlet-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:88e300d136eac057b2397aa1cfd7328b4c87c7eb66a09c7bc6a1292234db474e", size = 238089, upload-time = "2026-05-20T13:14:03.229Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/a9/a3c2fa886c5b94863fb0e61b3bc14610b7aa94cf4f17f8741b11708305fc/greenlet-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:cc6ab7e555c8a112ad3a76e368e86e12a2754bcae1652a5602e133ec7b635523", size = 234989, upload-time = "2026-05-20T13:08:27.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/37/4549f149c9797c21b32c2683c33522af22522099de128b2406672526d005/greenlet-3.5.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:fa4f98af3a528f0c3fd592a26df7f376f93329c8f4d987f6bb979057af8bf5e2", size = 286220, upload-time = "2026-05-20T13:07:28.463Z" },
+ { url = "https://files.pythonhosted.org/packages/38/ff/a4f436709716965eaab9f36ea7b906c8a927fbe32fb1372a2071d964f6b1/greenlet-3.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffea73584b216150eab159b6d12348fb253e68757974de1e2c40d8a318ac89ed", size = 601585, upload-time = "2026-05-20T14:00:06.141Z" },
+ { url = "https://files.pythonhosted.org/packages/65/ad/54bc3fcee3ad368a61b19b67d88117f7a8c29727bf71fffdeda81fbd946e/greenlet-3.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1072b4f9edcc1e192d9283a66a3e68d6b84c561de33a83d7858beb9ba1effe10", size = 614215, upload-time = "2026-05-20T14:05:42.675Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/6c/de5b1b388cd2d9fbdfeab324863daba37d54e6e233ddbefd70b385a8c591/greenlet-3.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89101bfd5011e069be974903cb3a4e4523845e4ece2d62dcd8d358933c0ef249", size = 620094, upload-time = "2026-05-20T14:09:09.18Z" },
+ { url = "https://files.pythonhosted.org/packages/40/69/b91cda0647df839483201545913514c2827ebea5e5ccdf931842763bc127/greenlet-3.5.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:add5217d68b31130f0beca584d7fef4878327d2e31642b66618a14eef312b63b", size = 611358, upload-time = "2026-05-20T13:14:26.37Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/43/1204baffab8a6476464795a7ccf394a3248d4f22c9f87173a15b36b6d971/greenlet-3.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:e6cd99ea59dd5d89f0c956606571d79bfe6f68c9eb7f4a4083a41a7f1587edee", size = 422782, upload-time = "2026-05-20T14:01:39.597Z" },
+ { url = "https://files.pythonhosted.org/packages/59/90/3cf77e080350cd02fa307bb2abf05df48f4482c240275bbd2c203ba8bb1c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a5ea42a752d47a145eae922b605cd1634665ac3d5ec1e72402d5048e8d60d207", size = 1570475, upload-time = "2026-05-20T14:02:25.29Z" },
+ { url = "https://files.pythonhosted.org/packages/65/2c/18cece62045e74598c3c393f70dce4a63f56222015ba29a5d4eeb04f764c/greenlet-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5551170cf4f5ff5623e9af81323751979fee2c731e2287b61f73cd27257b823", size = 1635625, upload-time = "2026-05-20T13:14:34.027Z" },
+ { url = "https://files.pythonhosted.org/packages/30/f5/310d104ddf41eb5a70f4c268d22508dfb0c3c8e86fec152be34d0d2ed819/greenlet-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c8bb982ad117d29478ef8f5533e97df21f1e2befd17a299257b0c96d1371c0b", size = 238791, upload-time = "2026-05-20T13:10:39.018Z" },
+ { url = "https://files.pythonhosted.org/packages/62/90/ceca11f504cd23a8047a3dea31919adc48df9b626dd0c13f0d858734fdfd/greenlet-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:80eb4b04dadc4e67df3fae179a32c4706a3f495bc7f22fc8a81115d5f5512188", size = 235580, upload-time = "2026-05-20T13:08:45.056Z" },
+ { url = "https://files.pythonhosted.org/packages/27/69/7f7e5372d998b81001899b1c0823c957aa413ba0f2662e65821611cc31e4/greenlet-3.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:51518ff74664078fc51bffcc6fc529b0df5ae58da192691cee765d45ce944a2b", size = 285060, upload-time = "2026-05-20T13:08:51.899Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/bf/387f9b6b865fd2ae0d0be09e0004827295a01b71be76ed350dd1e28a91a4/greenlet-3.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ffdb3c0bb002c99cd8f298957e046c3dbf6006b5b7cdf11a4e19194624a0a0a", size = 604370, upload-time = "2026-05-20T14:00:07.492Z" },
+ { url = "https://files.pythonhosted.org/packages/32/f5/169ce3d4e4c67291bd18f8cbe0299c9f3e45102c7f1fb3c14780c93e4532/greenlet-3.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7715a5a2c3378ba602c3a440558261e13a820bb53a82693aacd7b7f6d964e283", size = 616987, upload-time = "2026-05-20T14:05:44.237Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ba/c24110c55dffa55aa6e1d98b45310da33801aeba7686ff0190fe5d46fd32/greenlet-3.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d40a890035c0058cadbdc4af7569800fd28a0e527a0fdbb7b5f9418f176846ce", size = 622911, upload-time = "2026-05-20T14:09:10.598Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/e5/7f2e41d5273be07e77560d61ea4e56485b4d6c316d2a84518c62d1364061/greenlet-3.5.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc71ff466927a201b08305acac451ebe1aedfcea002f62f1f2f2ac2ac1e6a135", size = 613911, upload-time = "2026-05-20T13:14:27.539Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/7b/d20db2e8a5ad6c038702f3179b136f93f0a3d1a21a0c0777f3e470cdf4b2/greenlet-3.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:67821bb03e4e98664490edb787ff6af501194c29bbee0f5c1dfdcf1dc3d9d436", size = 425228, upload-time = "2026-05-20T14:01:40.837Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a4/fbdc67579b73615a1f91615e814303cc71e06128f7baaba87be79b8fb90c/greenlet-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cd443683db272ebaaca03af98c0b063ab30db70ea8a31a1559f35e3f7b744ccd", size = 1570689, upload-time = "2026-05-20T14:02:27.225Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b4/77abbe35078be39718a46cd49caf16bceb35662f97a34101dca28aa98e47/greenlet-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:089fff7a6ce8d9316d1f65ebc00273a56be258c1725b32b94de90a3a979557e1", size = 1635602, upload-time = "2026-05-20T13:14:36.344Z" },
+ { url = "https://files.pythonhosted.org/packages/37/f7/129f27ca700845b8ee8ca88ce7f43435a1239c2eddb7677fc938822762cf/greenlet-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:110a1ca7b49b014b097f6078272c3f4ed31af45b254de5228b79adba879f6af9", size = 238683, upload-time = "2026-05-20T13:11:50.57Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/5c/a485a36e87df8d8fd0632ee01511244f5156a20ed3746cc6599340326395/greenlet-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f16ba1efc0715b680a18b8123d90dad887c6112ae3555b4b5c32c149540c6b4e", size = 235499, upload-time = "2026-05-20T13:12:42.028Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cb/c62454606daf5640369c94d8a9dd540599b1bfc090e2d2180cb77f4038d2/greenlet-3.5.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8ab31c9de8651a2facdd5c5bb0011f2380dd1a7af78ce2adf4b56095294fc07", size = 285579, upload-time = "2026-05-20T13:08:56.396Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/71/c4270398c2eba968a6071af1dfbdcaeee6ec1c24bc8b435b8cc452700da6/greenlet-3.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e300185139abc337ade480c327183adf42a875ac7181bfe66d7d4efea31fbea", size = 651106, upload-time = "2026-05-20T14:00:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/ab/71e34b78a44ec271fb5f550c17bc46d301ddc5953890d935f270b0dcdb5a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7ffdb990dcaa0234cf9845aead5df2e3c3a8b6507d409274dd87e0d5ab05ffc2", size = 663478, upload-time = "2026-05-20T14:05:45.88Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/2d/2d80842910da44f78c286532d084b8a5c3717c844ae80ceb3858738ae89a/greenlet-3.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c09df69dc1712d131332054a858a3e5cca400967fa3a672e2324fbb0971448c", size = 667767, upload-time = "2026-05-20T14:09:12.15Z" },
+ { url = "https://files.pythonhosted.org/packages/77/96/4efd6fa5c62c85426a0c19077a586258ebc3a2a146ff2493e4312a697a22/greenlet-3.5.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f82b3597e9d83b63408affed0b48fd0f54935edac4302237b9a837be0dae33c", size = 660800, upload-time = "2026-05-20T13:14:29.129Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d3/dad2eecedfbb1ed7050a20dcfae40c1442b74bc7423608be2c7e03ee7133/greenlet-3.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:a4764e0bfc6a4d114c865b32520805c16a990ef5f286a514413b05d5ecd6a23d", size = 470786, upload-time = "2026-05-20T14:01:42.064Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/e0/6c71401a25cac7000261304e866a2f2cc04dc74810d40e2f118aa4799495/greenlet-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c0141e37414c10164e702b8fb1473304221ad98f71600850c6ef7ff4880feba0", size = 1617518, upload-time = "2026-05-20T14:02:28.662Z" },
+ { url = "https://files.pythonhosted.org/packages/41/26/c5c06643e8c0af9e7bf18e16cb51d0ab7625155f0392e1c9015d66d556cd/greenlet-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:50ae25a67bea74ea41fb14b960bc532df73eb713417b2d61892dced82fe8d3bc", size = 1681593, upload-time = "2026-05-20T13:14:39.417Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/bd/e11a108317485075e68af9d23039619b86b28130c3b50d227d42edece64b/greenlet-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:8a17c42330e261299766b75ac1ea32caa437a9453c8f65d16a13140db378ecd3", size = 239800, upload-time = "2026-05-20T13:09:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f8/8e8e8417b7bf28639a5a56356ef934d0375e1d0c70a57e04d7701e870ffe/greenlet-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:7b5f5fae05b8ac6d176a61b60c394a8cbdc2b5b91b81793066e68745cf165e54", size = 236862, upload-time = "2026-05-20T13:09:10.498Z" },
+ { url = "https://files.pythonhosted.org/packages/90/12/41bf27fde4d3605d3773ae57751eda182b8be2f5398011c041173b1d9534/greenlet-3.5.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:ea8da1e900d758d078810d4255d8c6aa572181896a31ec79d779eb79c3adc9ad", size = 293637, upload-time = "2026-05-20T13:12:35.529Z" },
+ { url = "https://files.pythonhosted.org/packages/44/44/ba14b23e9757707050c2f397d305bbcae62e5d7cad122f8b6baec5ae4a1f/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a19570c52a21420dcbc94e661994bc325c0b5b11304540fed514586da5dc8f2e", size = 650840, upload-time = "2026-05-20T14:00:11.079Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/37/5ddc2b686a6844f91abecef43411842426da2e1573f60b49ecf2547f4ae1/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d955c89b75eeca4723d7cc14135f393cd47c32e2a6cb4a8e4c6e760a26b0986", size = 656416, upload-time = "2026-05-20T14:05:47.118Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/46/5987dcd1a2570ba84f3b187536b2ca3ae97613387e57f5cfa99df068fe5e/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea37d5a157eb9493820d3792ac4ece28619a394391d2b9f2f78057d396ff0f0f", size = 656607, upload-time = "2026-05-20T14:09:13.949Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/f0/d17510297c35a2992712f0bf84de3779749999f7d3d63aa1f09db7c62dbe/greenlet-3.5.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2daaaebd1a5aa88c49045b6baf9310b3263796bd88db713edf37cf53e7bb4e", size = 654397, upload-time = "2026-05-20T13:14:30.696Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/c1/6da0a9ddcc29d7e51ef14883fa3dc1e53b3f4ffba00582106c7bf55da1d8/greenlet-3.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:8d8a23250ea3ec7b36de8fa4b541e9e2db3ee82915cc060ab0631609ad8b28de", size = 488287, upload-time = "2026-05-20T14:01:43.143Z" },
+ { url = "https://files.pythonhosted.org/packages/37/eb/147387705bb89092645b012586e7273cb5ed3c90ef7eaf3a69173eaf0209/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bfbd69cc349e43bf3a8ae1c85548ff0718efc887615c2db16c3833d7b0b072d", size = 1614469, upload-time = "2026-05-20T14:02:30.192Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/4e/37ee0da7732b7aa9896f17e15579a9df34b9fcb9dd494f0adfa749af6623/greenlet-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4378720dd888136c27215a0214d32a4d37c3852765d45bc37aad0623423cfd78", size = 1675115, upload-time = "2026-05-20T13:14:40.972Z" },
+ { url = "https://files.pythonhosted.org/packages/57/f3/97dfcf4a6eb5077f8a672234216fb5923eb89f2cab7081cb10b2cf75b605/greenlet-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:45718441607f9325d948db98cbc691276059316d0358c188c246da4e1d4d23d2", size = 245246, upload-time = "2026-05-20T13:12:22.646Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/73/d7f72e34b582f694f4a9b248162db7b09cc458a259ba8f0c0bfa1a34ea7d/greenlet-3.5.1-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:2baee5ca02031757ffe8cc3d69f0cc0aec7065ce362622da74f32d3bcab1c541", size = 285575, upload-time = "2026-05-20T13:12:07.043Z" },
+ { url = "https://files.pythonhosted.org/packages/df/59/fa9c6e87dc8ad27a95dabe2f29f372b733d05a8a67470f6c901ed9975655/greenlet-3.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b1ec3274918a81d3ea778b9e75b56b72b33f300edb6cf7f3a7fe1dae56683de", size = 656428, upload-time = "2026-05-20T14:00:12.556Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/f9/e753408871eaa61dfe35e619cfc67512b036fde99893685d50eea9e07146/greenlet-3.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:111e2390ffffc47d5840b01711dd7fac07d4c09283d0283e7f3264b14e284c64", size = 667064, upload-time = "2026-05-20T14:05:48.662Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/74/807a047255bf1e09303627c46dc043dca596b6958a354d904f32ab382005/greenlet-3.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:10a9a1c0bfbc93d41156ffcb90c75fbc05544054faf15dcc1fdf9765f8b607f0", size = 672962, upload-time = "2026-05-20T14:09:15.532Z" },
+ { url = "https://files.pythonhosted.org/packages/96/27/5565b5b40389f1c7753003a07e21892fda8660926787036d5bc0308b8113/greenlet-3.5.1-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e630136e905fe5ff43e86945ae41220b6d1470956a39220e708110ac48d01ea5", size = 665697, upload-time = "2026-05-20T13:14:32.943Z" },
+ { url = "https://files.pythonhosted.org/packages/76/32/19d4e13225193c29b13e308015223f7d75fd3d8623d49dd19040d2ce8ec1/greenlet-3.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:ef08c1567c78074b22d1a200183d52d04a14df447bf70bcbb6a3507a48e776fc", size = 476047, upload-time = "2026-05-20T14:01:44.39Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/82/e7de4178c0c2d1c9a5a3be3cc0b33e46a85b3ee4a77c071bf7ad8600e079/greenlet-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:975eac34b44a7077ca4d421348455b94f0f518246a7f14bc6d2fdcfe5b584368", size = 1621256, upload-time = "2026-05-20T14:02:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/00/10/f2dddcf7dacac17dfc68691809589adad06135eb28930429cf58a6467a2f/greenlet-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9ab3c3a0b2ae6198e67c898dad5215a49f9ae0d0081b3c3ec59f333e39eeca26", size = 1685956, upload-time = "2026-05-20T13:14:42.55Z" },
+ { url = "https://files.pythonhosted.org/packages/22/17/4a232b32133230ada52f70e9d7f5b65b0caef8772f01849bd8d149e7e4ca/greenlet-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:cbfc69be86e10dcfef5b1e6269d1d6926552aa89ee39e1de3353360c1b6989ab", size = 239802, upload-time = "2026-05-20T13:13:15.481Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ae/4e623a7e6d4d2a5f4cb8e4c82de4169fc637942caae68d6e676b8a128ac5/greenlet-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:92fd6d44ac5e5a887c8a5dc4a8ba0ba908527c31c12f78c6bc7dcfe8aab279f6", size = 236853, upload-time = "2026-05-20T13:15:37.301Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/57/816d9cff29119da3505b3d6a5e14a8af89006ac36f47f891ff293ee05af1/greenlet-3.5.1-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:a6fdf2433a5441ef9a95464f7c3e674775da1c8c1177fff311cee1acad4626ed", size = 293877, upload-time = "2026-05-20T13:10:19.078Z" },
+ { url = "https://files.pythonhosted.org/packages/23/a1/59b0a7c7d140ff1a75626680b9a9899b79a9176cab298b394968fb023295/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7546556f0d649f99f6a361098a55f761181bb2ea12ff150bb16d26092ad88244", size = 655333, upload-time = "2026-05-20T14:00:14.758Z" },
+ { url = "https://files.pythonhosted.org/packages/72/1b/5efe127597625042218939d01855109f352779050768b670b52edcc16a6c/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5ee3ea898009fa898f85f9982255d35278c477bebe185beca249cab42d4526c", size = 659443, upload-time = "2026-05-20T14:05:50.159Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/9d/1dcdf7b95ab3cf8c7b6d7277c18a5e167312f2b362ddfcc5d5e6d8d84b43/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a57b0d05a0448eed231d59c0ceb287dde984551e54cbc51ac2d4865712838e9c", size = 659998, upload-time = "2026-05-20T14:09:16.912Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/6d/c404246ea4d22d097a7426d0efb5b781bd7eb67715f09e79001bd552ab18/greenlet-3.5.1-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5c81f74d204d3edd136ebfd50dce53acbb776995d721a0fe801626cfc93b8cd", size = 658356, upload-time = "2026-05-20T13:14:35.091Z" },
+ { url = "https://files.pythonhosted.org/packages/05/7e/c4959664fc231d587d66d8e81f2095e98056ba1954beafdcbe635e251052/greenlet-3.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:b0703c2cef53e01baec47f7a3868009913ad71ec678bbecb42a6f40895e4ce62", size = 494470, upload-time = "2026-05-20T14:01:45.611Z" },
+ { url = "https://files.pythonhosted.org/packages/51/02/f8ee37fb6d2219329f350af241c27fcf12df57e723d11f6fc6d3bacdadaa/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:2c18ef16bf6d4dd410e4dd52996888ea1497be26892fe5bbc73580aba4287b8e", size = 1619216, upload-time = "2026-05-20T14:02:33.403Z" },
+ { url = "https://files.pythonhosted.org/packages/93/c5/3dc9475ace2c7a3680da12372cddd7f1ac874eb410a1ac48d3e9dab83782/greenlet-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:17d86354f0ae6b61bf9be5148d0dd34e06c3cb7c602c671f79f29ac3b150e659", size = 1678427, upload-time = "2026-05-20T13:14:43.71Z" },
+ { url = "https://files.pythonhosted.org/packages/df/4e/750c15c317a41ffb36f0bf40b933e3d744a7dede61889f74443ea69690cf/greenlet-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:e7516cf6ae6b8a582c2770a0caed47b8a48373ed732c33d69a72913ae6ac923e", size = 245225, upload-time = "2026-05-20T13:13:59.366Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/fd/d3baea2eeb7b617efd47e87ca06e2ec2c6118d303aa9e918e0ce16eadc10/greenlet-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:5028648bf2253ec4745add746129d3904121fa7fe871a76bed23c5720573ce0a", size = 239590, upload-time = "2026-05-20T13:13:37.382Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.81.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/15/f3/23f47b24f8d8c2028eba501db3acfbb2f592cbb5995eaa6e363a627b74d7/grpcio-1.81.0.tar.gz", hash = "sha256:a5acd7efd3b1fe9b4eb0bcaaa1507eed68a0ad0678b654c3f7b464df9ba9dca5", size = 13032272, upload-time = "2026-06-01T05:56:22.827Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/a8/9916ab10a0201f4c7afb6918125aa2f38a7626ee18ffbc066dd9cb04a74d/grpcio-1.81.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:794e6aa648e8df47d8f908dc8c3b42347d04ec58438f1dcd4e445f09b4f6b0ce", size = 6093557, upload-time = "2026-06-01T05:54:32.64Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/43/99e969a048904a65df3129ee53c5f523b7c4e43127786460cac4bee82470/grpcio-1.81.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cd78145b7f7784661c524624f3526c9c6f891b30a4b54cb93a40806d0d0d61e9", size = 12075345, upload-time = "2026-06-01T05:54:35.77Z" },
+ { url = "https://files.pythonhosted.org/packages/83/70/4c3a204e190333768d4f63f4ff56bd0bf405f05b9188f3a59a8bcf161f8b/grpcio-1.81.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:638ccc1b86f7540170a169cb900799b9296a1381e47879ce60b0de9d3db73d33", size = 6640664, upload-time = "2026-06-01T05:54:38.854Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/a9/0fa17ac8b4e29cf59b26915be6cab8c0d4583ce24a6208a287b6e5f6d072/grpcio-1.81.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:21ec30b9ea320c8207ea7cd05873ad64aa69fdd0e81b6758b3347983ba20b50a", size = 7332542, upload-time = "2026-06-01T05:54:41.39Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/18/7c8e3d0dda2fb7a17076fcd6c9085209eabad3354696c64230f87b3a14eb/grpcio-1.81.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dbdb99986548a7e87f8343805ef315fd4eb50ffaabf4fb1206e42f2542bb805d", size = 6842564, upload-time = "2026-06-01T05:54:43.57Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/19/2f1726c2e03ad3f3fe241e6b41534532ad580d595de14a4054ad84999c80/grpcio-1.81.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c36f5d5e97944cbda2d4096b4ae262e6e68506246b61582acf1b8591607f3ccc", size = 7446236, upload-time = "2026-06-01T05:54:46.042Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/dc/0321f892212e2c0bfe248cea24c00d7d7111639688ec5ffd8e36b5c02fe6/grpcio-1.81.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f355384e5543ab77a755a7085225ecc19f32b76032e851cbd8145715d79dec8", size = 8445633, upload-time = "2026-06-01T05:54:48.809Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/20/0e7ea7494955cf1beea3077b2fd2c04c84d4480c2ae85a1e1cfa150c62d7/grpcio-1.81.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:77eb4e9fe61486bd1198cc7236ebb0f70e66234e63c0348f40bc2553ed16a88b", size = 7873958, upload-time = "2026-06-01T05:54:52.135Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/6438e226046c2a0778060e2b1d791a4827277bbd9d223013c2c63ee7435e/grpcio-1.81.0-cp311-cp311-win32.whl", hash = "sha256:7915a2e63acdc05264a206e1bddfd8e1fb8a29e406c18d72d30f8c124e021374", size = 4202110, upload-time = "2026-06-01T05:54:54.134Z" },
+ { url = "https://files.pythonhosted.org/packages/42/6b/d0895e93d65b186f5f1737fcc186d7faa487e2d9d934eda111a37a309869/grpcio-1.81.0-cp311-cp311-win_amd64.whl", hash = "sha256:5e925a70fe99fe5794f7beca0ea034c75f068afcc356d79047e73f99cdcca34c", size = 4940942, upload-time = "2026-06-01T05:54:56.749Z" },
+ { url = "https://files.pythonhosted.org/packages/82/d5/896a3aaf07068d707d88b282a04914b872db4d32d3c7e6d88e43a3b911fa/grpcio-1.81.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:57b3b0e73a518fa286959b40c3eddd02703504ca186e8b7b2945954519bd8b2c", size = 6053538, upload-time = "2026-06-01T05:54:58.965Z" },
+ { url = "https://files.pythonhosted.org/packages/68/6a/7e3eafa4727cd405ff917605ed2949e2af162f233f5cbdd773723a5fea7d/grpcio-1.81.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8bb1789c94322a13336a2b6c58d9c14d68f8628b6e24205a799c69f5bf8516ce", size = 12053447, upload-time = "2026-06-01T05:55:01.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/79/a4302aa82428de48a922421f522b027a1a727ab4d0926368454aa953d36d/grpcio-1.81.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e4d053900a0d24b75d7521139a3872150301b3d6bde3bed5e12318fb25791e4d", size = 6595872, upload-time = "2026-06-01T05:55:04.946Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/1f/7ff2850eaefbecf99af3f624dbb28dd1ad6c5fd4c1d8c26909ed6482673b/grpcio-1.81.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:db217c2e52931719f9937bd12082cd4d7b495b35803d5760686975c285924bf8", size = 7303857, upload-time = "2026-06-01T05:55:07.205Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/98/1f3896a9baae1f2aedf4e99c55291d6fa1f30ad9603d63bc18bda967b53e/grpcio-1.81.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19f201da7b4e5c0559198abe5a97157e726f3abe6e8f5e832d4a50740f6dcc22", size = 6809676, upload-time = "2026-06-01T05:55:09.513Z" },
+ { url = "https://files.pythonhosted.org/packages/34/8b/3441983718095208c5d797fd3239882e97ea89a629f41c8df94b4eef4df9/grpcio-1.81.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:275144b0115353339dbb8a6f28a9cf8997b5bf40e37f8f66ac0b0ea57e95b43f", size = 7412654, upload-time = "2026-06-01T05:55:12.777Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/98/1eddf07df6e4fe85cf67502a793f7b05468b2dca3d1ef35b972cf5d54468/grpcio-1.81.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5192857589f223e5a98ff0e31f6e551b19040e647d17bfe10116c8a2ce3b8696", size = 8408026, upload-time = "2026-06-01T05:55:15.514Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/73/3860341e6a1f5347be6ab35c6c0e1e3a8eb59d010388207fd561dcf01a88/grpcio-1.81.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6ff087cb1f563f47b504b4e29e684129fc5ae4863faf3ebca08a327764ee6cb", size = 7849498, upload-time = "2026-06-01T05:55:18.078Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3f/0ea06bd85c701966aa3f8f37314f2ed83520d2b7590f42d643d445d8bc8b/grpcio-1.81.0-cp312-cp312-win32.whl", hash = "sha256:98c6240f563178fc5877bd50e6ff274463e53e1472128f4110742450739659fa", size = 4184161, upload-time = "2026-06-01T05:55:20.127Z" },
+ { url = "https://files.pythonhosted.org/packages/39/e3/a7c387406827a86f99ad7838b995bf9b4a182ffe2d2c439ed2873efec952/grpcio-1.81.0-cp312-cp312-win_amd64.whl", hash = "sha256:87e33b7afcfb3585121b5f007d2c52b8c534104d18f556e840d35193ca2a9141", size = 4929958, upload-time = "2026-06-01T05:55:22.736Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/29/779ee53c931d0fd55c1d459fde43e485172caa3ac87cbd43d003a13a0185/grpcio-1.81.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:62bbe463c9f0f2ff24e31bd25f8dd8b4bae78900e315915a3195a0ef1471a855", size = 6054973, upload-time = "2026-06-01T05:55:25.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/b6/7211807926b5a17f8d9a5d47c739a163d6812fefe3e4714e81cf92945ed7/grpcio-1.81.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43c121e135ae44d1559b430db2b2dfad7421cbbe40e1deba506c7dc62b439719", size = 12048662, upload-time = "2026-06-01T05:55:28.453Z" },
+ { url = "https://files.pythonhosted.org/packages/64/89/b1b93ef6b34bd20bbaf707fa99133bc9cc302139d5ec6f77a165c7169796/grpcio-1.81.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f345de40ef2e65f63645d53d251824e6070e07804827c5b00ec2e44555f9f901", size = 6599116, upload-time = "2026-06-01T05:55:31.185Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/bc/c89f9b9d1c22895715356a1e009554dae66319e97826bb4d30bcda7d29e8/grpcio-1.81.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8c0855a350886f713b9e458e2a10d208009dcaa849f574e39cd6067db1fe1279", size = 7307591, upload-time = "2026-06-01T05:55:33.463Z" },
+ { url = "https://files.pythonhosted.org/packages/65/4a/1df2a4cb4a1386e066ab7e4175e34bb884b35ccb60d3621c09c84af6aabb/grpcio-1.81.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a524cd530900bd24511fcb7f2ed144da4ea37711c4b094475d0bceca7a93a170", size = 6811797, upload-time = "2026-06-01T05:55:36.731Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/dc/fa189d20601a1be25b08850cfb733879bbb1047b62a8feec3a60e3e1a87b/grpcio-1.81.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7746ba3e6efc9e2b748eff59470a2b8684d5a9ec607c6580bcaa5be175820bc", size = 7415131, upload-time = "2026-06-01T05:55:39.451Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/a3/5625c48cb48d23c6631b3e5294f88e4c751f22a52591ae78859fab96dca1/grpcio-1.81.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:aaaa4f7f2057d795952e4eacf3f342be8b5b156992f6ac85023c8b98794ebd47", size = 8408398, upload-time = "2026-06-01T05:55:42.219Z" },
+ { url = "https://files.pythonhosted.org/packages/75/34/0f8202c6809a46c2b4d69125ef3667c40b1c211f8e19930e5fa1f1197039/grpcio-1.81.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0fba53cb96004b2b7fb758b46b2288cb49d0b658316a4e73f3ef67230616ee65", size = 7844481, upload-time = "2026-06-01T05:55:44.849Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/95/c3366b5b5edf4c4adc90f2e29ca16e57965a8e56dc8d2ee89565ba1905bb/grpcio-1.81.0-cp313-cp313-win32.whl", hash = "sha256:c197e2ef75a442528072b29e9755da299110e8610e8bcbb59a6b4cf55384f005", size = 4182777, upload-time = "2026-06-01T05:55:47.459Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/a7/932f2f748511a32e641a2aba0d30dded3ed6e8bc330e0924e4d5d86853e6/grpcio-1.81.0-cp313-cp313-win_amd64.whl", hash = "sha256:194eddfacc84d80f50512e9fd4ee851d5f2499f18f299c95aa8fb4748f0537e0", size = 4928085, upload-time = "2026-06-01T05:55:50.158Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/1d/28b231333857deb840bc3d182ae087510170ea6d68f21393aeb0fe499530/grpcio-1.81.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:a9351055f52660b58f3d4890ea66188b5134399f82b11aa0c55bd4b99eff5390", size = 6055712, upload-time = "2026-06-01T05:55:52.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/b8/999c14f9dff0fc47549d2e827cba1343ddc18e1d1bf0d06d2cf628eecbd9/grpcio-1.81.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:300f3337b6425fd16ead9a4f9b2ac25801acb64aa5bc0b99eb69901645b2b1d2", size = 12057189, upload-time = "2026-06-01T05:55:55.952Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/3d/1fbde079572562af65351151d840525a13879eb7b481d35b55cd64c6127a/grpcio-1.81.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:97bbd623f7ded558fd4f7cb5a4f600c4d4de65c5dd364c83a5b14b2a10a2d3b5", size = 6608136, upload-time = "2026-06-01T05:55:59.069Z" },
+ { url = "https://files.pythonhosted.org/packages/32/89/1f17cb6882abfd8e5a303a25d5d1665abef5a8c499a96198c65a651d1b85/grpcio-1.81.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ff83d889e3ebf6341c8c7864ad8031591ad5ca61599072fc511644d1eb962d2b", size = 7307045, upload-time = "2026-06-01T05:56:02.376Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/f98e91b2e755652e637ea2144318b0229b290062199f761b445fe1fa6015/grpcio-1.81.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c4fe218c5a35e1d87a5a26544237f1fa41dfd9cbd3c856b0810a30061f8b0aaf", size = 6812794, upload-time = "2026-06-01T05:56:05.777Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0c/77892d715ac41e7ec0ace2a50080ffb64e189188056f607a66fe0014d1ee/grpcio-1.81.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b8b025b6af43ee0ad4a70307025d77bcab5adde7c4597786010d802c203e9fc5", size = 7422767, upload-time = "2026-06-01T05:56:08.524Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/b8/aa04590c6564714d94954515f15a236e59d4b9b3ad01e615f1b706d7792d/grpcio-1.81.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:3d4e0ce5a40a998cf608c8ba60ecfe18fdf364a9aa193ae4ac3faeecd0e86757", size = 8408551, upload-time = "2026-06-01T05:56:11.283Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3d/4f4a3450a1973568910c6909cb74abbf2126f68aefae5976962f9f7ad50d/grpcio-1.81.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa948712c8e5fa40ec250870bda14bc7578e1bb832a8912d9d2a0f720518edbe", size = 7846468, upload-time = "2026-06-01T05:56:14.536Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f4/5827fd248221ad3b44161c23ce9b5f4ee405b04fc6da5fd402a9aa87a84a/grpcio-1.81.0-cp314-cp314-win32.whl", hash = "sha256:fbbe81314a9d92156abce8b62c09364eb8bafc0ca2a19919a45ec64b5c6cb664", size = 4264427, upload-time = "2026-06-01T05:56:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e8/127dc2b246096ad50ef7c8d9b7b31d757787aeb796368bcdd4454e4204c4/grpcio-1.81.0-cp314-cp314-win_amd64.whl", hash = "sha256:b93cee313cae4e113fbb3a0ce1ea5633db6f63cfde2b2dc1d817429026b2a50b", size = 5070848, upload-time = "2026-06-01T05:56:19.735Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
+ { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
+ { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
+ { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
+ { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
+ { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
+ { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
+ { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
+ { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
+ { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "tqdm" },
+ { name = "typer" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/65/9826515abb600b5722bcf53f8b4a2fb58340b1f8bfcaee19f83561c13a44/huggingface_hub-1.17.0.tar.gz", hash = "sha256:fad842b6763ef70ebc3919665b1b9273645203185400a7d6c5eddc2323cc3435", size = 797082, upload-time = "2026-05-28T15:12:13.347Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/28/d7cef5e477b855c25d415b8f57e5bc7347c7a90cad3acf1725d0c92ca294/huggingface_hub-1.17.0-py3-none-any.whl", hash = "sha256:3b8156d23118e87f6a587648bfbc04f04a12a757ccb4ed298b35c4ae638bf24c", size = 671546, upload-time = "2026-05-28T15:12:11.441Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "jiter"
+version = "0.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/b5/55f06bb281d92fb3cc86d14e1def2bd908bb77693183e7cb1f5a3c388b0c/jiter-0.15.0.tar.gz", hash = "sha256:4251acc80e2b7c9b7b8823456ea0fceeb0734dac2df7636d3c711b38476b5a76", size = 166640, upload-time = "2026-05-19T10:09:48.361Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/13/daa722f5765c393576f466378f9dfd29d77c9bed939e0688f96afa3601ea/jiter-0.15.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0f862193b8696249d22ec433e85fd2ab0ad9596bc3e45e6c0bc55e8aeba97be2", size = 310899, upload-time = "2026-05-19T10:07:12.89Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/82/2d2551829b082f4b6d82b9f939b031fb808a10aab1ec0664f82e150bb9a2/jiter-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1303d4d68a9b051ea90502402063ecf3807da00ad2affa19ca1ae3b90b3c5f67", size = 314963, upload-time = "2026-05-19T10:07:14.539Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0a/8b1a51466f7fe9f31dbe4bc7e0ca848674f9825e0f737b929b97e8c60aa7/jiter-0.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392b8ab019e5502d08aff85c6272209c24bc2cbe706ea82a56368f524236614a", size = 341730, upload-time = "2026-05-19T10:07:15.869Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/2a/e71dea19822e2e404e83992a08c1d6b9b617bb944f28c9c2fbd85d02c91e/jiter-0.15.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:773b6eb282ce11ee19f05f6b2d4404fa308e5bbd353b0b80a0262caad6db2cd7", size = 366214, upload-time = "2026-05-19T10:07:17.259Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/97e1fa539d124a509a00ab7f669289d1c1d236ecabf12948a18f16c91082/jiter-0.15.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2c0c44d569ce0f2850f5c926f8caeb5f245fbc84475aeb36efccc2103e6dbd", size = 459527, upload-time = "2026-05-19T10:07:18.741Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/7a/4a68d331aef8cf2e2393c14a3aacb635c62aa86071b0229899fb5baaa907/jiter-0.15.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032396229564bca02440396bd327710719f724f5e7b7e9f7a8eb3faa4a2c2281", size = 375451, upload-time = "2026-05-19T10:07:20.208Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7e/1c445c2b6f0e30a274dc8082e0c3c7825411cce80d726bccd697c98cc8d3/jiter-0.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d37768fce7f88dd2a8c6091f2325dea27d30d30d5c6e7a1c0f0af77723b708", size = 349428, upload-time = "2026-05-19T10:07:22.372Z" },
+ { url = "https://files.pythonhosted.org/packages/00/94/e20d38984fc17a636371bffd2ae0f698124fdc8e75ef969cd2da6ba7cea7/jiter-0.15.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2c9cb907439d20bd0c7d7565ca01ee52234203208433749bae5b516907526928", size = 355405, upload-time = "2026-05-19T10:07:23.916Z" },
+ { url = "https://files.pythonhosted.org/packages/94/fa/4d09f814779d0ea80a28ed8e4c6662ec9a4a8ecef0ac52190ebac6262d14/jiter-0.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9100ddbec09741cc66feb0fc6773f8bdbd0e3c345689368f260082ff85dcc0cd", size = 393688, upload-time = "2026-05-19T10:07:25.854Z" },
+ { url = "https://files.pythonhosted.org/packages/54/9d/8eb5d4fb8bf7e93a75964a5da71a75c67c864baf7fa3f98598187b3c7e57/jiter-0.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae1b0d82ac2d987f9ea512b1c9adfcc71a28de3dea3a6039b54d76cffda9901e", size = 520853, upload-time = "2026-05-19T10:07:27.303Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/2c/5e07874e59e623a943a0acf1552a80d05b70f31b402287a8fc6d7ec634c7/jiter-0.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8020c99ec13a7db2b6f96cbe82ef4721c88b426a4892f27478044af0284615ef", size = 551016, upload-time = "2026-05-19T10:07:28.846Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/d2d34422143474cadc15b60d482b1c35683dbc5c63c24346ddd0df09bcaf/jiter-0.15.0-cp311-cp311-win32.whl", hash = "sha256:42bfb257930800cf43e7c62c832402c704ab60797c992faf88d20e903eac8f32", size = 209518, upload-time = "2026-05-19T10:07:30.431Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/7d/52778b930e5cc3e52a37d950b1c10494244308b4329b25a0ff0d88303a81/jiter-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:860a74063284a2ae9bfedd694f299cc2c68e2696c5f3d440cc9d18bb81b9dd04", size = 200565, upload-time = "2026-05-19T10:07:32.125Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/4f/d9b4067feb69b3fa6eb0488e1b59e2ad5b463fe39f59e527eab2aca00bb0/jiter-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:37a10c377ce3a4a85f4a67f28b7afe093154cde77eaf248a72e856aa08b4d865", size = 195488, upload-time = "2026-05-19T10:07:33.846Z" },
+ { url = "https://files.pythonhosted.org/packages/44/53/4f6bddbcde3c71e56d0aa1337ec95950f3d27dd4153e25aadf0feac71751/jiter-0.15.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e90a1c315a0226ec822d973817967f9223b7701546c8c2a7913e7ab0926294d", size = 308793, upload-time = "2026-05-19T10:07:35.25Z" },
+ { url = "https://files.pythonhosted.org/packages/01/84/c01099b59a285a1ebba64ae93f62bfa036675340fd1b0045ae65890a0442/jiter-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8c9004af7c8d67cce7f1aae1026fb55607f4aa600710d08ede3a3ce4aeefe7e0", size = 309570, upload-time = "2026-05-19T10:07:36.919Z" },
+ { url = "https://files.pythonhosted.org/packages/58/64/8fb7f9d45bb98190355454cd04dad8d8f27223d6bd52f83af07f637168a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c210f8b35dc6f30aafd4b4365ca89b9d1189f21ab49b8e68fa6322a847aef138", size = 336783, upload-time = "2026-05-19T10:07:38.694Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/b6/f5739011d009b3a30f6a53c5240979030ba29ae46a8c67e3a15759f7c37d/jiter-0.15.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f30bae8bc1c2d613e28e5af3e8cceb09b742f1c8a8a5f839fb67afaffc03b61", size = 363555, upload-time = "2026-05-19T10:07:40.832Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/12/98a9d9f766665e8a3b6252454e17cb0c464606a28cf2fa09399b003345fa/jiter-0.15.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c60e71b6d10cfc284c9bf36bd885e8d44c46f688ce50aa91b5edd90181dea687", size = 452255, upload-time = "2026-05-19T10:07:42.62Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/d5/60f972840f79c5e7544fce567c56f1e4e50468f996baba3e78d823dd62a6/jiter-0.15.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ab068bce62a45aa3e7367eceaffb5dde60b7eb853be8dece45132e3d0ff4879", size = 373559, upload-time = "2026-05-19T10:07:44.201Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/cf/d46ef1234ba335aabc2f013210db8e0821a22f5e644a2e9449df199ecc23/jiter-0.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa248c9eb220197d363f688818dac2fd4b2f0cd7d843ca7105d652034823427d", size = 346055, upload-time = "2026-05-19T10:07:46.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/63/4d2749d8d54d230bad9b3a6b0d00cc28c6ff6b2fdffc26a8ccf76cc5a974/jiter-0.15.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2a77aadd57cac1682e4401a72724d2796d89a4ba129b1a5812aa94ee480826eb", size = 351406, upload-time = "2026-05-19T10:07:47.855Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b9/9965b990035d8773328e0a8c8b457a87bf2b19f6c4126d9d99296be5d16a/jiter-0.15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ae901f3a55bfafdde31d289590fa25e3245735a2b1e8c7cc15871710a002871", size = 389357, upload-time = "2026-05-19T10:07:49.665Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/55/9ddf903deda1413e87fed792f416b7123daee5b8efbad6a202a7421c36a5/jiter-0.15.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f0b271b462769543716f92d3a4f90527df6ef5ed05ee95ec4137f513e21e1b77", size = 517263, upload-time = "2026-05-19T10:07:51.537Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/76/a0c40ad064d3a20a4fde231e35d56e9a01ce82164278180e82d5daf85469/jiter-0.15.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2fb6a5d26af81fc0f00f9360a891e05cf755e149bba391c4d563adc54812973d", size = 548646, upload-time = "2026-05-19T10:07:53.196Z" },
+ { url = "https://files.pythonhosted.org/packages/23/4f/eca9b954942916ba2f453891b8593ab444cd872396fe66a3936616f236f3/jiter-0.15.0-cp312-cp312-win32.whl", hash = "sha256:c2f6bb8b5216ab9e7873bc08b5d7bef2b8abbb578a3069bf1cd14a45d71d771d", size = 206427, upload-time = "2026-05-19T10:07:55.307Z" },
+ { url = "https://files.pythonhosted.org/packages/95/bf/8ead82a87495149542748e828d153fd232a512a22c83b02c4815c1a9c7d8/jiter-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:40b2c7e92c44a84d748d21706c68dc6ff8161d80b59c99d774721a0d2317d7c7", size = 197300, upload-time = "2026-05-19T10:07:56.651Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/e4/9b8a78fb2d894471bc344e37f1949bdd784bd914d031dba0ba3a40c71dd7/jiter-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:cc0bc345cf2df9d1c00ac443f50d543c1ccfa8b0422cb85b1ab70d681c0b255b", size = 192702, upload-time = "2026-05-19T10:07:58.307Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f4/f708c900ecee41b2025ef8413d5351e5649eb2125c506f6720cc69b06f5c/jiter-0.15.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c11465f97e2abf45a014b83b730222f8f1c5335e802c7055a67d50de6f1f4e3", size = 307829, upload-time = "2026-05-19T10:07:59.704Z" },
+ { url = "https://files.pythonhosted.org/packages/86/59/db537c0949e83668c38481d426b9f2fd5ab758c4ee53a811dd0a510626a0/jiter-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d1e7b1776f0797956c509e123d0952d10d293a9492dea9f288ab9570ec01d1a5", size = 308445, upload-time = "2026-05-19T10:08:01.184Z" },
+ { url = "https://files.pythonhosted.org/packages/37/38/ea0e13b18c30ef951da0d47d39e7fa9edb82a93a62990ffbd7cea9b622d4/jiter-0.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:351a341c2105aa430b7047e30f1bf7975f6313b00165d3fc07be2edaf741f279", size = 336181, upload-time = "2026-05-19T10:08:02.688Z" },
+ { url = "https://files.pythonhosted.org/packages/58/fc/2303901b16c4ba05865588990a420c0b4156270b44379c20931544a1d962/jiter-0.15.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ab395feec8d249ec4044e228e98a7033f043426a265df439dc3698823f0a4e4", size = 362985, upload-time = "2026-05-19T10:08:04.394Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/6f/11bace093c52e7d4d26c8e606ccd7ae8c972189622469ec0d9e28161e28b/jiter-0.15.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2a438005b6f22d0273413484d6094d7c2c5d10ec1b3a3bf128e0d1d3ba53258", size = 453292, upload-time = "2026-05-19T10:08:05.967Z" },
+ { url = "https://files.pythonhosted.org/packages/22/db/987f2f086ca4d7a6582eb4ccd513f9b26b42d9e4243a087609a3137a8fc7/jiter-0.15.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f18f85e4218d1b40f000f42a92239a7a61a902cd42c65e6c360dbd17dcb20894", size = 373501, upload-time = "2026-05-19T10:08:07.857Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/7c/89fbcabb2739b7a5b8dc959a1b6c5761f6484f5fed3486854b3c789bb1de/jiter-0.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1aa62e277fc1cbd80e6deacae6f4d983b41b3d7728e0645c5d741a6149bba45", size = 344683, upload-time = "2026-05-19T10:08:09.431Z" },
+ { url = "https://files.pythonhosted.org/packages/30/6f/6cca7692e7dddfec6d8d76c54dc97f2af2a41df4ac0674b999df1f09a5f3/jiter-0.15.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:6550fa135c7deb8ead6af49ed7ff648532ea8334a1447fe34a36315ef79c5c29", size = 350892, upload-time = "2026-05-19T10:08:11.352Z" },
+ { url = "https://files.pythonhosted.org/packages/39/14/0338d6190cb8e6d22e677ab1d4eabd4117f67cca70c54cd04b82ff64e068/jiter-0.15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:066f8f33f18b2419cd8213b2436fa7fbc9c499f315971cfa3ce1f9820c001b1b", size = 388723, upload-time = "2026-05-19T10:08:12.912Z" },
+ { url = "https://files.pythonhosted.org/packages/90/31/cc19f4a1bdb6afb09ce6a2f2615aa8d44d994eba0d8e6105ed1af920e736/jiter-0.15.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:75e8a04e91432dde9f1838373cf93d23726c79d3e908d319acf0e796f85592e7", size = 516648, upload-time = "2026-05-19T10:08:14.808Z" },
+ { url = "https://files.pythonhosted.org/packages/49/9f/833c541512cd091b63c10c0381973dfe11bc7a503a818c16384417e0c81e/jiter-0.15.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a97261f1fccb8e50ecd2890a96e46efdc3f57c80a197324c6777827231eca712", size = 547382, upload-time = "2026-05-19T10:08:16.927Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/11/e7b70e91f90bc4477e8eee9e8a5f7cf3cb41b4525d6394dc98a714eb8f7f/jiter-0.15.0-cp313-cp313-win32.whl", hash = "sha256:c77496cb10bd7549690fbbab3e5ec05857b83e49276f4a9423a766ddd2afcd4c", size = 205845, upload-time = "2026-05-19T10:08:18.401Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/23/5c20d9ad6f02c493e4023e5d2d09e1c1f15fe2753c9102c544aff068a88e/jiter-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b15741f501469009ae0ae90b7147958a664a7dede40aa7ff174a8a4645f546d0", size = 196842, upload-time = "2026-05-19T10:08:20.131Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/11/1eb400ef248e8c925fd883fbe325daf5e42cd1b0d308539dd332bd4f7ffc/jiter-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d6a60072b44c3c2b797a7ddcbcbbf2b34ea3cfd4721580fbfd2a09d9d9b84ba", size = 192212, upload-time = "2026-05-19T10:08:21.807Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/60/2fd8d7c79da8acf9b7b277c7616847773779356b92acfc9bb158452174da/jiter-0.15.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ef1fd24d9413f6209e00d3d5a453e67acfe004a25cc6c8e8484faed4311ab9e8", size = 315065, upload-time = "2026-05-19T10:08:23.218Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f4/008fb7d65e8ac2abf00811651a661e025c4ba80bbc6f378450384ddd3aed/jiter-0.15.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144f8e72cb53dab146347b91cceac01f5481237f2b93b4a339a1ee8f8878b67c", size = 339444, upload-time = "2026-05-19T10:08:24.701Z" },
+ { url = "https://files.pythonhosted.org/packages/00/55/90b0c7b9c6896c0f2a591dd36d36b71d22e09674bfef178fa03ba3f81499/jiter-0.15.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553fcac2ef2cb990877f9fc0833b8b629a3e6a5670b6b5fd58219b41a653ddc4", size = 347779, upload-time = "2026-05-19T10:08:26.408Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6b/69666cec5000fd57734c118437394516c749ae8dbeea9fb66d6fef9c4775/jiter-0.15.0-cp313-cp313t-win_amd64.whl", hash = "sha256:774f93f65031856bf14ad9f59bdcab8b8cad501e5ceabd51ba3525f76937a25b", size = 200395, upload-time = "2026-05-19T10:08:28.055Z" },
+ { url = "https://files.pythonhosted.org/packages/39/04/a6aa62cd27e8149b0d28df5561f10f6cceaf7935a9ccf3f1c5a05f9a0cd8/jiter-0.15.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f1e1754960f38ec40613a07e5e372df67acb3b890fb383b6fb3de3e49ddbf3c7", size = 190516, upload-time = "2026-05-19T10:08:29.35Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d2/079f350ebf7859d081de30aa890f9e3be68516f754f3ba32366ffff4dcee/jiter-0.15.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ac0d9ddea4350974be7a221fc25895f251a8fee748c889bdced2141c0fec1a49", size = 308884, upload-time = "2026-05-19T10:08:31.667Z" },
+ { url = "https://files.pythonhosted.org/packages/04/4e/a2c30a7f69b48c03b20935d647479106fe932f6e63f75faf53937197e05d/jiter-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01a8222cf05ab1128e239421156c207949808acaaea2bdfd33130ae666786e86", size = 310028, upload-time = "2026-05-19T10:08:33.304Z" },
+ { url = "https://files.pythonhosted.org/packages/40/90/2e7cdfd3cf8ca967be38c48f5cf474d79f089efaf559a40f15984a77ae69/jiter-0.15.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:182226cbc930c9fab81bc2e41a4da672f89539906dadb05e75670ac07b94f71f", size = 337485, upload-time = "2026-05-19T10:08:35.259Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/11/15a1aa28b120b8ee5b4f1fb894c125046225f09847738bd64233d3b84883/jiter-0.15.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:71683c38c825452999b5717fcae07ea708e8c93003e808be4319c1b02e3d176e", size = 364223, upload-time = "2026-05-19T10:08:36.694Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/25/f442e8af5f3d0dcf47b39e83a0efd9ee45ea946aa6d04625dc3181eae3b6/jiter-0.15.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f2218e6a9e5c18bc10fe6d41ac189c442c88eacf11bad9f28ef95a9bef00e6", size = 456387, upload-time = "2026-05-19T10:08:38.143Z" },
+ { url = "https://files.pythonhosted.org/packages/da/f4/37f2d2c9f64f49af7da652ed7532bb5a2372e588e6927c3fdd76f911db65/jiter-0.15.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5157de9f76eb4bc5ea74a1219366a25f945ad305641d74e04f59c54087091aa9", size = 374461, upload-time = "2026-05-19T10:08:39.869Z" },
+ { url = "https://files.pythonhosted.org/packages/60/28/edcfbbbf0cb15436f36664a8908a0df47ab9006298d4cd937dc08ea932d6/jiter-0.15.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c5db5527c221249a876160663ab891ace358c17f7b9c93ec1478b7f0550e5c", size = 345924, upload-time = "2026-05-19T10:08:41.668Z" },
+ { url = "https://files.pythonhosted.org/packages/47/13/89fba6398dab7f202b7278c4b4aac122399d2c0183971c4a57a3b7088df5/jiter-0.15.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:3e4540b8e74e4268811ac05db226a6a128ff572e7e0ce3f1163b693cadb184cd", size = 352283, upload-time = "2026-05-19T10:08:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/da/0f6af8cef2c565a1ab44d970f268c43ccaa72707386ea6388e6fe2b6cd26/jiter-0.15.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:62ebd14e47e9aed9df4472afcb2663668ce4d74891cd54f86bf6e44029d6dc89", size = 389985, upload-time = "2026-05-19T10:08:44.915Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ec/b9cb7d6d29e24ee14910266157d2a279d7a8f60ee0df7fa840882976ba64/jiter-0.15.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0be6f5ad41a809f303f416d17cec92a7a725902fb9b4f3de3d19362ac0ef8554", size = 517695, upload-time = "2026-05-19T10:08:46.486Z" },
+ { url = "https://files.pythonhosted.org/packages/64/5e/6d1bda880723aae0ad86b4b763f044362448efe31e3e819635d41cb03451/jiter-0.15.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:813dfbb17d65328bf86e5f0905dd277ba2265d3ca20556e86c0c7035b7182e5a", size = 548868, upload-time = "2026-05-19T10:08:48.026Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/72/7de501cf38dcacaf35098796f3a50e0f2e338baba18a58946c618544b809/jiter-0.15.0-cp314-cp314-win32.whl", hash = "sha256:50e51156192722a9c58db112837d3f8ef96fb3c5ecc14e95f409134b08b158ec", size = 206380, upload-time = "2026-05-19T10:08:49.738Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/a9/e19addf4b0c1bdce52c6da12351e6bc42c340c45e7c09e2158e46d293ccc/jiter-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:30ce1a5d16b5641dc935d50ef775af6a0871e3d14ab05d6fc54dff371b78e558", size = 197687, upload-time = "2026-05-19T10:08:51.088Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c9/776b1db01db25fc6c1d58d1979a37b0a9fe787e5f5b1d062d2eaacb77923/jiter-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:510c8b3c17a0ed9ac69850c0438dada3c9b82d9c4d589fcb62002a5a9cf3a866", size = 192571, upload-time = "2026-05-19T10:08:52.451Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/f6/45bb4670bacf300fd2c7abadbfb3af376e5f1b6ae75fd9bc069891d15870/jiter-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7553333dd0930c104a5a0db8df72bf7219fe663d731383b576bb6ed6351c984d", size = 317151, upload-time = "2026-05-19T10:08:53.867Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/68/ed635ad5acd7b73e454283083bbb7c8205ad10e88b0d9d7d793b09fe8226/jiter-0.15.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2143ab06181d2b029eedcb6af3cebe95f11bbac62441781860f98ee9330a6a6", size = 341243, upload-time = "2026-05-19T10:08:55.383Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/db/3ff4176b817b8ea33879e71e13d8bc2b0d481a7ed3fe9e080f333d415c16/jiter-0.15.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eac374c5c975709b69c10f09afd199df74150172156ad10c8d4fd785b7da995", size = 363629, upload-time = "2026-05-19T10:08:56.928Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/24/5f8270e0ba9c883582f96f722f8a0b58015c7ce1f8c6d4571cf394e99b6b/jiter-0.15.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3b3b775e33d3bfaec9899edc526ae97b0da0bf9d071a46124ba419149a414f8", size = 456198, upload-time = "2026-05-19T10:08:58.618Z" },
+ { url = "https://files.pythonhosted.org/packages/45/5b/76fc02b0b5c54c3d18c60653156e2f76fde1816f9b4722db68d6ee2c897e/jiter-0.15.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3071db3346334beae1360b46da4606da57bf3528c167b3c38533afaf9f2c5", size = 373710, upload-time = "2026-05-19T10:09:00.151Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/52/4310821b0ea9277994d3e1f49fc6a4b34e4800caebacb2c0af81da59a454/jiter-0.15.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6694a173ecabc12eb60efbc0b474464ead1951ff65cd8b1e72100715c64512b", size = 349901, upload-time = "2026-05-19T10:09:01.621Z" },
+ { url = "https://files.pythonhosted.org/packages/93/fe/67648c35b3594fba8854ac64cc8a826d8bcd18324bbdb53d77697c60b6ef/jiter-0.15.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a254e10b593624d230c365b6d616b22ca0ad65e63a16e6631c2b3466022e6ba8", size = 352438, upload-time = "2026-05-19T10:09:03.216Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/28/0a1879d07ad6b3e025a2750027363452ced93c2d16d1c9d4b153ffd51c91/jiter-0.15.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8d2955167274e15d79a7a020afdd9b39c990eb80b2d89fca695d92dcfdd38ec", size = 388152, upload-time = "2026-05-19T10:09:04.741Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/78/46c6f6b56ba85c90021f4afd72ed42f691f8f84daacb5fe27277070e3858/jiter-0.15.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:acf4ee4d1fc55917239fe72972fb292dd773055d05eb040d36f4326e02cc2c0e", size = 517707, upload-time = "2026-05-19T10:09:06.231Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/cb/720662d4c88fcad606e826fef5424365527ba43ce4868a479aed8f8c507e/jiter-0.15.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:e7196e56f1cd69af1dbb07dff02dcfb260a50b45a82d409d92a06fedb32473b5", size = 548241, upload-time = "2026-05-19T10:09:08.093Z" },
+ { url = "https://files.pythonhosted.org/packages/60/e3/935b8034fd143f21125c87d51404a9e0e1449186a494405721ff5d1d695e/jiter-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:7f6163c0f10b055245f814dcc59f4818da60dfe72f3e72ab89fc24b6bd5e9c52", size = 207950, upload-time = "2026-05-19T10:09:09.616Z" },
+ { url = "https://files.pythonhosted.org/packages/93/59/984fd9ece895953dad3e0880a650e766f5a2da2c5514f0eafdaaabbeb5f9/jiter-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:980c256edb05b78a111b99c4de3b1d32e31634b867fd1fc2cf726e7b7bba9854", size = 200055, upload-time = "2026-05-19T10:09:11.367Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/a4/cf8d779feb133a27a2e3bc833bccb9e13aa332cdf820497ebf72c10ce8c3/jiter-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:66b1880df2d01e206e8339769d1c7c1753bcb653efd6289e203f6f24ebada0c0", size = 191244, upload-time = "2026-05-19T10:09:12.74Z" },
+ { url = "https://files.pythonhosted.org/packages/65/43/1fc62172aa98b50a7de9a25554060db510f85c89cfbed0dfe13e1907a139/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:411fa4dfa5a7ae3d11491027ffb9beadec3996010a986862db70d91abba1c750", size = 305585, upload-time = "2026-05-19T10:09:35.995Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/c4/dd58fcd9e2df83666e5c1c1347bef58ce919cd8efc3ffa38aeea62ce493b/jiter-0.15.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:2b0074e2f56eb2dacca1689760fd2852a068f85a0547a157b82cb4cafeb6768b", size = 306936, upload-time = "2026-05-19T10:09:37.435Z" },
+ { url = "https://files.pythonhosted.org/packages/39/86/b695e16f1180c07f43ea98e73ecd21cf63fa2e1b0c1103739013784d11ae/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:913d02d29c9606643418d9ccfc3b72492ab25a6bf7889934e09a3490f8d3438b", size = 342453, upload-time = "2026-05-19T10:09:39.294Z" },
+ { url = "https://files.pythonhosted.org/packages/34/56/55d76614af37fe3f22a3347d1e410d2a15da581997cb2da499a625000bb5/jiter-0.15.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15d3ec9b0449c40e85319bdb4caa8b77ab526e74f5532ed94bec15e2f66822c", size = 345606, upload-time = "2026-05-19T10:09:40.727Z" },
+ { url = "https://files.pythonhosted.org/packages/73/38/505941b2b092fd5bbbd60a52a880db1173f1690ae6751bed3af1c9ddcb4e/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:631f13a3d04e97d4e083993b10f4b99530e3a10d953e2eb5e196b7dc7f812ce0", size = 303769, upload-time = "2026-05-19T10:09:42.203Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/95/a06692b29e77473f286e1ec1f426d3ca44d7b5843be8ad21d7a5f3fcdcc0/jiter-0.15.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b6c0ffae686c39bf3737be60793783267628783ea42545632c10b291105aee45", size = 305128, upload-time = "2026-05-19T10:09:43.657Z" },
+ { url = "https://files.pythonhosted.org/packages/23/85/7270d7ad41d6061a25b950c6bf91d638bd9aacb113200a8c8d57a055fd67/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d54fb5b31dea401a41af3f8a7d2512e9b6a6a005491e6166c7e4ffab9639a9c", size = 340459, upload-time = "2026-05-19T10:09:45.452Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" },
+ { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" },
+ { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" },
+ { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" },
+ { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" },
+ { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" },
+ { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" },
+ { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" },
+ { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" },
+ { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" },
+ { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" },
+ { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" },
+ { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" },
+ { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" },
+ { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" },
+ { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" },
+ { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
+ { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
+ { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[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, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ast-serialize" },
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41", size = 14691685, upload-time = "2026-05-11T18:33:27.973Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca", size = 13555165, upload-time = "2026-05-11T18:32:16.107Z" },
+ { url = "https://files.pythonhosted.org/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538", size = 13994376, upload-time = "2026-05-11T18:32:39.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398", size = 14864618, upload-time = "2026-05-11T18:34:49.765Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563", size = 15102063, upload-time = "2026-05-11T18:34:05.855Z" },
+ { url = "https://files.pythonhosted.org/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389", size = 11060564, upload-time = "2026-05-11T18:35:36.494Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666", size = 9966983, upload-time = "2026-05-11T18:37:14.139Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" },
+ { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" },
+ { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" },
+ { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" },
+ { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" },
+ { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" },
+ { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" },
+ { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "narwhals"
+version = "2.22.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9c/1c/c80cb7719721a44846c6301ef118434bae30a423924bfad3a47f16bdc064/narwhals-2.22.0.tar.gz", hash = "sha256:6486282bb7e4b4ab55963efbd8be1451b764cc4874b74d1fd625eba9dc60b86f", size = 417565, upload-time = "2026-06-01T13:34:36.249Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b6/e7cdde7b8e90d5dff25b622f95833ef26567ad184c977278b93a1cbd5717/narwhals-2.22.0-py3-none-any.whl", hash = "sha256:1421797ede01789cc1537619dbc3f36f840737240f748fdb24a60a0225fc80be", size = 453815, upload-time = "2026-06-01T13:34:34.127Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
+ { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
+ { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
+ { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
+ { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
+ { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
+ { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
+ { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
+ { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
+ { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
+ { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
+ { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
+ { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
+ { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
+ { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
+ { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
+ { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
+ { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
+ { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
+ { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
+ { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
+ { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
+ { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
+ { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
+ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
+ { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
+ { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
+ { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
+]
+
+[[package]]
+name = "nvidia-cublas"
+version = "13.1.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cuda-nvrtc" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti"
+version = "13.0.85"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" },
+ { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc"
+version = "13.0.88"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime"
+version = "13.0.96"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu13"
+version = "9.20.0.48"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" },
+]
+
+[[package]]
+name = "nvidia-cufft"
+version = "12.0.0.61"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" },
+]
+
+[[package]]
+name = "nvidia-cufile"
+version = "1.15.1.6"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" },
+]
+
+[[package]]
+name = "nvidia-curand"
+version = "10.4.0.35"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver"
+version = "12.0.4.66"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas" },
+ { name = "nvidia-cusparse" },
+ { name = "nvidia-nvjitlink" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse"
+version = "12.6.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu13"
+version = "0.8.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" },
+ { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu13"
+version = "2.29.7"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink"
+version = "13.0.88"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" },
+]
+
+[[package]]
+name = "nvidia-nvshmem-cu13"
+version = "3.4.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx"
+version = "13.0.85"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
+]
+
+[[package]]
+name = "openai"
+version = "2.40.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/9f/136562ec6c3b1a50fe06eb0bb34ed21f0d7426ec0140e5cc43ac785b69a5/openai-2.40.0.tar.gz", hash = "sha256:9a756f91f274a24ad6026cbcb2042fd356c8d4a10e8f347b08d34465e585f7a2", size = 781177, upload-time = "2026-06-01T21:48:23.878Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/46/180e14be801a75bc13f234cb1b594b232adeb9c84e60a9ab1832e8333591/openai-2.40.0-py3-none-any.whl", hash = "sha256:2b205637ff214477f9ce9ab035e9f494db0e3fa8f1e599008953735fbf6ff1ff", size = 1350935, upload-time = "2026-06-01T21:48:21.462Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl", hash = "sha256:51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", size = 61311, upload-time = "2026-05-21T16:32:28.822Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-exporter-otlp-proto-grpc" },
+ { name = "opentelemetry-exporter-otlp-proto-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/08/94/8637919a5d01f81dacf510234bc0110b944f4687a6e96b0a02adf2f6bdce/opentelemetry_exporter_otlp-1.42.1.tar.gz", hash = "sha256:2d9ebaed714377a67d224d46795ddcc11d2c877fa5de35fda70b6f3b010729a9", size = 6086, upload-time = "2026-05-21T16:32:51.963Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/4d/c26080295a36fd22e201fefd7cb9c22cd203189b1af8cd73b158382b7ad8/opentelemetry_exporter_otlp-1.42.1-py3-none-any.whl", hash = "sha256:aedd54545bb0587cd45210abdc8be545af9c01413f3307786e276df1e3c83bee", size = 6733, upload-time = "2026-05-21T16:32:31.261Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-common"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-proto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl", hash = "sha256:f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", size = 17327, upload-time = "2026-05-21T16:32:33.387Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-grpc"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "googleapis-common-protos" },
+ { name = "grpcio" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-common" },
+ { name = "opentelemetry-proto" },
+ { name = "opentelemetry-sdk" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/87/87/ca7fc790dfdbcf4f9e9aab14a39ef1b7508ead13707e283de0b3131478d2/opentelemetry_exporter_otlp_proto_grpc-1.42.1.tar.gz", hash = "sha256:975c4461f167dd8ed8857d68d3b6b25f3d272eab896f6a9470d0f5b90e2faf15", size = 27140, upload-time = "2026-05-21T16:32:56.162Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/89/2b/28ba5b128f47fe8c3bab541000d6feb4b5a9bd26623ca013406f01c0fb60/opentelemetry_exporter_otlp_proto_grpc-1.42.1-py3-none-any.whl", hash = "sha256:0ae1177e2038b18a929b3098215243631ef91136cba26b7e2b12790ceb7e87cc", size = 19617, upload-time = "2026-05-21T16:32:34.278Z" },
+]
+
+[[package]]
+name = "opentelemetry-exporter-otlp-proto-http"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "googleapis-common-protos" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp-proto-common" },
+ { name = "opentelemetry-proto" },
+ { name = "opentelemetry-sdk" },
+ { name = "requests" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl", hash = "sha256:00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", size = 21793, upload-time = "2026-05-21T16:32:35.277Z" },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation"
+version = "0.63b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "packaging" },
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl", hash = "sha256:f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", size = 35577, upload-time = "2026-05-21T16:34:56.818Z" },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-logging"
+version = "0.63b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/cf/119381b1ae446fb07921a452e3a8e1887aa87f9856225f9829958dc20063/opentelemetry_instrumentation_logging-0.63b1.tar.gz", hash = "sha256:aa57d1bcb8931186b5dde565e9c17c572cf02412572d962da5b1a17ee5637d2c", size = 19823, upload-time = "2026-05-21T16:36:37.276Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/71/1ba447311adf33023be14a1a309852c4cf74219f095d0055a54c1824d9ff/opentelemetry_instrumentation_logging-0.63b1-py3-none-any.whl", hash = "sha256:6b3aac8d18bc897468814d5ce4ed00f9d43588c583b4ba2288267e191b96d944", size = 15993, upload-time = "2026-05-21T16:35:35.851Z" },
+]
+
+[[package]]
+name = "opentelemetry-proto"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl", hash = "sha256:dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", size = 71782, upload-time = "2026-05-21T16:32:44.867Z" },
+]
+
+[[package]]
+name = "opentelemetry-sdk"
+version = "1.42.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl", hash = "sha256:083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", size = 170907, upload-time = "2026-05-21T16:32:45.894Z" },
+]
+
+[[package]]
+name = "opentelemetry-semantic-conventions"
+version = "0.63b1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl", hash = "sha256:dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", size = 203713, upload-time = "2026-05-21T16:32:47.016Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
+[[package]]
+name = "pgvector"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" },
+ { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" },
+ { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" },
+ { url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" },
+ { url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" },
+ { url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" },
+ { url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" },
+ { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" },
+ { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" },
+ { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" },
+ { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" },
+ { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" },
+ { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" },
+ { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
+ { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
+ { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+ { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+ { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+ { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+ { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+ { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+ { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+ { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+ { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+ { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+ { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+ { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+ { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+ { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+ { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+ { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+ { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+ { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+ { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+ { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+ { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+ { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+ { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+ { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
+]
+
+[[package]]
+name = "pymysql"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" },
+]
+
+[[package]]
+name = "pyobvector"
+version = "0.2.26"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiomysql" },
+ { name = "numpy" },
+ { name = "pydantic" },
+ { name = "pymysql" },
+ { name = "sqlalchemy" },
+ { name = "sqlglot" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/01/5fe7eb97f683790c6a4c9b6b69b7d6368d789471d46079e28972a3c818dc/pyobvector-0.2.26.tar.gz", hash = "sha256:f393e72bb1af75d7bf61334beecf94097d1cfbaf70aac6c1f564faaf2280df7c", size = 78708, upload-time = "2026-04-15T02:22:27.143Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/79/bcfa5226d53f3f4bc295601fd347c68c3ce367f9a121bf58107280d6c2dd/pyobvector-0.2.26-py3-none-any.whl", hash = "sha256:82bcc81de40c74c4a1e30ff8d85cce1bdc344a5290cd27e252ecb5fc9e929b23", size = 64837, upload-time = "2026-04-15T02:22:25.903Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "rank-bm25"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347, upload-time = "2022-02-16T12:10:52.196Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/21/f691fb2613100a62b3fa91e9988c991e9ca5b89ea31c0d3152a3210344f9/rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae", size = 8584, upload-time = "2022-02-16T12:10:50.626Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2026.5.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/dc/0e/49aee608ad09480e7fd276898c99ec6192985fa331abe4eb3a986094490b/regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270", size = 416074, upload-time = "2026-05-09T23:15:19.37Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/dc/c1f2df4027e82fc54b5a473e4b250f5139faca49a0fbe29a48668d228f34/regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48", size = 489445, upload-time = "2026-05-09T23:12:06.111Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d2/59f01110660081cce9c0bc30ebd0b5ee250dacf658e3248ed92f01e0e8ee/regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8", size = 291271, upload-time = "2026-05-09T23:12:07.731Z" },
+ { url = "https://files.pythonhosted.org/packages/58/b6/14b2c84ff90ddb370c81d27503f4a0fcf071496416f4855f6cc8c5d81c35/regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555", size = 289212, upload-time = "2026-05-09T23:12:09.266Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d0/4db86529117320de0c84afd90e70bb47434625875e34fcef9d8c127c5b16/regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919", size = 792310, upload-time = "2026-05-09T23:12:11.416Z" },
+ { url = "https://files.pythonhosted.org/packages/07/78/fe4800cd322f862ecffd2d553409b20d80650e5ed71b9d178f853d020b82/regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451", size = 861721, upload-time = "2026-05-09T23:12:13.681Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/d0/b3618a895dd8feb897c61bb2954edd265e1767d82a01d53065d5871127a3/regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c", size = 906460, upload-time = "2026-05-09T23:12:15.443Z" },
+ { url = "https://files.pythonhosted.org/packages/33/6f/1481597e859ef19508b345eec4afd1416ed6e6b459c75a64026ef193aecf/regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc", size = 799843, upload-time = "2026-05-09T23:12:16.892Z" },
+ { url = "https://files.pythonhosted.org/packages/73/59/955734c803f59108deccba3597ae440c76b62a652733c0006e6243758420/regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d", size = 773610, upload-time = "2026-05-09T23:12:19.127Z" },
+ { url = "https://files.pythonhosted.org/packages/68/8f/70c04a236d651c81881dac42ef8538bddda6121434509d0a22d9e601503b/regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9", size = 781645, upload-time = "2026-05-09T23:12:20.806Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/96/05c7434d88185e5d27fe54aeb74df86bd77cd79f52f0b4eae54faa8fea70/regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2", size = 854473, upload-time = "2026-05-09T23:12:22.465Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/c1/6e3d8202d981f3117004bf341ee74893ba4ba8a9fbaf4b94615846550a08/regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf", size = 763311, upload-time = "2026-05-09T23:12:24.351Z" },
+ { url = "https://files.pythonhosted.org/packages/93/c7/e7737f1526b3fb32bd4c337fd6c71c3ebb5c8296fc34d11197e0955d2e35/regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611", size = 844593, upload-time = "2026-05-09T23:12:26.341Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/27/0daffb1a535bb39f422c3d200f4ab023c71110ad66a32b366bee708baba0/regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c", size = 789167, upload-time = "2026-05-09T23:12:27.975Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/fc/294fe4fac4f2ed67207b17471815870c1c45b3a489e08e0ac96daea16ef6/regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994", size = 266249, upload-time = "2026-05-09T23:12:30.141Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/b0/8dce459f6245bcf8f6e9f23ac9569f1a0f15c131cc0745e82b43226204cf/regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b", size = 278423, upload-time = "2026-05-09T23:12:31.676Z" },
+ { url = "https://files.pythonhosted.org/packages/db/8d/f9aeff6ad63a3ef720386f2907e6d34a35a510a6e498ebad28b0fb3f6ab6/regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046", size = 270420, upload-time = "2026-05-09T23:12:33.194Z" },
+ { url = "https://files.pythonhosted.org/packages/50/9b/6550044bc44e17c84d312c031c2ec42fbdb6a4ec4e29093be3a172d08772/regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06", size = 490451, upload-time = "2026-05-09T23:12:34.72Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/95/fc7ba4303b5a0f92446a12ee6778ef2c6c799233f5060042a31bf390cfe9/regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6", size = 292112, upload-time = "2026-05-09T23:12:36.285Z" },
+ { url = "https://files.pythonhosted.org/packages/54/4b/ee27938d1b2c443e89a9a10e00d2d19aa5ee300cd3d61140644e93bb083e/regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225", size = 289599, upload-time = "2026-05-09T23:12:38.089Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/dd/ba103dc19614e25f3880800ca67ce093d6e21b325d72b8383c7bf906e9fa/regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0", size = 796732, upload-time = "2026-05-09T23:12:40.062Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e7/f035b4fd858b050b0080bf302968dc0f59ba34e391872d54936758e6844e/regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107", size = 865440, upload-time = "2026-05-09T23:12:42.059Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/51/8cd301ecc899aea28124357f729f4272f44de7806fc7ca02490bfbe253e8/regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309", size = 912329, upload-time = "2026-05-09T23:12:44.373Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/1e/3fbe2fa1e8cebd62f3bb7d3321cff1640aca2e240b51d9bd624aad949260/regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8", size = 801239, upload-time = "2026-05-09T23:12:46.268Z" },
+ { url = "https://files.pythonhosted.org/packages/17/2f/6f6008682bf2cf98040a0d3153a8e557b6ab728d7713d045cee4ce544ab8/regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66", size = 777054, upload-time = "2026-05-09T23:12:48.051Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2b/eee0d20a6842ba04df4b8847a920b57ef56853f14ef85405473e586b605a/regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026", size = 785098, upload-time = "2026-05-09T23:12:49.851Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/98/6fc1e6410feefb92159edaed5041992bfe390e8d26c721865434acbca558/regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962", size = 860095, upload-time = "2026-05-09T23:12:51.666Z" },
+ { url = "https://files.pythonhosted.org/packages/18/a3/bd855e0f2cb1a978ecf6fa6bb69632dd9c3f6ea3b81cde62fde14c9daec7/regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621", size = 765762, upload-time = "2026-05-09T23:12:53.413Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/66/0ae8c092e60b14c79d24f8e0b7f0aea5bfbffdcab00b5483d13404d3c3a5/regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d", size = 852100, upload-time = "2026-05-09T23:12:55.256Z" },
+ { url = "https://files.pythonhosted.org/packages/21/de/8dfde60fc1b21c946a893ba273403b72617edb261370cb1087099a83f088/regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce", size = 789479, upload-time = "2026-05-09T23:12:57.573Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/1c/bdcc98f9a4af4fdd166c74941174619ccff4726d3ce32faa8e9a2ecd38dd/regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e", size = 266699, upload-time = "2026-05-09T23:12:59.14Z" },
+ { url = "https://files.pythonhosted.org/packages/78/87/240d36864f9e48ace85f72e79ced97ceb7f27ce87739a947dcb834b4e6bc/regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e", size = 277783, upload-time = "2026-05-09T23:13:00.789Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b5/7b30f312b0669dff5beebe5b0989dc2d1a312b1a44fab852199c387a5b96/regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070", size = 270513, upload-time = "2026-05-09T23:13:02.426Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/da/797e91ecec6f84135da778ddce78c20e0af5d2a15c26f87a81bc3eadb6db/regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb", size = 490303, upload-time = "2026-05-09T23:13:04.382Z" },
+ { url = "https://files.pythonhosted.org/packages/44/da/bf30abaaa737b58f4a4b8c4a03659e02fd92092c822e0197ed9e0daab917/regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f", size = 292019, upload-time = "2026-05-09T23:13:06.022Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e7/d0eaf5713828417b9e5648cf81fa9bacd4961f6ab98c380c2034f8716e35/regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c", size = 289468, upload-time = "2026-05-09T23:13:08.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9b/b3fdd62b003baa1a9b593cd8c8699c9651c2e80cc21a5c715707983c42d7/regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed", size = 796749, upload-time = "2026-05-09T23:13:10.573Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/30/66ab84588765f5b4b271a9ca09ef7ce2b87caa95176ec3d2ad65d7bc4902/regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020", size = 865445, upload-time = "2026-05-09T23:13:12.523Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/89/f05169e8588aac365f35ffc7f3bc3184f095ef4cfded7cfaa3c7fd5dbd89/regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2", size = 912322, upload-time = "2026-05-09T23:13:14.281Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e1/c93444052cf41581f3c884ab3fb5823daf0992f11cd4388d4275ca610558/regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2", size = 801269, upload-time = "2026-05-09T23:13:16.569Z" },
+ { url = "https://files.pythonhosted.org/packages/50/fe/0cf96b882f540e62e8b9956599798203d599c44cf4c77917ca27400ff69b/regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04", size = 777085, upload-time = "2026-05-09T23:13:18.675Z" },
+ { url = "https://files.pythonhosted.org/packages/23/5c/d78d4924e7fc875557b9e9b768423925fdfaac5549d06da7810019a9bd26/regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c", size = 785153, upload-time = "2026-05-09T23:13:20.525Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e0/5214774090e7b4524dcea3e3c4aa74141d43043f8beb49c1599db1c8b53a/regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f", size = 860164, upload-time = "2026-05-09T23:13:22.263Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/e1/4a57a83350319b1271f0d7a249b8672513ed928b237a741631270de6caea/regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8", size = 765731, upload-time = "2026-05-09T23:13:24.277Z" },
+ { url = "https://files.pythonhosted.org/packages/12/f4/499e74a20c156fc75836ee04a72a38d1a063978f600937f9760467beb1b0/regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6", size = 852062, upload-time = "2026-05-09T23:13:26.125Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/92/7eebc0d0a01e78629695f342ba17e0deaff8fb45e79cc0d7b98287da6e3e/regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21", size = 789577, upload-time = "2026-05-09T23:13:27.814Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a4/018e71f7d2ad48c1ebe6d3ae0026f9b7cb4802fd15c7cc02fdf724355102/regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127", size = 266691, upload-time = "2026-05-09T23:13:29.549Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/1d/861a93719fb9ee7dbfc3761b3797b7a3e112a5d42c6129459d2d741be9b5/regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca", size = 277747, upload-time = "2026-05-09T23:13:31.859Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/c6/0a2436ae4da1ba76e51cb98943c6838a9a721faa40ebe2dce07694ae34e3/regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6", size = 270500, upload-time = "2026-05-09T23:13:33.525Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/e9/d21346f7b60ed58789371358ed66b09d00f832e1bd7c06e55d9da5679882/regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3", size = 494172, upload-time = "2026-05-09T23:13:35.935Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/43/fd1177a2032037c681baecdb3422ee4e1424aec4e4f470ef47793d325274/regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6", size = 293952, upload-time = "2026-05-09T23:13:38.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7d/9fbf919768368d3f8a4f6c692cf2aa61e482b2b81ec6a298ace4cbf02480/regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff", size = 292314, upload-time = "2026-05-09T23:13:40.353Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6c/e41bfeecb589716843e7c4df09ba46ff2a42961457afece19059d85caeef/regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88", size = 811681, upload-time = "2026-05-09T23:13:42.543Z" },
+ { url = "https://files.pythonhosted.org/packages/87/83/a5c1c525fba0aa656e88ad0face0b1829788ef4c2fb6b26df58aa1151b84/regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178", size = 871135, upload-time = "2026-05-09T23:13:44.326Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d4/80882e799e440dd878b0979cbebf8fa4d54624a332c83037c7a701649e3f/regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100", size = 917265, upload-time = "2026-05-09T23:13:47.295Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/ff/8db60211e2286e396aad7dc7725356c502bff0901ea05bd6cdc2e1a042b9/regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e", size = 816311, upload-time = "2026-05-09T23:13:49.885Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/47/742ef579c61730f8d268e5cf1f9ce0e37e2ea041ad0f5644724f2378e463/regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2", size = 785498, upload-time = "2026-05-09T23:13:52.25Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ab/cb0999802dcb0fb95b1ab005e8d4163d8afdd67efc2cb6b6630ac13f8cb1/regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b", size = 801348, upload-time = "2026-05-09T23:13:54.127Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/8ca59a24c55bc34d166eefaf3717bd77772f329fdbf984d86581e0a3571c/regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e", size = 866493, upload-time = "2026-05-09T23:13:56.067Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/3d/30f2ae62cef3278bb5bb821f467277a55fb73f01032cf85997e15e8289a8/regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041", size = 772811, upload-time = "2026-05-09T23:13:57.867Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/ae/7d2089bcd78ad0c0161bc684339df50032acb438a7bd3305e7ddb1193cec/regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0", size = 856584, upload-time = "2026-05-09T23:13:59.679Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/29/92ff47f75990131ea4f24ba17819e5a9d141e10819807e09addd73409af6/regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081", size = 803453, upload-time = "2026-05-09T23:14:01.978Z" },
+ { url = "https://files.pythonhosted.org/packages/04/99/eff29f1037dcab36702c9ee5d6858cf1ce2336ea8ea2987f64245b99ea5e/regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5", size = 269951, upload-time = "2026-05-09T23:14:03.661Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/9d/8870b8981d27b22cda77bb26a5ac7ebfa9c7d9e0dea195a834a82380e748/regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4", size = 281240, upload-time = "2026-05-09T23:14:05.56Z" },
+ { url = "https://files.pythonhosted.org/packages/72/b1/3379415e8f135c13ac551353397cc4fe97b4978f3cac73c5fcbcded548b8/regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de", size = 272383, upload-time = "2026-05-09T23:14:07.843Z" },
+ { url = "https://files.pythonhosted.org/packages/13/3e/9c3cd292d8808b3645a2ce517e200179b6d0e903f176300bd8b542e14de5/regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a", size = 490376, upload-time = "2026-05-09T23:14:09.64Z" },
+ { url = "https://files.pythonhosted.org/packages/60/70/d43ee8a2ca0a8b68d167f21658b85520ac0574617c7f320367c5047f7556/regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4", size = 291964, upload-time = "2026-05-09T23:14:11.424Z" },
+ { url = "https://files.pythonhosted.org/packages/21/91/9d50b433828d8e74196904e168a43abf1e6e88b2a15d47ed742456720c37/regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c", size = 289682, upload-time = "2026-05-09T23:14:13.123Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/b835e3cafbb9d977736912436259ff551d60919f7d7b3d37d46659c63564/regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9", size = 796996, upload-time = "2026-05-09T23:14:14.923Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/a6/9f992d00019166b9de01c546dd4549bc679f2a68df11b877740b0760b7c2/regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af", size = 866089, upload-time = "2026-05-09T23:14:17.757Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/08/4d32af657e049b19cb62b02e46e38fe1518797bfb2203ee93a510b21b0dc/regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0", size = 911530, upload-time = "2026-05-09T23:14:20.353Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/27/2af43dd1dc201d1fecefda64a45f4ad0995855b92724f795a777b402ee69/regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4", size = 800643, upload-time = "2026-05-09T23:14:22.265Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/dd/23a249047013b5321d4a60c4d2437462086f601b061776a525e5fba2a59f/regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf", size = 777223, upload-time = "2026-05-09T23:14:24.179Z" },
+ { url = "https://files.pythonhosted.org/packages/94/6a/e85ed9538cd19586d0465076a4578a12e093ce776d15f3f8ce92733a8dd6/regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346", size = 785760, upload-time = "2026-05-09T23:14:26.065Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/c4/f25473209438638e947c55f9156fd8f236f74169229028cc99116380868e/regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676", size = 860891, upload-time = "2026-05-09T23:14:28.17Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/f7/f4f86e3c74419c37370e91f150ae0c2ef7d34b2e0e4cdd5da046a02e4022/regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14", size = 765891, upload-time = "2026-05-09T23:14:30.06Z" },
+ { url = "https://files.pythonhosted.org/packages/26/70/704d8e13765939146b1cd0ef4e2feb71d7929727d2290f026eed10095955/regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd", size = 851380, upload-time = "2026-05-09T23:14:32.123Z" },
+ { url = "https://files.pythonhosted.org/packages/26/29/1a13582a8460038edc38e49f64ceb0dd7c60f5caba77571f4bf6601965d9/regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e", size = 789350, upload-time = "2026-05-09T23:14:34.799Z" },
+ { url = "https://files.pythonhosted.org/packages/73/56/3dcafe34fc72e271d62ad9a291801e88a1457bb251c132f15fcc2e5aad1a/regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad", size = 272130, upload-time = "2026-05-09T23:14:36.729Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9c/02eebf0be95efe416c664db7fb8b6b05b7a0b06a7544f2884f2558b0526f/regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763", size = 280999, upload-time = "2026-05-09T23:14:39.126Z" },
+ { url = "https://files.pythonhosted.org/packages/70/5a/1dd1abee76cb7a846a0bcf42fdc87e5720c3c33c24f3e37814310a513d9f/regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372", size = 273500, upload-time = "2026-05-09T23:14:41.059Z" },
+ { url = "https://files.pythonhosted.org/packages/86/c1/c5f619b0057a7965cb78ec559c1d7a45ce8c99a35bea95483d64959a93d9/regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499", size = 494269, upload-time = "2026-05-09T23:14:42.869Z" },
+ { url = "https://files.pythonhosted.org/packages/05/2c/5d01f1aee33de4bbe60c8452945bfc8477ca7c5ae4450f6bfe711036cb36/regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1", size = 293954, upload-time = "2026-05-09T23:14:44.822Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/fe/e8988b2ae2108c6ef71bd4aa8d87fbe257976dd0810e826cd75f701c68b6/regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d", size = 292405, upload-time = "2026-05-09T23:14:47.211Z" },
+ { url = "https://files.pythonhosted.org/packages/79/34/d2b0937faa7859263f7f0a3c6b103a1296306be6952dc173d0154e9a2f49/regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c", size = 811855, upload-time = "2026-05-09T23:14:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/80/fe/daf53a47457a8486db66c66c01ceb9c2303eecee3f87197f1e77eb1a736d/regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5", size = 871189, upload-time = "2026-05-09T23:14:51.555Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/75/058fc4470cbfbf57d800aff1a0022b929a3f9fa553ee10a0cdf2070eb31f/regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20", size = 917485, upload-time = "2026-05-09T23:14:53.633Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e7/179cfda3a28bc843b5c6cfe7f79f23489c791ed95f151083803660878432/regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0", size = 816369, upload-time = "2026-05-09T23:14:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/41/90/6f0cc422071688266d344fca8462d787cba0a2c144acb25721f9a61ec265/regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d", size = 785869, upload-time = "2026-05-09T23:14:58.602Z" },
+ { url = "https://files.pythonhosted.org/packages/02/67/a31f1760f09c27b251ef39e9beb541f462cf977381d067faa764c2c0e393/regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b", size = 801427, upload-time = "2026-05-09T23:15:00.642Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/c4/1a80654597b6bc1e1ea0494824c31200e8a956abe290afae9b19a166a148/regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a", size = 866482, upload-time = "2026-05-09T23:15:03.384Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/960724e06482c08466ff5611e242e86f80062949cdf6b4b9cc317b9dd93d/regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415", size = 773022, upload-time = "2026-05-09T23:15:05.625Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a8/a9979c3e7918280e93159ebcab5ef1a65116dd4f3bd6091be0eae4a126e8/regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2", size = 856642, upload-time = "2026-05-09T23:15:07.966Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d4/a9b732f2f0072c0ab12227483abb24fffcb9f73f8a2b203df0a6d0434735/regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41", size = 803552, upload-time = "2026-05-09T23:15:10.215Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/fe/1b3113817447a1d4155e4ac76d2e072f42c0bcba2f43fa8a0e756ea2cd91/regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58", size = 275746, upload-time = "2026-05-09T23:15:12.609Z" },
+ { url = "https://files.pythonhosted.org/packages/92/73/93d42045302636c91f2e5ef588b65b84b01428f28ec77de256b1dfdfbe5c/regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77", size = 285685, upload-time = "2026-05-09T23:15:15.086Z" },
+ { url = "https://files.pythonhosted.org/packages/da/80/35b4c33c804a165a7f55289afda3ea9e3eb6d15800341a2d66455c0f1f30/regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa", size = 275713, upload-time = "2026-05-09T23:15:16.98Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
+]
+
+[[package]]
+name = "rich"
+version = "15.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.15.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
+ { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
+ { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
+ { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
+ { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
+ { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
+]
+
+[[package]]
+name = "safetensors"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
+ { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
+ { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "joblib" },
+ { name = "narwhals" },
+ { name = "numpy" },
+ { name = "scipy" },
+ { name = "threadpoolctl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" },
+ { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" },
+ { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" },
+ { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" },
+ { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7d/c9a35cf59b20a86fec24d306f1547b78dec194b08d367ce2a3e4854169d9/scikit_learn-1.9.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9656acd4e93f74e0b66c8a36c88830a99252dfa900044d36bc2212ae89a47162", size = 8713289, upload-time = "2026-06-02T11:53:58.788Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a7/552a7821597c632b907f7bfe8f36f9f572777af8ef8a48353041cf8e091a/scikit_learn-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:24360002ae845e7866522b0a5bbf690802e7bc388cac8663502e78aa98598aa2", size = 8245141, upload-time = "2026-06-02T11:54:01.694Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/79/f4a0c4fe9711154cddabf913471153af79056382ddc612cfe5ee0ff4b72e/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5162ad10a418c8a282dde04c9aa06965de3e9a65f33c1440c0ae69bb1a09d913", size = 8847671, upload-time = "2026-06-02T11:54:04.448Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/af/4d72d9e475ac83719160c662619e4bf7b95c19507cd582e7d0167a3c3dae/scikit_learn-1.9.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fea2cc5677ab49d6f5bade978c866da44957b712d92e9635e8b4f723013c3cb", size = 9118104, upload-time = "2026-06-02T11:54:07.205Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/d5/6a58eea2cb9abbb9b3f2bb8b2cfb3243d1152d69f442d256c7af71304769/scikit_learn-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:64fa347efc1c839c487433e40c5144d38c336e8a2b59c81aa8660373945c2673", size = 8290674, upload-time = "2026-06-02T11:54:10.087Z" },
+ { url = "https://files.pythonhosted.org/packages/65/5b/d4c879cf358f1187141cf90ced473f087183489090244f50c124a2ee478b/scikit_learn-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:1b944b6db288f6b926e3650026ddafb988929de95d11fc2cc5fa117773c9ba42", size = 7978807, upload-time = "2026-06-02T11:54:12.769Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/43/bfae3121ec67ae09150d453c442c7c1cc166e9aefe056e6ab3b7728a5cfc/scikit_learn-1.9.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4ccacf04ca5f4b492158a5f28afe0ace43f81b2571e4b9a66d34848b46128949", size = 9031941, upload-time = "2026-06-02T11:54:15.436Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b0/20a4546eb17f3b25d3c66df15810411c14ed5065bcfab50b53c96fb627b2/scikit_learn-1.9.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ee1a8db2c18c08e34c7412d4b10be1cac214cd4ea7dc9715a6a327eb49a37c96", size = 8613528, upload-time = "2026-06-02T11:54:18.842Z" },
+ { url = "https://files.pythonhosted.org/packages/18/3c/e440e039bb82cd19004edaaad00acbde0fb9b461083c3ecf37941c557312/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:147e9329ef0e39f75d4cffa02b2aa48d827832684926cd5210d9a2cb5c57246b", size = 8855050, upload-time = "2026-06-02T11:54:21.699Z" },
+ { url = "https://files.pythonhosted.org/packages/43/26/b341b8dab5998da6270a3a42c2152c578501354d36f944b5856757035ef8/scikit_learn-1.9.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bad8f8b9950321b54c965fdcbac6c6c55e79e16646b49977bcf3668d3870a1a", size = 9097190, upload-time = "2026-06-02T11:54:24.454Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/de/b650b4d69b84468cfa2e28a3ff7b8103743029e6446ce1a97fe060ef688c/scikit_learn-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:78fc56eafd4edb9575d2d8950d1dd152061abb573341a1cb7e099fc40f6c6666", size = 8963204, upload-time = "2026-06-02T11:54:27.428Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f3/ff83d76d7418112e5a61326443cdda87be3545dd8d6599c95b2481a4419e/scikit_learn-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:051075bda8b7aab87b1906ab3d4740a1e1224a19d7b3781a576736edc94e76aa", size = 8222661, upload-time = "2026-06-02T11:54:30.192Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" },
+ { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" },
+ { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" },
+ { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" },
+ { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" },
+ { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" },
+ { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" },
+ { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
+ { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
+ { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
+ { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
+ { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
+ { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
+ { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
+ { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
+ { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
+ { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
+]
+
+[[package]]
+name = "sentence-transformers"
+version = "5.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "scikit-learn" },
+ { name = "scipy" },
+ { name = "torch" },
+ { name = "tqdm" },
+ { name = "transformers" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/d4/7ef93157485e978c016f49da05363c1e4e7237beb5343b64b5631101f0f1/sentence_transformers-5.5.1.tar.gz", hash = "sha256:02b7740dfc60bdbbcb6061625f5d97a5c1a4e2d3baac5f9391b912bb5eae2290", size = 445161, upload-time = "2026-05-20T07:37:44.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/03/ee99a6b030e7a2e056547729f8a4709dd93e13d9c6f07590f74c395c4017/sentence_transformers-5.5.1-py3-none-any.whl", hash = "sha256:4fe11d433badc5282d32f7fc08bc714216b7a5aca426f9df77a45a554756deb7", size = 588887, upload-time = "2026-05-20T07:37:43.004Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "81.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[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, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[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, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "sqlalchemy"
+version = "2.0.50"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/da/6fbf010c8ebb347679d0d100b22fe9ba5e13fd04046c5df7280d2f0bf706/sqlalchemy-2.0.50.tar.gz", hash = "sha256:af5607d11ef90fd6a5c0549fe0045dce1663d427426bcfb506dcb5346a85a3b9", size = 9907424, upload-time = "2026-05-24T19:20:04.018Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/5d/3172686af1770e4de2805f919a51441085f589ddadf3dd76ec582f84f497/sqlalchemy-2.0.50-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa6e403663a9c43c8fef7ce4bdb4cf48bcd8d352e91deda2a99f963270bd508", size = 2161366, upload-time = "2026-05-24T20:00:02.061Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/90/e98dedea3c3e663a17afcd003a34ba45efdac2cea3b6f2e4585e2b1e2537/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51b637a84f9fa35ae1f9017e786cb142974a25305085e1b378b3647a67f65ad3", size = 3318926, upload-time = "2026-05-24T20:07:42.369Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/4f/501308c2babb62c11753ecb4ee88ba9eef019419a4d6cbf7cb13e2bad353/sqlalchemy-2.0.50-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dab927761d9108550f0cf8e66ff21af56f907a0ce0a689793db615e2b55f62c", size = 3319199, upload-time = "2026-05-24T20:14:28.551Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/39/d88996c5e03ed6248c3a788d20f0b8d8b376b9f8a495e4bab9df7c72d2f8/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:545eae198d37bcf837a10ede3684e2af32458d6f35c597c35c2de7502dc38fc4", size = 3270301, upload-time = "2026-05-24T20:07:44.917Z" },
+ { url = "https://files.pythonhosted.org/packages/42/1b/1ae0e65161b51cc43e5ca75430ef79d80e23b5042d645586c2c342c3b92e/sqlalchemy-2.0.50-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fec460e18cdbb4c7773531122ce9a27e96c6ca17af3933941d94da475ad2c86", size = 3293465, upload-time = "2026-05-24T20:14:30.501Z" },
+ { url = "https://files.pythonhosted.org/packages/83/29/17c0003f2c0dfa6d1b97672475707e3ec5980db09defd7fa20beb6833bbd/sqlalchemy-2.0.50-cp311-cp311-win32.whl", hash = "sha256:e6e814658818fd165e749e3d8490ef16cc7f379a118c37ada8b0589ffbaaac22", size = 2120694, upload-time = "2026-05-24T20:08:09.237Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/18/280d00654cc19d1fccf236fa5070f6dd04b84dde6f1b2e637bde0ff340a7/sqlalchemy-2.0.50-cp311-cp311-win_amd64.whl", hash = "sha256:1c5f858fe79c9f5d8fda065c06186356acb7f8df3cd52dbd5ee3f200e4b144f5", size = 2145315, upload-time = "2026-05-24T20:08:10.952Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b0/a9d19b43f38f878b1278bca5b00b909f7540d41494396dd2561f9ad0956d/sqlalchemy-2.0.50-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae23d8b9d344d30d0a92f06d45825024a5790f1c1dd4cf452636a50d3e58cb", size = 2159807, upload-time = "2026-05-24T19:27:53.086Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/2c/191dd58a248fd2cfd4780fa82c375c505e4ad98c8b522fa69ec492130d77/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47b71b933e7b4ebad407c8fdfd70d2c4f08b78b3238bb30eebdd6eb32ca51b89", size = 3343358, upload-time = "2026-05-24T20:09:29.279Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/2b/514fce8a7df81cf5bad7ff7865de7ac0c5776a38cc043475c4703eb7fe8b/sqlalchemy-2.0.50-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:110fdac56ace278949f00de805edacbd6141e382d992f9ba28238b3a0827a600", size = 3357994, upload-time = "2026-05-24T20:17:13.495Z" },
+ { url = "https://files.pythonhosted.org/packages/35/a6/a0e283f5494f92b0d77e319ff77e437b1ffe4a051ba67c81d53234825475/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5e4ac70e9e757f6b3e87c0491ff034442ecd8dfd36d041a50564c322dafc0e", size = 3289399, upload-time = "2026-05-24T20:09:32.239Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/96/1b07325ba71752d6a028b77d07bed1483ad545f794e8b1dc89b3ba3b3c68/sqlalchemy-2.0.50-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724f3dcbe53dd0151e3cb5e7ec4ba4c620bede579caacd16275dc35ce06e8615", size = 3321216, upload-time = "2026-05-24T20:17:15.581Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/8e/bad6ed253e8a99edfc99af02f7173ec48a1d3ed1b9b35a1b8bc1700900cc/sqlalchemy-2.0.50-cp312-cp312-win32.whl", hash = "sha256:1208050441471d003b7c8cb4054fb084f185cf35ac3f0ea270803865bca9939a", size = 2119194, upload-time = "2026-05-24T19:50:04.943Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2d/314a6690dda4b9cfc571eab1a63cf6fe6e1470aa3759ccda6aa016ee0f5a/sqlalchemy-2.0.50-cp312-cp312-win_amd64.whl", hash = "sha256:9d1af51558029a156a70986b7df88f042b3d158d7c8d8fb5072912d4b32d89c7", size = 2146186, upload-time = "2026-05-24T19:50:06.74Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/c4/c42356b527296e9862f67990efce31ef78b4cf69cd3f80873a528a060320/sqlalchemy-2.0.50-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:06a9210bdc5f4298cff0781087e2ff45683922252dacc452846373a58761f093", size = 2156697, upload-time = "2026-05-24T19:27:54.764Z" },
+ { url = "https://files.pythonhosted.org/packages/60/a1/b1a70e3c4365ac7fe9e347f3710f19b562c866fb96d45e3c891588789a7b/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b53784972ade4f8174b9aa661f31a06f8a936d2cfdd602913ff3c6dd40ae873", size = 3284260, upload-time = "2026-05-24T20:09:34.195Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/4a/f3ac3caa19f263d57b0a47f8c91bbf56583dc2d3fc63acfbf644abb24fe0/sqlalchemy-2.0.50-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31648fa14460537e768a7303b078e4344d208e0d23e06867c1f376a227ed82db", size = 3302280, upload-time = "2026-05-24T20:17:17.825Z" },
+ { url = "https://files.pythonhosted.org/packages/66/55/ccada3e3d62254587819749a0bc69f41173eb48a6e385d10e66d32a9c88e/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03f4323c980ad0e918cc9e5369b015f759f4e534db5bbaf4dc36832c10d05064", size = 3231580, upload-time = "2026-05-24T20:09:36.406Z" },
+ { url = "https://files.pythonhosted.org/packages/05/f6/6809349130a2de0e109e7f00fd7d431da9565b9b2868b32ee684754f672b/sqlalchemy-2.0.50-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b9dcc43afef8ac157cd92fce96985d6b8b0cfbd3df4d666f66b4d55a75d202f", size = 3269375, upload-time = "2026-05-24T20:17:20.34Z" },
+ { url = "https://files.pythonhosted.org/packages/48/84/278a811ef4e07be9c89dc5cdd7be833268509a66a68c4897cf585e67428f/sqlalchemy-2.0.50-cp313-cp313-win32.whl", hash = "sha256:60922d6599065ddca2c6f376b9aa2f41a6b85a271725e0909490bbc50b1998a5", size = 2117229, upload-time = "2026-05-24T19:50:08.215Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/1c/067cc6187ed32d2ec222fe6d2643acc1659a6d0659f8a7cbc5ad3ae83280/sqlalchemy-2.0.50-cp313-cp313-win_amd64.whl", hash = "sha256:287086e67275a212c4582d166a6fb03a65ccc5551d80866270ce0dd9f34eccd3", size = 2143126, upload-time = "2026-05-24T19:50:09.691Z" },
+ { url = "https://files.pythonhosted.org/packages/df/32/10ac51b4be7cdecd7e93d069251c86dfbf70b7adbd7c67b48ccea6c49e1c/sqlalchemy-2.0.50-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c966932507a4d7d0a37314927dbfcd89720e3f37d2a1e3352e7ae7939fa8e8a0", size = 2158519, upload-time = "2026-05-24T19:27:56.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/76/e703d2f7681d7d66c4c891af3f07c7ccf4c76ad7f18351de035b5eda007a/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:faffef4bcc20a1892e65e155293d99d60855bbbc79250ab712819cfd56a8e6bb", size = 3282063, upload-time = "2026-05-24T20:09:38.57Z" },
+ { url = "https://files.pythonhosted.org/packages/31/26/ef168b184a25701f9995e8fb7e503fafd7a99c1c77cda1bc1a26ea2ed486/sqlalchemy-2.0.50-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c206aec519a2e7bd08abbfb33436e325fd22c632d9c21a9047e376ce241646e", size = 3287069, upload-time = "2026-05-24T20:17:21.942Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/15/765acc2bc693bccc43ca4a95d5b69750da8aaf6db1b5c616536e087f8920/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bef4ac756363227ef6402a75fee025a4bc690f92328e825868939b3b3a446a6d", size = 3230453, upload-time = "2026-05-24T20:09:40.398Z" },
+ { url = "https://files.pythonhosted.org/packages/63/61/08e03c3adbf5db0087a0b6816746fec8f3032fb2f7fc899a9bb9b2a48ce4/sqlalchemy-2.0.50-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96fbee6b19c19cd1556c8bf9419447cf2ec149ffcab7ab64348c23e54ef8547f", size = 3252413, upload-time = "2026-05-24T20:17:24.067Z" },
+ { url = "https://files.pythonhosted.org/packages/03/0c/370a1f2db38436c615e10134c8a37de3688e74084792380695f3f5083860/sqlalchemy-2.0.50-cp314-cp314-win32.whl", hash = "sha256:8f00e3eb43ba30eb1b238ee03a8a62309486d1321eda3328bb611e0340033ad8", size = 2120063, upload-time = "2026-05-24T19:50:11.08Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/a0/fe92bb9817863bc13ba093bda931979a26cc2ca69f8e8f26d07add3d7c6f/sqlalchemy-2.0.50-cp314-cp314-win_amd64.whl", hash = "sha256:15708c613cd5005b7dffe1f66ee6a63ee8f5e46799f71c70ebad74178c676a39", size = 2145830, upload-time = "2026-05-24T19:50:12.452Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/ff/e5640a98a0b2f491eb8fde10fb6c773621a2e44340de231fafcc9370f4a9/sqlalchemy-2.0.50-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3699dac4be410e97049a1658e9480da9cde956594aa0f3aebc60b88f21c5ba70", size = 2178435, upload-time = "2026-05-24T19:42:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/85/337116e186f1236375b5fb70c21cfac98e8e8ab0d3a47be838dc47a59e08/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96233858e3df43932ac11589e22520da6e8aeb624b03fedfeebb0e8ea213086", size = 3566059, upload-time = "2026-05-24T20:01:20.848Z" },
+ { url = "https://files.pythonhosted.org/packages/96/34/bb0e190e161c3c2c24314a65add57218be14a4a9486886b7f5047c1ff7c8/sqlalchemy-2.0.50-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c4e70c46fad30c3bcc6a4708bc0130a3173e11a5b25f0ea4a9d8911b450f1f52", size = 3535366, upload-time = "2026-05-24T20:03:56.768Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/a7f759f97e4fd499c5d4e4488c760d5a7fbecf3028b465a04274fcd52384/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1918a3cf564d16d95bca7301005f41ab2ad50b07cd3b9da50d3ed986db148d6a", size = 3474879, upload-time = "2026-05-24T20:01:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/d9/2907ea38eb60687d297bf9c39e5ee58053c87b57fe8a9cae97090cecbf10/sqlalchemy-2.0.50-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b00098cdbdbd38c7be3d568b0c9c3122b8c0ec62b911b57cd5e6e0254d60a76d", size = 3486117, upload-time = "2026-05-24T20:03:59.052Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e3/5aa06f167559f8c0bdae487e297d23ba548150ab016a3418265d617a4985/sqlalchemy-2.0.50-cp314-cp314t-win32.whl", hash = "sha256:1fbd55a969d7ac44a98e3dec75016074f809fa08f871585ace58dde110d1bf3e", size = 2150823, upload-time = "2026-05-24T20:08:58.644Z" },
+ { url = "https://files.pythonhosted.org/packages/65/9b/112fb8f977582d7489d036e409e3723948bcf5320b3ac465f3c481bbe8f9/sqlalchemy-2.0.50-cp314-cp314t-win_amd64.whl", hash = "sha256:c5c3cdb753a9004183e1ccb634b41611654c989e61bc68617ce878e46d6f1e51", size = 2185794, upload-time = "2026-05-24T20:09:00.319Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/10/f7220e9b784d295d241c86ed99aeb537f92afcd469a64861f2717e9bb077/sqlalchemy-2.0.50-py3-none-any.whl", hash = "sha256:92064363517a3ff8212b5a93b8c62876579d8dfd1ca5b561335f30152d884fa9", size = 1943861, upload-time = "2026-05-24T19:59:01.119Z" },
+]
+
+[[package]]
+name = "sqlglot"
+version = "30.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/64/89299aefc6ebdf4fc899f5dc14c7fcb7eb9da9290a2b4d615ae7ab884b17/sqlglot-30.8.0.tar.gz", hash = "sha256:1c5f93fb742dd9aaa75eee6bb33a637794a858b9a86375fac23a2dc0f7bc127e", size = 5869750, upload-time = "2026-05-13T09:04:38.923Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl", hash = "sha256:af903378c331d5b72277a1b41118f07bc3e50cf4478e2d47eed12c96ee6a22a4", size = 687831, upload-time = "2026-05-13T09:04:36.336Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/44/ec35f1b6e83094b997da438a02c8c9b0ade2b1e84cfc48bd4656780760a6/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6", size = 2701854, upload-time = "2026-05-31T01:07:51.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" },
+]
+
+[[package]]
+name = "strictyaml"
+version = "1.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/08/efd28d49162ce89c2ad61a88bd80e11fb77bc9f6c145402589112d38f8af/strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407", size = 115206, upload-time = "2023-03-10T12:50:27.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/7c/a81ef5ef10978dd073a854e0fa93b5d8021d0594b639cc8f6453c3c78a1d/strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7", size = 123917, upload-time = "2023-03-10T12:50:17.242Z" },
+]
+
+[[package]]
+name = "structured-context-language"
+source = { editable = "." }
+dependencies = [
+ { name = "fastapi" },
+ { name = "numpy" },
+ { name = "openai" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-exporter-otlp" },
+ { name = "opentelemetry-instrumentation-logging" },
+ { name = "opentelemetry-sdk" },
+ { name = "pyyaml" },
+ { name = "rank-bm25" },
+ { name = "strictyaml" },
+ { name = "uvicorn" },
+ { name = "watchdog" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+local = [
+ { name = "sentence-transformers" },
+]
+oceanbase = [
+ { name = "pyobvector" },
+ { name = "sqlalchemy" },
+]
+postgres = [
+ { name = "pgvector" },
+ { name = "psycopg2-binary" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "fastapi", specifier = ">=0.135.3" },
+ { name = "mypy", marker = "extra == 'dev'" },
+ { name = "numpy", specifier = ">=2.3.5" },
+ { name = "openai", specifier = ">=2.14.0" },
+ { name = "opentelemetry-api", specifier = ">=1.39.1" },
+ { name = "opentelemetry-exporter-otlp", specifier = ">=1.39.1" },
+ { name = "opentelemetry-instrumentation-logging", specifier = ">=0.60b1" },
+ { name = "opentelemetry-sdk", specifier = ">=1.39.1" },
+ { name = "pgvector", marker = "extra == 'postgres'", specifier = ">=0.4.2" },
+ { name = "psycopg2-binary", marker = "extra == 'postgres'", specifier = ">=2.9.11" },
+ { name = "pyobvector", marker = "extra == 'oceanbase'" },
+ { name = "pytest", marker = "extra == 'dev'" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'" },
+ { name = "pytest-cov", marker = "extra == 'dev'" },
+ { name = "pyyaml", specifier = ">=6.0.3" },
+ { name = "rank-bm25", specifier = ">=0.2.2" },
+ { name = "ruff", marker = "extra == 'dev'" },
+ { name = "sentence-transformers", marker = "extra == 'local'" },
+ { name = "sqlalchemy", marker = "extra == 'oceanbase'" },
+ { name = "strictyaml", specifier = ">=1.7.3" },
+ { name = "uvicorn", specifier = ">=0.44.0" },
+ { name = "watchdog", specifier = ">=6.0.0" },
+]
+provides-extras = ["oceanbase", "postgres", "local", "dev"]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.22.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
+ { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
+ { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
+ { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
+ { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
+ { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
+ { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
+ { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
+ { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
+ { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
+ { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
+ { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
+ { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
+ { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
+ { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
+ { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
+ { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
+ { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
+ { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
+ { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
+ { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
+ { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
+ { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
+ { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-bindings", marker = "sys_platform == 'linux'" },
+ { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cublas", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "triton", marker = "sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/62/131124fb95df03811b8260d1d43dcc5ee85ea1a344b964613d7efe77fb08/torch-2.12.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:10802fd383bbfed646212e765a72c37d2185205d4f26eb197a254e8ac7ddcb25", size = 87990344, upload-time = "2026-05-13T14:55:42.154Z" },
+ { url = "https://files.pythonhosted.org/packages/12/9c/dda0dbd547dc549839824135f223792fd0e725f28ed0715dda366b7acaa2/torch-2.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c12592630aef72feaf18bd3f197ef587bbfa21131b31c38b23ab2e55fce92e36", size = 426362932, upload-time = "2026-05-13T14:54:15.295Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/d2/a7dd5a3f9bdaa7842124e8e2359202b317c48d47d2fc5816fafdf2049adb/torch-2.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:415c1b8d0412f67551c8e89a2daca0fb3e56694af0281ba155eaa9da481f58b4", size = 532170085, upload-time = "2026-05-13T14:55:20.788Z" },
+ { url = "https://files.pythonhosted.org/packages/12/1b/a61ce2004f9ab0ea8964a6e6168133a127795667639e2ff4f8f2bdb16a65/torch-2.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd37188ea325042cb1f6cafa56822b11ada2520c04791a52629b0af25bdfbfd9", size = 122953128, upload-time = "2026-05-13T14:54:52.744Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/bb/285d643f254731294c9b595a007eac39db4600a98682d7bca688f42ca164/torch-2.12.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b41339df93d491435e790ff8bcbae1c0ce777175889bfd1281d119862793e6a2", size = 88010197, upload-time = "2026-05-13T14:55:35.414Z" },
+ { url = "https://files.pythonhosted.org/packages/79/81/76debf1db1343bd929bbb5d74c89fb437c2ed88eb144712557e7bd3eea45/torch-2.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8fbef9f108a863e7722a73740998967e3b074742a834fc5be3a535a2befa7057", size = 426376751, upload-time = "2026-05-13T14:55:03.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/f0/80026028b603c4650ff270fc3785bdef4bd6738765a9cc5a0f5a637d65a2/torch-2.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4b4f64c2c2b11f7510d93dd6412b87025ff6eddd6bb61c3b5a3d892ea20c4756", size = 532261691, upload-time = "2026-05-13T14:52:54.453Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c2/64b06cbb7830fb3cd9be13e1158b31a3f36b68e6a209105ee3c9d9480be0/torch-2.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b958caff4a14d3a3b0b2dfc6a378f64dda9728a9dad28c08a0db9ce4dafb549", size = 122988114, upload-time = "2026-05-13T14:54:42.153Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ca/01896c80ba921676aa45886b2c5b8d774912de2a1f719de48169c6f755cd/torch-2.12.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:90dd587a5f61bfe1307148b581e2084fc5bc4a06e2b90a20e9a36b81087ff16b", size = 88009511, upload-time = "2026-05-13T14:54:47.411Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/04/52bdaf4787eab6ac7d7f5851dff934e4def0bc8ead9c8fd2b69b3e529699/torch-2.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:864392c73b7654f4d2b3ae712f607937d0dbb1101c4555fbb41848106b297f39", size = 426383231, upload-time = "2026-05-13T14:53:32.129Z" },
+ { url = "https://files.pythonhosted.org/packages/49/8a/94bdecd13f5aaa90d45920b89789d9fe7c6f4af8c3cdd7ce01fcb59908fc/torch-2.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5d6b560dfa7d56291c07d615c3bb73e8d9943d9b6d87f76cd0d9d570c4797fa6", size = 532269288, upload-time = "2026-05-13T14:53:49.423Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/2f/bdbaaa267de519ef1b73054bf590d8c93c37a266c9a4e24a01bd38b6918f/torch-2.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:3fee918902090ade827643e758e98363278815de583c75d111fdd665ebffde9f", size = 122987706, upload-time = "2026-05-13T14:54:00.335Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ad/e95e822f3538171e22640a7fbe839a1fdb666600bf6487025de2ff03b11a/torch-2.12.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:10ee1448a9f304d3b987eb4656f664ba6e4d7b410ca7a5a7c642199777a2cf88", size = 88319556, upload-time = "2026-05-13T14:54:05.574Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/07/055d06d985b445d67422d25b033c11cf55bbb81785d4c4e68e28bca5820e/torch-2.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af68dbf403439cae9ceaeaaf92f8352b460787dcd27b92aa05c40dd4a19c0f1e", size = 426397656, upload-time = "2026-05-13T14:52:38.84Z" },
+ { url = "https://files.pythonhosted.org/packages/43/94/b0b4fdc3014122e0a7302fb90086d352aa48f2576f0b252561ebb38c01a8/torch-2.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a6a2eebb237d3b1d9ad3b378e86d9b9e0782afdea8b1e0eba6a13646b9b49c07", size = 532183124, upload-time = "2026-05-13T14:53:16.178Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/c8/052405e6ad05d3237bfe5a4df78f917773956f8e17813a2d44c059068b74/torch-2.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2140e373e9a51a3e22ef62e8d14366d0b470d18f0adf19fdc757368077133a34", size = 123232462, upload-time = "2026-05-13T14:52:27.26Z" },
+ { url = "https://files.pythonhosted.org/packages/67/dc/ac069f8d6e8be701535921141055293b0d4819d3d7f224a4612cf157c7f9/torch-2.12.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7dfae4a519197dfa050e98d8e36378a0fb5899625a875c2b54445005a2e404e", size = 88027282, upload-time = "2026-05-13T14:53:05.258Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c3/1c1eb00e34555b536dddf792676026a988d710ed36981aa00499b36b0620/torch-2.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:891c769072637c74e9a5a77a3bc782894696d8ffec83b938df8536dee7f0ba78", size = 426386961, upload-time = "2026-05-13T14:51:28.406Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/d4/7e730dba0c7032a4154dc9056b76cf9625515e030e269cfbf8098fcfee7d/torch-2.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e2ad3eb85d39c3cab62dfa93ed5a73516e6a53c6713cb97d004004fe089f0f1f", size = 532272265, upload-time = "2026-05-13T14:51:59.308Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/b4/92c80d1bbfee1c0036c06d1d2155a3065bd2423134c83bf8a47e65cd6b9b/torch-2.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:c66696857e987efb8bc1777a37357ec4f60ab5e8af6250b83d6034437fa2d8f3", size = 122987138, upload-time = "2026-05-13T14:51:45.942Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/78/2e12b37ce50a19a037d7bc62d652a5a8f27385a7b05859d6bc9204f20cfe/torch-2.12.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b4556715c8572758625d62b6e0ae3b1f76c440221913a6fb5e100f321fb4fb02", size = 88320100, upload-time = "2026-05-13T14:51:39.955Z" },
+ { url = "https://files.pythonhosted.org/packages/56/5e/83c450ec7b0bb40a7b74611c1b5440f9260e33c54c90d556fd4a1f0fd955/torch-2.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a43ac605a5e13116c72b64c359644cce0229f213dde48d2ae0ae5eb5becf7feb", size = 426391871, upload-time = "2026-05-13T14:52:14.989Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/e9/1a0b575d98d0afedd8f157d23fa3d2759421483660448e60d0a4b10b6daa/torch-2.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a7512adfdd7f6732e40de1c620831e3c75b39b98cef60b11d0c5f0a76473ec5", size = 532192241, upload-time = "2026-05-13T14:51:07.795Z" },
+ { url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
+]
+
+[[package]]
+name = "transformers"
+version = "5.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+ { name = "typer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/58/7f843608f2e8421f86bb97060b54649be6239ec612b82bf9d41e65c26c00/transformers-5.9.0.tar.gz", hash = "sha256:25997cb8fa6053533171634b6162d7df54346530ec2aa9b42bb834e63668c842", size = 8642240, upload-time = "2026-05-20T14:50:49.278Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/ca/2eaa5359f2ccb8c2e1656bc26305ad0cf438aa392ce4b29ae67a315c186e/transformers-5.9.0-py3-none-any.whl", hash = "sha256:1d19509bcff7028ebc6b277d71caa712e8353778463d38764237d14b42b52788", size = 10787648, upload-time = "2026-05-20T14:50:45.337Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.7.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/c1/5d842314bb6c78442cc60437928781701c6050b8d479bc2a1aed691d37ca/triton-3.7.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9e71fc392675fac364e0ecf4ef3f76f85b7f5433a16f4c3c5fe5f05a52c85fe", size = 188480277, upload-time = "2026-05-07T19:05:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/13/31/8315ea5f8dd18e60970b3022e3a8b93fd37e0b784fbbef86e10c8e6e5ca1/triton-3.7.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22bacffce443f54593dd20f05294d5a40622e0ea9ab632816f87154504356221", size = 201415942, upload-time = "2026-05-07T18:46:06.479Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/13/ec05adfcd87311d532ba61e3af143e8be59fcd26675884c4682841406a20/triton-3.7.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4bf49b00a7a377a68a6da603a876e797614e6455a80e9021669c476a953ad9a", size = 188505104, upload-time = "2026-05-07T19:05:09.843Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7b/468a576e35beef1426e0828e28e9ba9e65f5474d496f16ee126c15646324/triton-3.7.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f111161d49bf903c0eaedde3962353a3d841c08a836839b7cc1025b8426efcf", size = 201457567, upload-time = "2026-05-07T18:46:13.505Z" },
+ { url = "https://files.pythonhosted.org/packages/01/e1/a59a583de59b8f62c495d67c80ee3ea97d09e91ac80c4c6e76456ed8d8ac/triton-3.7.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abdf6beaa89b1bcfb9a43cd990536ce66091a997841a4814b260b7bee4c88c3c", size = 188503209, upload-time = "2026-05-07T19:05:17.935Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b1/b7507bb9815d403927c8dd51d4158ed2e11751a92dbc118a044f247b6848/triton-3.7.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a35d7afe3f3f058e7ec49fcce09794049e0ffc5c59019ac25ec3413741b8c4e7", size = 201453566, upload-time = "2026-05-07T18:46:20.427Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/8f/0bea7a6a0c989315c9135a1d7fb37e41905cfb3a17cbc1f10044ebd4cc3a/triton-3.7.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc1d61c172d257db80ddf42595131fb196ad2e9bdd751e90fe2ef13531734e8b", size = 188612899, upload-time = "2026-05-07T19:05:24.955Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/02/d96f57828d0912aec733b9bc7e0e7dbfd2c6f079a8fa433ac25cb93d1a30/triton-3.7.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70fb9bbdc9f400afc54bbf6eb2670af28829a6ae3996863317964783141daf56", size = 201553816, upload-time = "2026-05-07T18:46:27.49Z" },
+ { url = "https://files.pythonhosted.org/packages/40/fb/82a802dac4689f2a2fb2e69302e6a138eecc3e175bbe976ba3cfc717683a/triton-3.7.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a44a8476d0d3571eac4e4d1048e1ff75aad81a09ff4602ccfc56c6dea1672e", size = 188507879, upload-time = "2026-05-07T19:05:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/af/9904ec6d3c93d9b24e5ec360445bbdf758b7f00bfbeedb89cb0eb64eb8bb/triton-3.7.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9b85e72968a9d8bba5ddb24e9b64aaabaf48affb042f2755cb7cfa92b7531ce", size = 201460637, upload-time = "2026-05-07T18:46:34.749Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/f9/4835a8ea746b88727d8899f4e3ccce4f9cacb38abfc3bb0a638266c53111/triton-3.7.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a160de426fd99f92b0baf509045360afbd3bfaa0b4a5171dde800ec9f09684", size = 188608706, upload-time = "2026-05-07T19:05:39.218Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.48.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e6/bf/f6544ba992ddb9a6077343a576f9844f7f8f06ab819aefd00206e9255f18/uvicorn-0.48.0.tar.gz", hash = "sha256:a5504207195d08c2511bf9125ede5ac4a4b71725d519e758d01dcf0bc2d31c37", size = 91074, upload-time = "2026-05-24T12:08:41.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/be/72532be3da7acc5fdfbccdb95215cd04f995a0886532a5b423f929cda4cc/uvicorn-0.48.0-py3-none-any.whl", hash = "sha256:48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad", size = 71410, upload-time = "2026-05-24T12:08:40.258Z" },
+]
+
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
+[[package]]
+name = "wrapt"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/ac/4370bde262c0e633e6c4f0e56d55095710024cf9a5cecc20c59a10de483c/wrapt-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd57607acc85678925940bd5df0385ff8332083a32fa8d7a43f8767f4997263c", size = 80321, upload-time = "2026-05-22T14:47:43.996Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/79/b8ff3a61e71babf58a8cf4c0d63358e8bad383e15bf7f35e62d2f6b6e4a4/wrapt-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ae574d65c9fa8e86f64f6a7c2668f9fcd507b183e0e577619f504b883cb0a6c", size = 81216, upload-time = "2026-05-22T14:47:45.243Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/fd/c0cac1f77c9c4f6fe58a920ca632ce379bb8be928720e11e8d73de28a5e9/wrapt-2.2.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a04c28c10ba7fd12842b109d2edb0678872a2fe65277ca4ff06a0d61edee245", size = 159208, upload-time = "2026-05-22T14:47:47.176Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/4f/744132a7b2fbefa6b81118ec5942eca5fc2e9a129f9055a0c5e46885a549/wrapt-2.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e2f02472a1cbbf3884b365714a810b5947134a95ad6952b554cb8cce9d492b0", size = 160322, upload-time = "2026-05-22T14:47:49.04Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/95/b7cd9a22a06cf93e6482904ee6afc956248983553593fd1009296d1b3b31/wrapt-2.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac2745950b2bff80219c15ebf2fa9d8427eba7e249739f97e55c9d169e47e9e1", size = 153243, upload-time = "2026-05-22T14:47:50.386Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/4a/eb79423192015f46f0db2872e7e04a3dde8d359b83411e8959e7c9287eaa/wrapt-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67a97e5b6c457f0cd3cfc19ebb2d84463e60c3ece754cc831e4281a3ca29bb18", size = 159231, upload-time = "2026-05-22T14:47:51.753Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/dc/435015b58ce33c6fc4104158fa91ddb0e809ab03a5751fb7465d1d461456/wrapt-2.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:c803a3d331796255af51ba2c79ed0ac8275865b516c09e61f248d1e7aff31ce9", size = 152351, upload-time = "2026-05-22T14:47:53.214Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ac/5d203f98df8fd136b95c5227139aea02d34505e18baf812d0c005df61963/wrapt-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b984d1eb252145d6302c1dbd5e87fc6d404d45531447c84eadec04bf1fcb027", size = 158347, upload-time = "2026-05-22T14:47:54.982Z" },
+ { url = "https://files.pythonhosted.org/packages/52/2f/a92427dbdc74e54c1674abbed27e61b2cb5e7a94441b8c1270c70671d928/wrapt-2.2.1-cp311-cp311-win32.whl", hash = "sha256:8a983a603a18c8708f024f7f6991b2e66159219abbf894634c5056243c55f3cd", size = 77562, upload-time = "2026-05-22T14:47:56.275Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/56/987b9c13b3e1c1a3c6de71284076f996b79caec90e75a87c044a40c23db9/wrapt-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:9c210a6994b21aa9b29e81c8d11560e8fdab54c117e9cff37870d0a27bde1343", size = 80616, upload-time = "2026-05-22T14:47:57.854Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/25/d01f560888d99d94a959c85533de349ce68d71ace3f2591d6ea8f632cfed/wrapt-2.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:401229e9d63ca09f9b8891ecf83798d26c11bbb445d11ed9f1836b6d4585b38a", size = 79025, upload-time = "2026-05-22T14:47:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/89/0c/bfae7b9401583b6d05938cd16dedc43857d96da2f8a3d50d78cc515bf6ff/wrapt-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ffad790d9d11d8ecf9f17c4bb671a5b4089e4d8b575c46c5129597f41f836b0", size = 81021, upload-time = "2026-05-22T14:48:00.313Z" },
+ { url = "https://files.pythonhosted.org/packages/26/58/80f6a6599f933f4caecc1cb3ee88a04faf81e8b9bddbd6109c688dd63e0f/wrapt-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:628f5220c7a904d5fc78f7075c8d7871433eb6d035c94728a22fdf85f193d2a8", size = 81692, upload-time = "2026-05-22T14:48:01.49Z" },
+ { url = "https://files.pythonhosted.org/packages/17/93/fb357cc7847c58a8ae790be718903afa81a28d23e642c843dc4129e8a0b2/wrapt-2.2.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:61acce4257a9883669703c525447c5b4c392edf0f987ae77ec32668440158f0e", size = 169364, upload-time = "2026-05-22T14:48:02.791Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/0b/76b601ee309a8bd556af0eecb184394c20b3c49aa9c8e085aa1ffacc2568/wrapt-2.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727ab4244622cd6ad2390f322642090c877d2e83a608d2653a7643ae5368d926", size = 171079, upload-time = "2026-05-22T14:48:04.22Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/87/ee3f32d5658e3e26d3e0e457922b47a36dd3bfbdfee7f97bb3e802344a66/wrapt-2.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03df9ebed4c73ab93fa8c07e3d41d818dfca1852b15731a3de59457b27814624", size = 160205, upload-time = "2026-05-22T14:48:05.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d0/ae2fd64277a67f5d7bffcf2d05eea1e476263fb2a072baf0b0129ab85984/wrapt-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0d9ff006f420b2ec8296aa56ade43ea7da3e997e85769f0aafc5e0661aacb710", size = 168922, upload-time = "2026-05-22T14:48:07.132Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/f3/2d541a060c5bbafb9400bca4917e4d78bfd1f239f404782c86831a8f6b29/wrapt-2.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:844c858fc3bb7eacc0ba8efa904935d16aac6a4470948ad1e7e55c9f5a2a665f", size = 158388, upload-time = "2026-05-22T14:48:08.629Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/68/8d92c8800c57e93cb116ae9e9d6cbafc34fade5ee9f9107b6f203fb4dc35/wrapt-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87bacdaf225117a342a20d9c03438d701c02112f6e3f351ce9b7f32354f14797", size = 167682, upload-time = "2026-05-22T14:48:10.042Z" },
+ { url = "https://files.pythonhosted.org/packages/30/72/83ea3790ea352439442349388e29ff07b76e0686265f9088bbb505d1608d/wrapt-2.2.1-cp312-cp312-win32.whl", hash = "sha256:2f8c90c8afde51969487be4e1343ae049b268854877d415c2510baf833775052", size = 77857, upload-time = "2026-05-22T14:48:11.782Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/cb/99450668dd3502d62a54a1c8aa56e44f34cb8c1261b381cfe2e7926c3b75/wrapt-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ce32763ac31ce94fe9aada947e479b1975012bff166da409b4b9e4e376cf7e5", size = 80825, upload-time = "2026-05-22T14:48:13.046Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3a/87512881be64e743f9ee4c66f4cbe8e884974bef2a5989af71f999653ac7/wrapt-2.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d1b4d0e0c2119587a31f5c029abd547e0c81d93b89d394566fe1588659eb579", size = 79087, upload-time = "2026-05-22T14:48:14.323Z" },
+ { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" },
+ { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" },
+ { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" },
+ { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" },
+ { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" },
+ { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" },
+ { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/a3/11d7f34ebbf3231bc907a3e6d5ee051b14d034c1bc7b65a97d5cc00516df/wrapt-2.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f56a647e4eaf5f0ca40330fb070f566bdf9f7b0db89a1af20d71c28dcd7a0ab", size = 80879, upload-time = "2026-05-22T14:48:51.802Z" },
+ { url = "https://files.pythonhosted.org/packages/13/3c/b74cfd984cef560b900fb1a727af20352d89e1f06bf2e1114dd3f00f5f5a/wrapt-2.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:64b7deeda4b70408e382328d8bbe52a256fe9bc63ae3db86d804608367e5422c", size = 81462, upload-time = "2026-05-22T14:48:53.18Z" },
+ { url = "https://files.pythonhosted.org/packages/15/a3/7c8f704b8dc07dfe0a5d01c2edbfd88317aa8e5e3fa7c743eb7a085ae767/wrapt-2.2.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9cf53ba90717db2e292401de290776c498d4bbfb0d4a559ca2895db8b9dcb5c", size = 167251, upload-time = "2026-05-22T14:48:54.562Z" },
+ { url = "https://files.pythonhosted.org/packages/80/85/a34d1888d97247da6c2ff6118c3a721c73ed8cc4dd198c00208bb73b6f80/wrapt-2.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf3638274ab9d9b724c9baa0b4c04e132cd6faefb78b4dd3dd1a02a4bdaad41e", size = 166316, upload-time = "2026-05-22T14:48:56.065Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d7/72ffaeb01eebc704afe3fb99e840480f4bda45f0fa66e3381b6a39251c8f/wrapt-2.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aed9658797d0b45d6c49adcfc6b41f66e6f2d0c6de3ec79e16cf4b1855df240f", size = 157952, upload-time = "2026-05-22T14:48:57.924Z" },
+ { url = "https://files.pythonhosted.org/packages/24/5b/36f5d6b024e4edfdd90b140742d11ebcf7836daf5c9daf326c55c24db412/wrapt-2.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d676ee388bc42a04d56dd7deb5605244dac2e35cc2fadbb43c9fa25bbd93508", size = 166130, upload-time = "2026-05-22T14:48:59.384Z" },
+ { url = "https://files.pythonhosted.org/packages/81/06/9296d9e97bfdef5483dfcc859d57b095b257144b2bc5300ab521e06f4bc7/wrapt-2.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e395f7bc31851ef9b612050368cb446e9bc14cd7454b025018980349caf25ae5", size = 156604, upload-time = "2026-05-22T14:49:00.921Z" },
+ { url = "https://files.pythonhosted.org/packages/53/37/16953929ed6776175720e58fc966e779926d8d71e2c7b2273230590ca71f/wrapt-2.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f1845c2a8cc1180ccccfa45785dd06f562730d19ef75be180334254012b6283", size = 166007, upload-time = "2026-05-22T14:49:02.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/73/20ee58c0612dae7c31131a7095345812ed2c7b389019e175f68cde34e5b4/wrapt-2.2.1-cp314-cp314-win32.whl", hash = "sha256:436addbc4bb4fc0a88c702577f51195d7d73683a7f3e0e5b253d8404d7847243", size = 78327, upload-time = "2026-05-22T14:49:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/22/b3/ef7c3295d02e0448a71c639a36a057f46d524d057c9486291a7a3039e65c/wrapt-2.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:50972a1d974ea07725a7f6b1cec5f8759008afd030a0024843ebe7d52de47f2b", size = 81144, upload-time = "2026-05-22T14:49:05.093Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/dc/7bdf336953f99f4ceb0a584bb8870e42c8f26f93ea10c87834dad62f1668/wrapt-2.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:1c9934ea5d92957e3cd0adbc0845539dccfd62710ebe16195a8c66c53954db36", size = 79569, upload-time = "2026-05-22T14:49:06.413Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/6d/6dfae80150ff1919c356d1dd528f049bcdfaae29b4d284bc957e022caef4/wrapt-2.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17de18fc12cea55b8a9587314cb830573e37fb33b247a7515696350863714188", size = 82892, upload-time = "2026-05-22T14:49:07.925Z" },
+ { url = "https://files.pythonhosted.org/packages/82/7b/4e34766a7d7804ffce9e71befe47e9b3225dc350c49c94493c4ab39fd3a5/wrapt-2.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9dec1aca52dddde7df94818310fa2fe79739c8f385b2014c4cb1035f5508199", size = 83333, upload-time = "2026-05-22T14:49:09.257Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/57/0b34db3e8de44ccfece62d7b337abd1631dd810f5adc5f3db571727836b5/wrapt-2.2.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:69f2e9244542cb34dd59c7f073445b9e54ad9f3fce8d93606c368a1b499fc413", size = 202899, upload-time = "2026-05-22T14:49:10.572Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/45/ac0c459f154b99d92789a6cba7ca727185b83513b986f8ec7fe2aacddcbf/wrapt-2.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d83966dc7f4f45e8b97b5933685ac2e6e67fc0e19246ea314bceb9a8970c956", size = 209986, upload-time = "2026-05-22T14:49:12.229Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e4/77e37ff33ad018fa81ade52c25fa327b80b56f81d734279a63614fcb4cbc/wrapt-2.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78b0aa6bfb7be8deed0ab23e7aa028cc5210c29bc2d32a04d52b50e517a7307e", size = 194893, upload-time = "2026-05-22T14:49:14.139Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/9d/7ea651d1ab032fc5fa222fbec91d0f8a1397f6ae04ebb93fa7219aa921d7/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:05d5cb74d1b232ec8cfa130a8f900708699ff2491d97b8f85a4cdc5996294b85", size = 205636, upload-time = "2026-05-22T14:49:15.714Z" },
+ { url = "https://files.pythonhosted.org/packages/09/af/8e88031a701275b9085c54e64bc88c0b1cd55c77eadd400691c371cd76c4/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f6518b94edb9150452e9aba08027d4cc293433753ec1fbefb4629a21cbc74181", size = 192267, upload-time = "2026-05-22T14:49:17.283Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/a8/e657ca876b06710194f243d81c4b0896ade646e244bdbec2d87c8c56a8bd/wrapt-2.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ed55af48b3eb28f43228ca2306788892bcb629eb2b5c4876e2a3659872c2f17a", size = 198378, upload-time = "2026-05-22T14:49:18.785Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/59/822efe4ea722a3961331bfa35b7d90937790d2c20f0616de1997ccc3aebd/wrapt-2.2.1-cp314-cp314t-win32.whl", hash = "sha256:2e08688ab16525897da6589d56d0aebaf417bbe91c2d8e3b96203b1efa596e85", size = 80226, upload-time = "2026-05-22T14:49:20.264Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/31/2a7dc5f6abb2fca0b6e1610e120419f603650aceb4f1d3ac4cae0354e162/wrapt-2.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd0135d34387f5fd087d9be368ea77ea89cf2451dc1cd1c622d35021bcb3ab50", size = 83835, upload-time = "2026-05-22T14:49:21.634Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c0/782b86e28d1ceebeb74cccea12d2cd3d2ba0bd68e3dec20b1bc5873f6127/wrapt-2.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f70db64e8266d7c45d3b735f2e08eeb434b5e03da9a479ae42b2e2e486a21a00", size = 80722, upload-time = "2026-05-22T14:49:23.59Z" },
+ { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" },
+]