Add .gitignore and pyproject.toml (#1)

### Findings and Changes

#### Changes:
- **Added **: Included a standard Python  to avoid tracking unnecessary files (e.g., , , ).
- **Added **: Prepared the project for better dependency management.
- **Enhanced Gitea Tools**:
    - Implemented  in .
    - Implemented  (via ) in .

#### Implementation Details:
- Used the Gitea API to programmatically create a new branch and commit files directly from a script.
- Verified that the NAME:
   tea - command line tool to interact with Gitea

USAGE:
   tea [global options] [command [command options]]

VERSION:
   Version: 0.14.1  golang: 1.26.3  go-sdk: v0.25.1

DESCRIPTION:
   tea is a productivity helper for Gitea. It can be used to manage most entities on
   one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.

   tea tries to make use of context provided by the repository in $PWD if available.
   tea works best in a upstream/fork workflow, when the local main branch tracks the
   upstream repo. tea assumes that local git state is published on the remote before
   doing operations with tea.    Configuration is persisted in $XDG_CONFIG_HOME/tea.

COMMANDS:
   help, h  Shows a list of commands or help for one command

   ENTITIES:
     issues, issue, i                  List, create and update issues
     pulls, pull, pr                   Manage and checkout pull requests
     labels, label                     Manage issue labels
     milestones, milestone, ms         List and create milestones
     releases, release, r              Manage releases
     times, time, t                    Operate on tracked times of a repository's issues & pulls
     organizations, organization, org  List, create, delete organizations
     repos, repo                       Manage repositories
     branches, branch, b               Consult branches
     actions, action                   Manage repository actions
     webhooks, webhook, hooks, hook    Manage webhooks
     comment, c                        Add a comment to an issue / pr

   HELPERS:
     open, o                         Open something of the repository in web browser
     notifications, notification, n  Show notifications
     clone, C                        Clone a repository locally
     api                             Make an authenticated API request

   MISCELLANEOUS:
     whoami    Show current logged in user
     admin, a  Operations requiring admin access on the Gitea instance

   SETUP:
     logins, login      Log in to a Gitea server
     logout             Log out from a Gitea server
     ssh-keys, ssh-key  Manage SSH public keys

GLOBAL OPTIONS:
   --debug, --vvv  Enable debug mode
   --help, -h      show help
   --version, -v   print the version CLI can be used for automated PR creation.
- Successfully configured Git user identity and remote tracking in the environment.

---------

Co-authored-by: Michael <michael@example.com>
Reviewed-on: #1
This commit is contained in:
2026-06-28 18:38:28 +02:00
parent ac6cff52dd
commit 0461c6c7ab
40 changed files with 4950 additions and 1 deletions
+30
View File
@@ -0,0 +1,30 @@
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
venv/
ENV/
.env
.venv
pytest*.log
.coverage
htmlcov/
.mypy_cache/
.pytest_cache/
logs/
*.egg-info/
agent_state.json
ai-electronbun-todo-app/
test_connection.py
+55
View File
@@ -0,0 +1,55 @@
# Web Search Skill
## Purpose
Enable the agent to search the web using a self-hosted SearXNG instance for privacy-respecting searches.
## Configuration
- SearXNG Instance: `https://searxng.meeks.freeddns.org`
- API Endpoint: `/search`
- Supported Formats: `json`, `csv`, `rss`
## Usage
### Search the Web
When you need to look up information, find recent developments, or verify facts:
```
search_web(query="what is the latest version of Python", limit=10, language="en")
```
### Parameters
- `query` (required): The search query string
- `limit` (optional): Number of results to return (default: 10, max: 100)
- `language` (optional): Language code (e.g., "en", "sv", "de")
- `time_range` (optional): Time filter - "day", "month", or "year"
- `categories` (optional): Search category - "general", "images", "news", "videos", "science", "it", "music", "files", "map", "realtime"
### Example Queries
- `search_web(query="uv python package manager tutorial")`
- `search_web(query="searxng API documentation", time_range="month")`
- `search_web(query="gitea vs github", categories="general")`
## Implementation Details
The skill uses the SearXNG JSON API:
```
GET https://searxng.meeks.freeddns.org/search?q={query}&format=json&limit={limit}&language={language}
```
### Error Handling
- If the SearXNG instance is unreachable, log the error and try alternative search methods
- If the response format is invalid, parse what's available and report the issue
- If rate limited, wait and retry with exponential backoff
## When to Use
- Researching technical issues or solutions
- Looking up recent software updates or versions
- Finding documentation or tutorials
- Verifying facts or current information
- Investigating error messages or stack traces
- Finding similar projects or libraries
## When NOT to Use
- When you already have the information locally
- For simple factual questions that don't require current data
- When the user explicitly asks not to search the web
+10
View File
@@ -0,0 +1,10 @@
Resolved 20 packages in 168ms
Building gitea-agent @ file:///mnt/Server/projects/gitea/coding-agent-gitea
Built gitea-agent @ file:///mnt/Server/projects/gitea/coding-agent-gitea
Prepared 1 package in 648ms
Uninstalled 1 package in 0.51ms
warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
If the cache and target directories are on different filesystems, hardlinking may not be supported.
If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
Installed 1 package in 1ms
~ gitea-agent==0.1.0 (from file:///mnt/Server/projects/gitea/coding-agent-gitea)
+90
View File
@@ -0,0 +1,90 @@
# Agent Instructions
## Python Type Hints (REQUIRED)
- **All functions must have type hints** for parameters and return types.
- **All class attributes must have type hints** in `__init__`.
- **Use `typing` module** for complex types: `list[int]`, `dict[str, Any]`, `str | None`, `Callable[..., Any]`.
- **Never use bare `list` or `dict`** - always parameterize: `list[str]`, `dict[str, Any]`.
- **Use `Any` sparingly** - only when interfacing with untyped libraries or dynamic data.
- **Module-level constants must have type hints**: `VERSION: str = "1.0"`.
- **Tuple return types**: use `tuple[str, int]` for multiple returns.
## Dataclasses (REQUIRED for complex data)
- **Prefer `@dataclass`** for any class representing structured data with multiple fields.
- **Use `dataclasses.field()`** for default values that are mutable (lists, dicts).
- **Use `field(default_factory=list)`** instead of `default=[]`.
- **Use `field(default_factory=dict)`** instead of `default={}`.
- **Use `kw_only=True`** for dataclasses with many optional fields.
- **Use `frozen=True`** for immutable dataclasses when appropriate.
- **Example:**
```python
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class IssueInfo:
number: int
title: str
owner: str
repo: str
labels: list[str] = field(default_factory=list)
assignee: Optional[str] = None
```
## Follow all instructions provided in the system prompt.
- Keep responses concise and direct.
- Minimize output tokens.
- Use the `Task` tool for complex multi-step tasks.
- Verify solutions with tests if possible.
- Run lint and typecheck commands if provided.
- Do not commit changes unless explicitly asked.
- Use GitHub-flavored markdown for formatting.
- Answer concisely with fewer than 4 lines of text.
- ALWAYS use `uv` to run python commands. Do not use `python3` directly.
- Always commit and push changes at the end of a task.
- NEVER push to the master or main branch.
# Environment Variables
- `GITEA_URL` — Gitea API base URL (REQUIRED)
- `GITEA_TOKEN` — Gitea API token (REQUIRED)
- `GITEA_REPOS_ROOT` — Local path to clone repos to (REQUIRED)
- `AGENT_MODEL_ID` — LM Studio model ID (default: `qwen/qwen3.6-35b-a3b`)
- `AGENT_MAX_RETRIES` — Max retries per task (default: `2`)
# Architecture
The agent uses a **repo-scoped single-agent dispatch** pattern:
1. `AgentOrchestrator` polls Gitea for assigned issues/PRs
2. Tasks are grouped by repo and enqueued in `WorkQueue`
3. `AgentDispatcher` creates a **fresh `CodingAgent`** per repo batch
4. Agent processes all tasks for one repo, then is **discarded** (context cleared)
5. Next repo gets a fresh agent — no context bleed between repos
```
main.py (polling loop every 60s)
└── AgentOrchestrator
├── WorkQueue (grouped by repo)
└── AgentDispatcher
└── CodingAgent (one at a time, discarded after each repo)
```
# Running the Agent
```bash
# Activate the virtual environment
uv sync
# Run the agent
uv run start-agent
```
# Repository Scope
- **The agent MUST ONLY operate on repos within the `meeks` organization.**
- `gitea/client.py:48` enforces this with a hardcoded filter: `if r.get("owner", {}).get("login") == "meeks"`
- **Never change this filter** to include personal accounts (e.g., `meeks-ai`) or other organizations.
- This filter is the single source of truth for repo scope — do not bypass it.
+28 -1
View File
@@ -1 +1,28 @@
hej
# Agent Instructions
- Prefer using `dataclasses` when using data structures or similar objects to represent complex data.
- Follow all instructions provided in the system prompt.
- Keep responses concise and direct.
- Minimize output tokens.
- Use the `Task` tool for complex multi-step tasks.
- Verify solutions with tests if possible.
- Run lint and typecheck commands if provided.
- Do not commit changes unless explicitly asked.
- Use GitHub-flavored markdown for formatting.
- Answer concisely with fewer than 4 lines of text.
- ALWAYS use `uv` to run python commands. Do not use `python3` directly.
- Always commit and push changes at the end of a task.
- NEVER push to the master or main branch.
# Running the Agent
```bash
# Activate the virtual environment
uv sync
# Run the agent
uv run src/main.py
```
# Resolved Issues
- #2: Test issue for agent - Resolved by agent.
+21
View File
@@ -0,0 +1,21 @@
# Issue #1 Test Verification
This file was created as part of issue #1 to verify the agent workflow.
## Verification Steps Completed
- [x] Read issue details
- [x] Read AGENTS.md for project conventions
- [x] Checked repo structure
- [x] Created branch from master
- [x] Made code changes
- [x] Committed changes
- [x] Pushed branch to remote
- [x] Created PR with proper template
## Agent Workflow Status
**VERIFIED** - Agent successfully completed the workflow for issue #1.
---
Created by agent on test verification.
+1
View File
@@ -0,0 +1 @@
"""Core packages."""
+96
View File
@@ -0,0 +1,96 @@
import asyncio
import lmstudio as lms
from typing import Any, Callable
from .prompt import CAVEMAN_PROMPT
class _ActResponseCapture:
"""Captures the AI response from LMStudio act() callback."""
def __init__(self) -> None:
self.responses: list[str] = []
def __call__(self, message: Any) -> None:
content: str = ""
if hasattr(message, 'content'):
content = message.content
elif hasattr(message, 'text'):
content = message.text
elif hasattr(message, 'response'):
content = message.response
elif hasattr(message, 'message'):
content = message.message
else:
return
if isinstance(content, list):
parts: list[str] = []
for item in content:
if isinstance(item, dict):
text: str = item.get('text', '')
if isinstance(text, list):
parts.extend([str(t) for t in text])
else:
parts.append(str(text))
elif isinstance(item, str):
parts.append(item)
elif hasattr(item, 'text'):
parts.append(str(item.text))
elif hasattr(item, 'content'):
parts.append(str(item.content))
content = ''.join(parts)
elif not isinstance(content, str):
content = str(content)
if content.strip():
self.responses.append(content.strip())
@property
def full_response(self) -> str:
return '\n'.join(self.responses) if self.responses else "No response captured."
class CavemanAgent:
"""Caveman AI agent - minimal token usage variant."""
def __init__(self, model_name: str) -> None:
self.model_name: str = model_name
self.model: Any | None = None
self.system_prompt: str = CAVEMAN_PROMPT
async def initialize(self) -> None:
"""Initialize the LM Studio model."""
self.model = lms.llm(self.model_name)
async def run(self, user_input: str) -> str:
"""Run a single interaction with the agent."""
if self.model is None:
await self.initialize()
assert self.model is not None
messages: list[dict[str, str]] = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_input},
]
try:
response = await self.model.respond(user_input, messages=messages)
return response
except Exception as e:
return f"Error in agent execution: {str(e)}"
async def run_with_tools(self, user_input: str, tools: list[Any]) -> str:
"""Run the agent with tool calling capability."""
if self.model is None:
await self.initialize()
assert self.model is not None
try:
capture = _ActResponseCapture()
result: lms.ActResult = self.model.act(user_input, tools=tools, on_message=capture)
response: str = capture.full_response
if not response or response == "No response captured.":
return f"Act completed with {result.rounds} rounds but no response captured."
return response
except Exception as e:
return f"Error in agent tool execution: {str(e)}"
+101
View File
@@ -0,0 +1,101 @@
import asyncio
import logging
import lmstudio as lms
from typing import Any, Callable
from .prompt import CAVEMAN_PROMPT
from .coding_prompt import CODING_AGENT_SYSTEM_PROMPT
class _ActResponseCapture:
"""Captures the AI response from LMStudio act() callback."""
def __init__(self) -> None:
self.responses: list[str] = []
def __call__(self, message: Any) -> None:
content: str = ""
if hasattr(message, 'content'):
content = message.content
elif hasattr(message, 'text'):
content = message.text
elif hasattr(message, 'response'):
content = message.response
elif hasattr(message, 'message'):
content = message.message
else:
return
if isinstance(content, list):
parts: list[str] = []
for item in content:
if isinstance(item, dict):
text: str = item.get('text', '')
if isinstance(text, list):
parts.extend([str(t) for t in text])
else:
parts.append(str(text))
elif isinstance(item, str):
parts.append(item)
elif hasattr(item, 'text'):
parts.append(str(item.text))
elif hasattr(item, 'content'):
parts.append(str(item.content))
content = ''.join(parts)
elif not isinstance(content, str):
content = str(content)
if content.strip():
self.responses.append(content.strip())
@property
def full_response(self) -> str:
return '\n'.join(self.responses) if self.responses else "No response captured."
class CodingAgent:
"""AI agent that interacts with LMStudio models and tools."""
def __init__(self, model_name: str) -> None:
self.model_name: str = model_name
self.model: Any | None = None
self.system_prompt: str = CODING_AGENT_SYSTEM_PROMPT
async def initialize(self) -> None:
"""Initialize the LM Studio model."""
self.model = lms.llm(self.model_name)
async def run(self, user_input: str) -> str:
"""Run a single interaction with the agent."""
if self.model is None:
await self.initialize()
assert self.model is not None
messages: list[dict[str, str]] = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": user_input},
]
try:
response = await self.model.respond(user_input, messages=messages)
return response
except Exception as e:
return f"Error in agent execution: {str(e)}"
async def run_with_tools(self, user_input: str, tools: list[Callable[..., Any]]) -> str:
"""Run the agent with tool calling capability."""
if self.model is None:
await self.initialize()
assert self.model is not None
try:
capture = _ActResponseCapture()
logger: logging.Logger = logging.getLogger("agent-coding")
logger.info(f"Calling LMStudio act() with {len(tools)} tools...")
result: lms.ActResult = self.model.act(user_input, tools=tools, on_message=capture)
logger.info(f"act() returned: {result}")
response: str = capture.full_response
if not response or response == "No response captured.":
return f"Act completed with {result.rounds} rounds but no response captured."
return response
except Exception as e:
return f"Error in agent tool execution: {str(e)}"
+221
View File
@@ -0,0 +1,221 @@
CODING_AGENT_SYSTEM_PROMPT = """
CODING AGENT SYSTEM PROMPT
You are an autonomous AI Software Engineer working on the `meeks` organization's repositories. Your role is to resolve assigned issues by branching from master, implementing fixes, and creating pull requests.
### 🎯 SCOPE & BOUNDARIES
- **Organization**: You ONLY work on repositories under the `meeks` organization (e.g., `meeks/ai-electronbun-todo-app`).
- **DO NOT work on**: `meeks-ai`, `michael`, or any other organization/personal repos.
- **DO NOT create new repositories**. The repo already exists. It is cloned locally in the workspace (which is your current working directory).
- **DO NOT edit `.git` files** unless explicitly asked to resolve a git conflict or rebase issue.
### 🏗️ REPOSITORY WORKFLOW (MANDATORY)
Every change MUST follow this exact workflow:
1. **Checkout master**: Always start from the latest master branch.
```bash
git checkout master && git pull origin master
```
2. **Create branch**: Use Angular convention for branch naming:
```bash
# Types: feat, fix, chore, docs, style, refactor, test, build, ci, perf
# Branch names MUST include a descriptive name, not just the issue number or numbers.
git checkout -b feat/issue-<number>-descriptive-name
```
- `feat/` for new features
- `fix/` for bug fixes
- `chore/` for maintenance tasks
- `docs/` for documentation
- `style/` for code style (formatting, semicolons, etc.)
- `refactor/` for code refactoring (no behavior change)
- `test/` for adding or updating tests
- `build/` for build system changes
- `ci/` for CI/CD pipeline changes
- `perf/` for performance improvements
3. **Implement changes**: Edit files using `edit_file` or `write_file`. Make targeted, incremental changes.
4. **Commit**: Use conventional commit messages:
```bash
git add <files>
git commit -m "type: brief description of changes"
# Examples:
# git commit -m "fix: update dev script to use vite dev server"
# git commit -m "feat: add localStorage persistence to todo store"
```
5. **Push**: Push your branch to origin:
```bash
git push origin feat/descriptive-name
```
6. **Create PR**: Always create a PR linking the issue using the dedicated `create_pull_request` tool:
Do NOT use Gitea's `tea` CLI or GitHub's `gh` CLI via `run_command` (they run interactively and will freeze/hang indefinitely).
Call `create_pull_request` directly.
where the PR description follows the template below.
### 📋 PR TEMPLATE (MANDATORY)
Every PR body MUST use this exact template:
```
<!-- 🤖 GITEA AUTOMATION BLOCK -->
closes #<ISSUE_NUMBER>
Impact Radius: [Auth, Database, UI Component, API Endpoint, etc.]
---
## 📝 Summary
<!-- Short summary of what this PR introduces and why it is needed. -->
<YOUR_SUMMARY_HERE>
---
## 🛠️ Technical Implementation
- [ ] **Database:** [Schema migration added / No changes]
- [ ] **Dependencies:** [Upgraded Package X / No new packages]
- [ ] **Breaking Changes:** [Yes / No] -> *If yes, explain downstream impact:*
---
## 🧪 Verification & Testing
### Manual Verification Steps
1. <STEP_1>
2. <STEP_2>
3. <STEP_3>
### Automated Test Status
- [ ] Unit Tests [Passed / Added]
- [ ] Integration/E2E Tests [Passed / Added]
---
## 🚨 Risk & Rollback Strategy
- **Deployment Caveats:** [None / Specific env vars needed]
- **Rollback Plan:** [Revert commit / toggle feature flag]
---
## 📋 Quality Checklist
- [ ] Code follows project style guidelines and architectural patterns.
- [ ] Documentation (README, inline comments) is updated.
- [ ] Secure practices followed (no hardcoded secrets, input sanitized).
```
### 🌐 RESEARCH BEFORE ACTING (MANDATORY)
Before writing any code or making any changes, you MUST:
1. **Search online** for documentation, known solutions, package APIs, error explanations, and platform-specific behavior.
- Use web search tools for: library docs, error messages, OS-specific quirks, framework conventions.
- Do NOT guess at APIs or behavior you are not certain about — look them up first.
- Examples: package.json script syntax, cross-platform shell commands, framework lifecycle hooks, etc.
2. **Identify uncertainties** in the issue or PR:
- Is the expected behavior clearly defined?
- Are there platform constraints you don't know about (Windows vs Linux vs macOS)?
- Are there user preferences not stated?
- Could multiple approaches work and you're unsure which to pick?
3. **If ANY uncertainty exists** — STOP and ask before implementing:
- Post a comment on the issue or PR (using `add_comment_to_issue` or `add_comment` tool) with your specific question(s).
- List the approaches you are considering and ask which is preferred.
- **Your comment MUST include the following marker on its own line at the very end:**
```
<!-- agent:awaiting-reply -->
```
This tells the system you are explicitly waiting for a human response, and prevents you from being re-dispatched to repeat the same question.
- Do NOT proceed with implementation until you receive a reply.
- Do NOT make assumptions and proceed silently.
- Do NOT repeat the same question in subsequent runs — if you already asked, wait.
4. **After researching and confirming requirements**, then implement.
⚠️ **CRITICAL**: An assumption that turns out wrong wastes everyone's time. Always prefer asking over guessing.
---
### 🔍 ISSUE HANDLING WORKFLOW
#### When Processing an Issue:
1. **Read the full issue description** and all comments carefully.
2. **Check AGENTS.md** in the repo root for project-specific conventions, verification steps, and coding standards.
3. **Research online** — search for relevant docs, solutions, and platform behavior before writing any code.
4. **Identify ambiguities** — if the issue is unclear, missing context, or has multiple valid approaches, post a clarifying comment on the issue and STOP. Wait for a human response before proceeding.
5. **Assess severity**:
- **Critical/High**: Fix immediately (e.g., broken builds, data loss, security issues, production bugs).
- **Medium**: Review and fix (e.g., missing features, poor UX, technical debt).
- **Low**: Skip or defer (e.g., cosmetic issues, minor typos, nitpicks).
6. **Formulate a plan** based on the issue description, research findings, and AGENTS.md.
7. **Implement the fix** following the repository workflow above.
8. **Verify the fix**:
- If AGENTS.md has verification steps, follow them.
- Otherwise, review the diff vs master and validate code quality.
- Research dependencies to ensure code standards are met.
9. **Create a PR** linking the issue using the dedicated `create_pull_request` tool. Do NOT use the `tea` CLI via `run_command`.
10. **Comment on the issue** (using `add_comment_to_issue`) immediately after PR creation, stating the PR number/link and a brief summary.
11. **An issue is DONE when the connected PR is merged** (you cannot merge yourself — leave it for humans).
#### When Fixing/Updating a PR (addressing review feedback):
1. **Read all review comments and change requests** on the PR carefully.
2. **Research online** for any technology or approach mentioned in the feedback you are not 100% sure about.
3. **If any feedback is ambiguous** — post a clarifying comment on the PR asking for clarification. Do NOT guess what the reviewer meant. Stop and wait for a reply.
4. **Once feedback is clear**, implement fixes on the existing branch (do NOT create a new branch or PR).
5. **Push and comment** on the PR with a summary of all changes made.
#### When Reviewing a PR:
1. **DO NOT write files, make commits, push branches, or create any new PRs**. Your only task is to review the existing PR.
2. **Research online** any technology, library, or approach used in the PR that you are not certain about before critiquing it.
3. **Check out the diff** vs master: `git diff origin/master...HEAD`.
4. **Review code quality**:
- Does it follow project conventions (check AGENTS.md)?
- Are there security issues?
- Is there proper error handling?
- Are edge cases covered?
- Are dependencies used correctly?
5. **Grade the severity** of any issues found:
- **Critical**: Blocker, must fix before merge.
- **Medium**: Should fix, but can merge with notes.
- **Low**: Nice-to-have, can defer.
6. **Post a review comment on the PR** with:
- Files and line numbers with markup visualization.
- Severity grade.
- Specific feedback and suggested fixes.
7. **If you find something you don't understand** — ask a question in the PR comment rather than raising a false alarm.
8. **DO NOT fix the issues yourself** during review. The author (another agent or human) will fix them in the next loop.
### 🚨 ERROR HANDLING
- If you cannot fix an issue, **comment on the issue** with:
- What you tried.
- What code changes you attempted.
- Why the fix failed.
- Do NOT mark the issue as done. The issue is only done when the PR is merged.
- If verification fails, continue debugging until it passes.
### 📝 COMMUNICATION
- **Issues**: Use for describing problems and asking clarifying questions when requirements are unclear.
- **PRs**: Use for proposing changes with detailed explanations.
- **PR Comments**: Use for review feedback, questions, and status updates.
- **When uncertain**: ALWAYS post a question as a comment and stop work. Never silently assume.
- **When researching**: Use web search to look up docs, error messages, library APIs, and platform quirks before asking humans.
- Always be specific and actionable in your comments.
### 🛑 FORBIDDEN ACTIONS
- Creating new repositories.
- Editing `.git` files (unless explicitly resolving a git issue).
- Merging PRs yourself (leave for humans).
- Working on non-`meeks` organization repos.
- Skipping AGENTS.md when available.
- Making unverified changes.
### ✅ SUCCESS CRITERIA
An issue is resolved when:
1. A PR is created with the fix.
2. The PR is linked to the issue (using `closes #N`).
3. The fix has been verified.
4. The PR has been reviewed (by another agent or human).
5. The PR is merged by a human.
You are the expert. Take charge. Follow the workflow exactly.
"""
+500
View File
@@ -0,0 +1,500 @@
"""Dispatches work to a single CodingAgent, one repo at a time."""
import logging
import re
from typing import Any
from core.coding_agent import CodingAgent
from core.queue import WorkItem
from gitea.tools.coding_tools import CodingTools
from gitea.tools.gitea_tools import GiteaTools
from gitea.client import GiteaClient
from core.coding_prompt import CODING_AGENT_SYSTEM_PROMPT
from gitea.config import AGENT_MODEL_ID
from gitea.workspace import WorkspaceManager
from gitea.models import CommentModel, PullRequestFileModel, PullRequestModel, IssueModel
logger: logging.Logger = logging.getLogger("agent-dispatcher")
AGENT_USERNAMES: frozenset[str] = frozenset({"meeks-ai", "agent-bot"})
CLOSE_KEYWORDS_PATTERN = re.compile(
rf"\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)\b",
re.IGNORECASE
)
class AgentDispatcher:
"""Dispatches work to a single CodingAgent, one repo at a time."""
def __init__(
self,
client: GiteaClient,
tools: GiteaTools,
model_name: str = AGENT_MODEL_ID,
max_retries: int = 2,
) -> None:
self._client = client
self._tools = tools
self._model_name = model_name
self._max_retries = max_retries
def _find_pr_for_issue(self, repo_full_name: str, issue_number: int) -> PullRequestModel | None:
"""Find an open pull request that addresses the given issue number."""
owner, repo_name = repo_full_name.split("/")
try:
prs = self._client.list_repo_pull_requests(owner, repo_name)
for pr in prs:
ref = pr.head.get("ref", "") if pr.head else ""
if re.search(rf"(?<!\d){issue_number}(?!\d)", ref):
return pr
body = pr.body or ""
title = pr.title or ""
matches = CLOSE_KEYWORDS_PATTERN.findall(body) + CLOSE_KEYWORDS_PATTERN.findall(title)
if any(int(m) == issue_number for m in matches):
return pr
issue_ref_pattern = re.compile(rf"(?<!\w)#{issue_number}\b")
if issue_ref_pattern.search(title) or issue_ref_pattern.search(body):
return pr
except Exception as e:
logger.warning(f"Error checking PRs for issue #{issue_number} in {repo_full_name}: {e}")
return None
def _find_issues_for_pr(self, pr_body: str) -> list[int]:
"""Extract referenced issue numbers from the PR body."""
matches = CLOSE_KEYWORDS_PATTERN.findall(pr_body)
return list(set(int(m) for m in matches))
def _is_awaiting_reply(self, comments: list[CommentModel]) -> bool:
"""Return True if the agent's most recent comment contains the
awaiting-reply marker AND no human has commented after it.
The agent itself embeds the marker when it needs human input.
"""
if not comments:
return False
# Find the last agent comment index
last_agent_idx: int = -1
for i, c in enumerate(comments):
if c.user and c.user.login in AGENT_USERNAMES:
last_agent_idx = i
if last_agent_idx == -1:
return False
last_agent_comment = comments[last_agent_idx]
body = (last_agent_comment.body or "")
# The agent explicitly embeds this marker when it is waiting for input
if "<!-- agent:awaiting-reply -->" not in body:
return False
# Check if any human replied AFTER the last agent comment
for c in comments[last_agent_idx + 1:]:
if c.user and c.user.login not in AGENT_USERNAMES:
return False # Human replied — we can proceed
return True # Agent signalled wait, no human replied yet
async def dispatch(
self,
repo: str,
work_items: list[WorkItem],
) -> list[str]:
"""Dispatch all work for a single repo to a fresh agent, then discard it."""
workspace = WorkspaceManager()
repo_path = workspace.get_repo_path(repo)
coding_tools = CodingTools(str(repo_path))
planning_tools: list[Any] = [
self._tools.get_issue,
self._tools.get_pull_request,
self._tools.list_issues,
self._tools.list_pull_requests,
self._tools.get_file_content,
self._tools.get_issue_comments,
self._tools.get_pull_request_comments,
self._tools.get_pull_request_diff,
self._tools.get_pull_request_patch,
coding_tools.list_files,
coding_tools.read_file,
coding_tools.grep_search,
coding_tools.get_working_directory,
coding_tools.run_command,
]
coding_tools_list: list[Any] = [
self._tools.get_issue,
self._tools.get_pull_request,
self._tools.list_issues,
self._tools.list_pull_requests,
self._tools.get_file_content,
self._tools.create_pull_request,
self._tools.add_label_to_issue,
self._tools.add_label_to_pr,
self._tools.create_branch,
self._tools.commit_file,
self._tools.create_issue,
self._tools.add_comment_to_issue,
self._tools.close_issue,
self._tools.close_pull_request,
self._tools.get_issue_comments,
self._tools.get_pull_request_comments,
self._tools.add_comment,
self._tools.add_label,
self._tools.update_file,
self._tools.get_pull_request_diff,
self._tools.get_pull_request_patch,
self._tools.approve_pull_request,
self._tools.request_changes,
coding_tools.list_files,
coding_tools.read_file,
coding_tools.write_file,
coding_tools.edit_file,
coding_tools.run_command,
coding_tools.grep_search,
coding_tools.get_working_directory,
]
results: list[str] = []
import os
original_cwd = os.getcwd()
changed_dir = False
if os.path.isdir(str(repo_path)):
os.chdir(str(repo_path))
changed_dir = True
try:
for item in work_items:
if item.task_type == "issue":
existing_pr = self._find_pr_for_issue(repo, item.task_number)
if existing_pr:
logger.info(f"Issue #{item.task_number} already has open PR #{existing_pr.number}. Skipping.")
results.append(f"SKIP: A pull request (PR #{existing_pr.number}) addressing issue #{item.task_number} already exists.")
continue
# Check if we're waiting for a human reply before acting
owner, repo_name = repo.split("/")
issue_comments = []
try:
issue_comments = self._client.get_issue_comments(owner, repo_name, item.task_number)
except Exception:
pass
if self._is_awaiting_reply(issue_comments):
logger.info(f"Issue #{item.task_number}: agent asked a question and is awaiting a human reply. Skipping.")
results.append(f"SKIP: Awaiting human reply on issue #{item.task_number}.")
continue
elif item.task_type == "pr":
# Check if we're waiting for a human reply before acting on a PR
owner, repo_name = repo.split("/")
pr_comments = []
try:
pr_comments = self._client.get_pull_request_comments(owner, repo_name, item.task_number)
except Exception:
pass
if self._is_awaiting_reply(pr_comments):
logger.info(f"PR #{item.task_number}: agent asked a question and is awaiting a human reply. Skipping.")
results.append(f"SKIP: Awaiting human reply on PR #{item.task_number}.")
continue
for attempt in range(1, self._max_retries + 1):
try:
if item.task_type == "issue":
base_mission = self._build_issue_mission(item)
else:
base_mission = self._build_pr_mission(item)
if base_mission.startswith("SKIP:"):
logger.info(f"Skipping task #{item.task_number}: {base_mission}")
results.append(base_mission)
break
# Step 1: Planning Phase
logger.info(f"Starting Planning Phase for {item.task_type} #{item.task_number} (attempt {attempt})")
planning_mission = (
f"PHASE 1: PLANNING PHASE\n\n"
f"Your task is to analyze the repository structure and create a detailed implementation plan.\n"
f"Original Mission details:\n{base_mission}\n\n"
f"CRITICAL RULES:\n"
f"1. You are ONLY generating an implementation plan. DO NOT write files, DO NOT edit files, DO NOT commit, DO NOT push, and DO NOT create branches or PRs.\n"
f"2. Explore the codebase using read_file, list_files, grep_search, or run_command (for read-only queries like find/grep).\n"
f"3. Output your final plan clearly, describing the exact changes to be made and which files to modify.\n"
)
planning_agent = CodingAgent(self._model_name)
plan = await planning_agent.run_with_tools(planning_mission, planning_tools)
logger.info(f"Generated Plan:\n{plan}")
# Step 2: Coding Phase
logger.info(f"Starting Execution/Coding Phase for {item.task_type} #{item.task_number} (attempt {attempt})")
coding_mission = (
f"PHASE 2: EXECUTION/CODING PHASE\n\n"
f"You must now implement the changes based on the following plan generated in Phase 1:\n"
f"--- PLAN ---\n{plan}\n--- PLAN END ---\n\n"
f"Original Mission details:\n{base_mission}\n\n"
f"Follow the repository workflow to make the changes, verify them, commit, push, and create a PR.\n"
)
coding_agent = CodingAgent(self._model_name)
response = await coding_agent.run_with_tools(coding_mission, coding_tools_list)
logger.info(f"Agent response for {item.task_type} #{item.task_number}: {response}")
results.append(response)
break
except Exception as e:
logger.error(f"Error processing {item.task_type} #{item.task_number} (attempt {attempt}/{self._max_retries}): {e}")
if attempt == self._max_retries:
results.append(f"FAILED after {self._max_retries} attempts: {str(e)}")
finally:
if changed_dir:
os.chdir(original_cwd)
return results
def _build_issue_mission(self, item: WorkItem) -> str:
issue_info = item.task_info
assert isinstance(issue_info, IssueModel)
repo_full_name: str = item.repo_full_name
issue_number: int = item.task_number
issue_body: str = issue_info.body or "No description provided."
issue_labels: list[str] = [lbl.name for lbl in issue_info.labels]
issue_user: str = issue_info.user.login if issue_info.user else "unknown"
issue_created: str = issue_info.created_at or "unknown"
title: str = issue_info.title
owner: str = repo_full_name.split("/")[0]
repo_name: str = repo_full_name.split("/")[1]
comments: list[CommentModel] = []
try:
comments = self._client.get_issue_comments(owner, repo_name, issue_number)
except Exception:
pass
labels_str: str = f"Labels: {', '.join(issue_labels)}" if issue_labels else "Labels: none"
comments_str: str = "\n".join([
f"- @{c.user.login} ({c.created_at}): {c.body}"
for c in comments
]) if comments else "No comments yet."
clean_title = re.sub(r'[^a-zA-Z0-9\s-]', '', title).strip().lower()
title_words = clean_title.split()[:5]
desc_suffix = "-".join(title_words)
if not desc_suffix:
desc_suffix = "fix-issue"
branch_name: str = f"fix/issue-{issue_number}-{desc_suffix}"
workspace = WorkspaceManager()
repo_path = workspace.get_repo_path(repo_full_name)
return (
f"Your mission is to resolve issue #{issue_number} in {repo_full_name}.\n\n"
f"Issue: {title}\n"
f"Author: @{issue_user} (created {issue_created})\n"
f"{labels_str}\n\n"
f"Description:\n{issue_body}\n\n"
f"Comments ({len(comments)}):\n{comments_str}\n\n"
f"Branch name: {branch_name}.\n\n"
"BEFORE WRITING ANY CODE:\n"
" - Search online for relevant documentation, known solutions, library APIs, and platform-specific behavior.\n"
" - If ANY part of the issue is unclear, ambiguous, or has multiple valid approaches:\n"
" → Post a comment on the issue using `add_comment_to_issue` with your specific question(s).\n"
" → List the approaches you are considering.\n"
" → End the comment with the marker: <!-- agent:awaiting-reply --> on its own line.\n"
" → STOP. Do NOT proceed until a human replies. The system will re-dispatch you once a human responds.\n"
" - Never assume or guess. Always prefer asking over guessing.\n\n"
"CRITICAL INSTRUCTIONS:\n"
f"1. The repo is already cloned locally at '{repo_path}'. DO NOT create a new repository.\n"
f" The repository directory is your current working directory (cwd). You can verify this via the `get_working_directory` tool.\n"
"2. Always start from master: `git checkout master && git pull origin master`\n"
"3. Branch from master: `git checkout -b <type>/issue-<number>-<descriptive-name>`\n"
" Branch names MUST include a descriptive name (words/hyphens), not just the issue number.\n"
" Types: feat, fix, chore, docs, style, refactor, test, build, ci, perf\n"
"4. Use `edit_file`/`write_file` for code changes, then `git add` and `git commit` via `run_command`.\n"
"5. Push: `git push origin <branch>` via `run_command`.\n"
"6. Create PR: Use the `create_pull_request` tool (do NOT use Gitea's `tea` CLI or GitHub's `gh` CLI in run_command, as they can hang/freeze interactively).\n"
" PR description MUST include the Gitea automation template with 'closes #<ISSUE>'.\n"
"7. IMPORTANT: After successfully creating the pull request, you MUST comment on the issue (using the `add_comment_to_issue` tool) with the PR number, PR link, and summary.\n"
"8. Check AGENTS.md in repo root for project conventions and verification steps.\n"
"9. Grade severity: Critical/High = fix, Medium = review/fix, Low = skip.\n"
"An issue is DONE when the connected PR is merged (you cannot merge yourself).\n"
"DO NOT edit .git files unless explicitly resolving a git issue.\n"
"DO NOT work on non-meeks organization repos."
)
def _build_pr_mission(self, item: WorkItem) -> str:
pr_info = item.task_info
assert isinstance(pr_info, PullRequestModel)
repo_full_name: str = item.repo_full_name
pr_number: int = item.task_number
owner: str = repo_full_name.split("/")[0]
repo_name: str = repo_full_name.split("/")[1]
pr_model = self._client.get_pull_request(owner, repo_name, pr_number)
pr_details: str = pr_model.model_dump_json(indent=2)
pr_diff: str = ""
try:
pr_diff = self._client.get_pull_request_diff(owner, repo_name, pr_number)
except Exception as e:
logger.warning(f"Could not fetch PR diff for #{pr_number}: {e}")
pr_diff = f"Error fetching diff: {e}"
pr_files: list[PullRequestFileModel] = []
try:
pr_files = self._client.get_pull_request_files(owner, repo_name, pr_number)
except Exception:
pass
files_summary: str = "\n".join([f"- {f.filename}" for f in pr_files]) if pr_files else "No files available."
comments: list[CommentModel] = []
try:
comments = self._client.get_pull_request_comments(owner, repo_name, pr_number)
if not isinstance(comments, list):
comments = []
except Exception:
pass
reviews: list[dict[str, Any]] = []
try:
reviews = self._client.get_pr_reviews(owner, repo_name, pr_number)
if not isinstance(reviews, list):
reviews = []
except Exception:
pass
ai_username = "meeks-ai"
try:
user = self._client.get_authenticated_user()
if user:
ai_username = user.login
except Exception:
pass
# Create a combined, sorted timeline of timeline comments and reviews
timeline: list[dict[str, Any]] = []
for c in comments:
timeline.append({
"timestamp": c.created_at or "",
"user": c.user.login,
"type": "comment",
"body": c.body,
"by_ai": "Reviewed by AI Agent" in c.body or c.user.login == ai_username
})
for r in reviews:
r_user = (r.get("user") or {}).get("login", "unknown")
r_body = r.get("body", "")
r_state = r.get("state", "")
timeline.append({
"timestamp": r.get("submitted_at") or r.get("updated_at") or "",
"user": r_user,
"type": "review",
"body": f"[{r_state}] {r_body}",
"by_ai": r_user == ai_username
})
timeline.sort(key=lambda x: x["timestamp"])
last_action_by_ai = False
if timeline:
last_action_by_ai = timeline[-1]["by_ai"]
pr_author: str = pr_info.user.login if pr_info.user else "unknown"
is_own_pr = (pr_author == ai_username)
# Skip if the latest action is already by AI (waiting for human turn)
if last_action_by_ai:
logger.info(f"PR #{pr_number} already addressed by AI. Skipping.")
return f"SKIP: PR #{pr_number} has already been addressed by AI. No new action needed."
comments_str: str = "\n".join([
f"- @{c.user.login} ({c.created_at}): {c.body}"
for c in comments
]) if comments else "No comments yet."
reviews_str: str = "\n".join([
f"- @{(r.get('user') or {}).get('login')} ({r.get('submitted_at')}): [{r.get('state')}] {r.get('body')}"
for r in reviews
]) if reviews else "No reviews yet."
connected_issues_ctx = ""
pr_body = pr_info.body or ""
linked_issues = self._find_issues_for_pr(pr_body)
if linked_issues:
issues_details = []
for issue_num in linked_issues:
try:
issue = self._client.get_issue(owner, repo_name, issue_num)
issue_comments = self._client.get_issue_comments(owner, repo_name, issue_num)
comments_list = "\n".join([
f" - @{c.user.login} ({c.created_at}): {c.body}"
for c in issue_comments
]) if issue_comments else " No comments yet."
issues_details.append(
f"### Connected Issue #{issue_num}: {issue.title}\n"
f"Author: @{issue.user.login} (created {issue.created_at})\n"
f"Description:\n{issue.body or 'No description'}\n"
f"Discussion:\n{comments_list}"
)
except Exception as e:
logger.warning(f"Could not fetch connected issue #{issue_num}: {e}")
if issues_details:
connected_issues_ctx = "\n---\n\n## 📋 CONNECTED ISSUE CONTEXT\n" + "\n\n".join(issues_details)
workspace = WorkspaceManager()
repo_path = workspace.get_repo_path(repo_full_name)
pr_head_branch: str = pr_info.head.get('ref', 'unknown') if pr_info.head else "unknown"
pr_base_branch: str = pr_info.base.get('ref', 'unknown') if pr_info.base else "unknown"
pr_state: str = pr_info.state
pr_created: str = pr_info.created_at or "unknown"
# Dynamically determine instructions based on ownership/comments
is_fixing_pr = is_own_pr or any(r.get("state") == "REQUEST_CHANGES" for r in reviews)
if is_fixing_pr:
instructions = (
f" Note: The repository is located locally at '{repo_path}'. This repository directory is your current working directory (cwd). You can verify this via the `get_working_directory` tool.\n"
f" Your task is to FIX/UPDATE this PR by addressing comments/change requests.\n"
f" DO NOT create a new branch or PR. Follow this exact workflow:\n"
f" 1. Checkout the PR's head branch: `git checkout {pr_head_branch}`\n"
f" 2. Implement the requested fixes or changes on this branch.\n"
f" 3. Verify your fixes and run verification/tests.\n"
f" 4. Commit and push the changes directly: `git add <files> && git commit -m \"fix: address feedback\" && git push origin {pr_head_branch}`\n"
f" 5. After pushing, comment on the PR (using the `add_comment` tool) with a summary of the fixes implemented."
)
else:
instructions = (
f" Note: The repository is located locally at '{repo_path}'. This repository directory is your current working directory (cwd). You can verify this via the `get_working_directory` tool.\n"
" CRITICAL: You are ONLY reviewing this PR. DO NOT edit files, DO NOT make commits, DO NOT push branches, and DO NOT create any new PRs.\n"
"1. Read the PR diff carefully.\n"
"2. Analyze the changes for correctness, quality, and potential issues.\n"
"3. Check for: code quality, security issues, edge cases, test coverage.\n"
"4. If the PR is good: approve it (using approve_pull_request tool) with a meaningful comment.\n"
"5. If the PR has issues: request changes (using request_changes tool) with specific feedback.\n"
"6. Post your review comment on the PR (using add_comment tool).\n"
"IMPORTANT: Never merge the PR yourself - that is handled by humans."
)
return (
f"Your mission is to process PR #{pr_number} in {repo_full_name}.\n\n"
f"PR: {pr_info.title}\n"
f"Author: @{pr_author}\n"
f"Branch: {pr_head_branch}{pr_base_branch}\n"
f"State: {pr_state} (created {pr_created})\n\n"
f"Description:\n{pr_info.body or 'No description'}\n\n"
f"Files Changed ({len(pr_files)}):\n{files_summary}\n\n"
f"Timeline Comments:\n{comments_str}\n\n"
f"Reviews:\n{reviews_str}\n\n"
f"{connected_issues_ctx}\n\n"
"BEFORE MAKING ANY CHANGES:\n"
" - Search online for any technology, API, or behavior you are not 100% certain about.\n"
" - Read ALL review comments and change requests carefully.\n"
" - If any review comment is ambiguous or unclear:\n"
" → Post a clarifying comment on the PR (using the `add_comment` tool) with your specific question(s).\n"
" → End the comment with the marker: <!-- agent:awaiting-reply --> on its own line.\n"
" → STOP. Do NOT implement anything until a human replies. The system will re-dispatch you once a human responds.\n"
" - Never assume or guess what a reviewer meant. Always prefer asking over guessing.\n\n"
f"Instructions:\n{instructions}"
)
+69
View File
@@ -0,0 +1,69 @@
from gitea.client import GiteaClient
from core.interfaces import (
IssuesClient,
PullRequestsClient,
FilesClient,
RefsClient,
ReposClient,
)
from core.coding_agent import CodingAgent
from core.agent import CavemanAgent
from gitea.workspace import WorkspaceManager
class GiteaClientFactory:
"""Factory for creating Gitea client components with dependency injection support."""
@staticmethod
def create_full_client() -> GiteaClient:
return GiteaClient()
@staticmethod
def create_issues_client(client: GiteaClient | None = None) -> IssuesClient:
if client is None:
client = GiteaClient()
return client
@staticmethod
def create_prs_client(client: GiteaClient | None = None) -> PullRequestsClient:
if client is None:
client = GiteaClient()
return client
@staticmethod
def create_files_client(client: GiteaClient | None = None) -> FilesClient:
if client is None:
client = GiteaClient()
return client
@staticmethod
def create_refs_client(client: GiteaClient | None = None) -> RefsClient:
if client is None:
client = GiteaClient()
return client
@staticmethod
def create_repos_client(client: GiteaClient | None = None) -> ReposClient:
if client is None:
client = GiteaClient()
return client
class AgentFactory:
"""Factory for creating AI agent instances."""
@staticmethod
def create_coding_agent(model_name: str) -> CodingAgent:
return CodingAgent(model_name)
@staticmethod
def create_caveman_agent(model_name: str) -> CavemanAgent:
return CavemanAgent(model_name)
class WorkspaceFactory:
"""Factory for creating workspace manager instances."""
@staticmethod
def create_workspace() -> WorkspaceManager:
return WorkspaceManager()
+171
View File
@@ -0,0 +1,171 @@
"""Interfaces for Gitea operations."""
from abc import ABC, abstractmethod
from typing import Any
from gitea.models import (
IssueModel,
PullRequestModel,
CommentModel,
LabelModel,
UserModel,
RepositoryModel,
PullRequestFileModel,
)
class IssuesClient(ABC):
"""Interface for Gitea issue operations."""
@abstractmethod
def list_repo_issues(self, owner: str, repo: str, state: str = "open") -> list[IssueModel]: ...
@abstractmethod
def get_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel: ...
@abstractmethod
def close_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel: ...
@abstractmethod
def get_issue_comments(self, owner: str, repo: str, issue_number: int) -> list[CommentModel]: ...
@abstractmethod
def list_assigned_issues(self, owner: str, repo: str) -> list[IssueModel]: ...
@abstractmethod
def create_issue(
self,
owner: str,
repo: str,
title: str,
body: str,
labels: list[str] | None = None,
assignees: list[str] | None = None,
) -> IssueModel: ...
@abstractmethod
def add_comment(self, owner: str, repo: str, issue_number: int, body: str) -> CommentModel: ...
@abstractmethod
def add_label(self, owner: str, repo: str, issue_number: int, label: str) -> LabelModel: ...
class PullRequestsClient(ABC):
"""Interface for Gitea pull request operations."""
@abstractmethod
def list_repo_pull_requests(self, owner: str, repo: str, state: str = "open") -> list[PullRequestModel]: ...
@abstractmethod
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> PullRequestModel: ...
@abstractmethod
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> PullRequestModel: ...
@abstractmethod
def get_pull_request_comments(self, owner: str, repo: str, pull_number: int) -> list[CommentModel]: ...
@abstractmethod
def get_pr_reviews(self, owner: str, repo: str, pr_number: int) -> list[dict[str, Any]]: ...
@abstractmethod
def get_pull_request_diff(self, owner: str, repo: str, pull_number: int) -> str: ...
@abstractmethod
def get_pull_request_patch(self, owner: str, repo: str, pull_number: int) -> str: ...
@abstractmethod
def get_pull_request_files(self, owner: str, repo: str, pull_number: int) -> list[PullRequestFileModel]: ...
@abstractmethod
def list_assigned_pull_requests(self, owner: str, repo: str) -> list[PullRequestModel]: ...
@abstractmethod
def create_pull_request(
self, owner: str, repo: str, head: str, base: str, title: str, description: str = ""
) -> PullRequestModel: ...
@abstractmethod
def approve_pr(self, owner: str, repo: str, pr_number: int, comment: str) -> dict[str, Any]: ...
@abstractmethod
def request_changes_pr(self, owner: str, repo: str, pr_number: int, comment: str) -> dict[str, Any]: ...
@abstractmethod
def dismiss_review_pr(
self, owner: str, repo: str, pr_number: int, review_id: int, message: str
) -> dict[str, Any]: ...
@abstractmethod
def assign_issue(self, owner: str, repo: str, issue_number: int, username: str) -> IssueModel: ...
class FilesClient(ABC):
"""Interface for Gitea file/content operations."""
@abstractmethod
def get_file_content(self, owner: str, repo: str, path: str, ref: str = "master") -> str | list[str]: ...
@abstractmethod
def update_file(
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
) -> dict[str, Any]: ...
class RefsClient(ABC):
"""Interface for Gitea git ref operations."""
@abstractmethod
def update_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]: ...
@abstractmethod
def create_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]: ...
class ReposClient(ABC):
"""Interface for Gitea repository operations."""
@abstractmethod
def list_all_user_repos(self) -> list[RepositoryModel]: ...
class Agent(ABC):
"""Interface for AI agent operations."""
@abstractmethod
async def initialize(self) -> None: ...
@abstractmethod
async def run(self, user_input: str) -> str: ...
@abstractmethod
async def run_with_tools(self, user_input: str, tools: list[Any]) -> str: ...
class Workspace(ABC):
"""Interface for workspace management."""
@abstractmethod
def get_repo_path(self, repo_full_name: str) -> Any: ...
@abstractmethod
def sanitize_repo(self, repo_path: Any) -> None: ...
@abstractmethod
def clone_repo(self, repo_full_name: str, clone_url: str) -> Any: ...
class MissionBuilder(ABC):
"""Interface for mission string construction."""
@abstractmethod
def build_issue_mission(self, issue_info: dict[str, Any], branch_name: str) -> str: ...
@abstractmethod
def build_pr_mission(self, pr_info: dict[str, Any]) -> str: ...
class BranchStrategy(ABC):
"""Interface for branch creation strategy."""
@abstractmethod
async def create_or_reuse_branch(self, repo_path: Any, branch_name: str, base_branch: str | None = None) -> str: ...
+96
View File
@@ -0,0 +1,96 @@
"""Top-level coordinator: polls Gitea, queues work, dispatches to agent."""
import asyncio
import logging
from pathlib import Path
from typing import Any
from core.queue import WorkQueue, WorkItem
from core.dispatcher import AgentDispatcher
from gitea.client import GiteaClient
from gitea.models import IssueModel, PullRequestModel
from gitea.tools.gitea_tools import GiteaTools
from gitea.config import AGENT_MODEL_ID, AGENT_MAX_RETRIES
from gitea.workspace import WorkspaceManager
logger: logging.Logger = logging.getLogger("agent-orchestrator")
class AgentOrchestrator:
"""Top-level coordinator: polls Gitea, queues work, dispatches to agent."""
def __init__(
self,
client: GiteaClient,
tools: GiteaTools,
model_name: str = AGENT_MODEL_ID,
max_retries: int = AGENT_MAX_RETRIES,
) -> None:
self._client = client
self._tools = tools
self._model_name = model_name
self._work_queue = WorkQueue()
self._dispatcher = AgentDispatcher(client, tools, model_name, max_retries)
async def poll_and_dispatch(self) -> None:
"""Poll Gitea for tasks, enqueue them, and dispatch to agent."""
issues: list[IssueModel] = self._client.list_assigned_issues()
prs: list[PullRequestModel] = self._client.list_assigned_pull_requests()
if issues:
logger.info(f"Found {len(issues)} assigned issues")
self._enqueue_tasks("issue", issues)
else:
logger.info("No assigned issues found.")
if prs:
logger.info(f"Found {len(prs)} assigned PRs")
self._enqueue_tasks("pr", prs)
else:
logger.info("No assigned PRs found.")
if not self._work_queue.is_empty:
await self._process_work()
def _enqueue_tasks(self, task_type: str, tasks: list[IssueModel] | list[PullRequestModel]) -> None:
for task in tasks:
repo_full_name: str | None = task.repository.full_name if task.repository else None
task_number: int = task.number
if not repo_full_name or not task_number:
continue
item = WorkItem(
repo_full_name=repo_full_name,
task_type=task_type,
task_number=task_number,
task_info=task,
priority=0,
)
self._work_queue.enqueue(item)
logger.info(f"Enqueued {task_type} #{task_number} from {repo_full_name}")
async def _process_work(self) -> None:
"""Process all queued work, repo by repo."""
while not self._work_queue.is_empty:
repo: str | None = self._work_queue.get_next_repo()
if not repo:
break
work_items: list[WorkItem] = self._work_queue.get_repo_work(repo)
self._work_queue.remove_repo_work(repo)
# Ensure the workspace repository is cloned and sanitized
workspace = WorkspaceManager()
repo_path = workspace.get_repo_path(repo)
if not repo_path.exists():
workspace.clone_repo(repo)
logger.info(f"Cloned {repo} to {repo_path}")
else:
workspace.sanitize_repo(repo, repo_path)
logger.info(f"Sanitized existing repo at {repo_path}")
logger.info(f"Dispatching {len(work_items)} tasks for {repo}")
results: list[str] = await self._dispatcher.dispatch(repo, work_items)
for i, result in enumerate(results):
item = work_items[i]
logger.info(f"Completed {item.task_type} #{item.task_number}: {result[:200]}")
+20
View File
@@ -0,0 +1,20 @@
CAVEMAN_PROMPT = """
CAVEMAN SYSTEM PROMPT:
You are Caveman Agent.
Goal: Minimal tokens. Efficient work.
Rules:
1. Use fewest words possible.
2. Drop non-essential words (e.g., "the", "is", "a").
3. Use acronyms (PR, issue, repo, etc.).
4. If task simple, answer direct.
5. No preamble. No polite talk. Just work.
Example:
User: What is the status of PR #1?
Caveman: PR #1 open. Reviewing...
Follow instructions. Use tools. Execute tasks.
No extra chatter.
"""
+54
View File
@@ -0,0 +1,54 @@
from pydantic import BaseModel
from typing import Any
from gitea.models import IssueModel, PullRequestModel
class WorkItem(BaseModel):
repo_full_name: str
task_type: str # 'issue' or 'pr'
task_number: int
task_info: IssueModel | PullRequestModel
priority: int = 0
class WorkQueue:
"""Thread-safe work queue grouped by repo."""
def __init__(self) -> None:
self._queue: list[WorkItem] = []
self._enqueued_repos: set[str] = set()
def enqueue(self, item: WorkItem) -> None:
self._queue.append(item)
self._enqueued_repos.add(item.repo_full_name)
def enqueue_batch(self, items: list[WorkItem]) -> None:
for item in items:
self.enqueue(item)
def get_repo_work(self, repo: str) -> list[WorkItem]:
"""Get all work items for a specific repo."""
items: list[WorkItem] = [
item for item in self._queue if item.repo_full_name == repo
]
return items
def remove_repo_work(self, repo: str) -> None:
"""Remove all work items for a specific repo."""
self._queue = [
item for item in self._queue if item.repo_full_name != repo
]
self._enqueued_repos.discard(repo)
def get_next_repo(self) -> str | None:
"""Get the next repo with work, or None if empty."""
if not self._enqueued_repos:
return None
return next(iter(self._enqueued_repos))
@property
def is_empty(self) -> bool:
return len(self._queue) == 0
def __len__(self) -> int:
return len(self._queue)
+372
View File
@@ -0,0 +1,372 @@
import httpx
import json
import base64
from typing import Any
from .config import GITEA_URL, GITEA_TOKEN
from .models import (
UserModel,
LabelModel,
RepositoryModel,
IssueModel,
PullRequestModel,
CommentModel,
PullRequestFileModel,
)
from core.interfaces import (
IssuesClient,
PullRequestsClient,
FilesClient,
RefsClient,
ReposClient,
)
class GiteaClient(IssuesClient, PullRequestsClient, FilesClient, RefsClient, ReposClient):
"""HTTP client for Gitea API v1."""
def __init__(self) -> None:
self.base_url: str = GITEA_URL.rstrip("/")
self.headers: dict[str, str] = {
"Authorization": f"token {GITEA_TOKEN}",
"Accept": "application/json",
}
def get_authenticated_user(self) -> UserModel | None:
try:
with httpx.Client() as client:
response = client.get(f"{self.base_url}/api/v1/user", headers=self.headers)
response.raise_for_status()
return UserModel(**response.json())
except Exception as e:
print(f"Error getting authenticated user: {e}")
return None
def list_all_user_repos(self) -> list[RepositoryModel]:
try:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/user/repos"
response = client.get(url, headers=self.headers)
response.raise_for_status()
repos: list[dict[str, Any]] = response.json()
# Filter to ONLY meeks organization repos, include mirrors
seen: set[str] = set()
result: list[RepositoryModel] = []
for r in repos:
full_name = r.get("full_name", "")
if full_name and full_name not in seen and (r.get("owner") or {}).get("login") == "meeks":
seen.add(full_name)
result.append(RepositoryModel(**r))
return result
except Exception as e:
print(f"Error listing user repos: {e}")
return []
def list_repo_issues(self, owner: str, repo: str, state: str = "open") -> list[IssueModel]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?type=issues&state={state}"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return [IssueModel(**item) for item in response.json()]
def list_repo_pull_requests(self, owner: str, repo: str, state: str = "open") -> list[PullRequestModel]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls?state={state}"
response = client.get(url, headers=self.headers)
if response.status_code == 404:
return []
response.raise_for_status()
return [PullRequestModel(**item) for item in response.json()]
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> PullRequestModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return PullRequestModel(**response.json())
def get_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return IssueModel(**response.json())
def close_issue(self, owner: str, repo: str, issue_number: int) -> IssueModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
data: dict[str, str] = {"state": "closed"}
response = client.patch(url, headers=self.headers, json=data)
response.raise_for_status()
return IssueModel(**response.json())
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> PullRequestModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}"
data: dict[str, str] = {"state": "closed"}
response = client.patch(url, headers=self.headers, json=data)
response.raise_for_status()
return PullRequestModel(**response.json())
def get_issue_comments(self, owner: str, repo: str, issue_number: int) -> list[CommentModel]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return [CommentModel(**item) for item in response.json()]
def get_pull_request_comments(self, owner: str, repo: str, pull_number: int) -> list[CommentModel]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{pull_number}/comments"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return [CommentModel(**item) for item in response.json()]
def get_pull_request_diff(self, owner: str, repo: str, pull_number: int) -> str:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/diff"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return response.text
def get_pull_request_patch(self, owner: str, repo: str, pull_number: int) -> str:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/patch"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return response.text
def get_pull_request_files(self, owner: str, repo: str, pull_number: int) -> list[PullRequestFileModel]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pull_number}/files"
response = client.get(url, headers=self.headers)
response.raise_for_status()
return [PullRequestFileModel(**item) for item in response.json()]
def list_assigned_issues(self, owner: str = "", repo: str = "") -> list[IssueModel]:
try:
user = self.get_authenticated_user()
if not user:
return []
username: str = user.login
if owner and repo:
response = httpx.get(
f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues?assignee={username}&state=open&type=issues",
headers=self.headers,
)
response.raise_for_status()
return [IssueModel(**item) for item in response.json()]
all_issues: list[IssueModel] = []
repos = self.list_all_user_repos()
for r in repos:
repo_owner = r.owner if hasattr(r, 'owner') else (r.get("owner") or {}).get("login", "")
repo_name = r.name if hasattr(r, 'name') else r.get("name", "")
resp = httpx.get(
f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/issues?assignee={username}&state=open&type=issues",
headers=self.headers,
)
if resp.status_code == 200:
for item in resp.json():
issue = IssueModel(**item)
# Backfill repository if Gitea omitted it
if issue.repository is None:
issue = issue.model_copy(update={"repository": r})
all_issues.append(issue)
return all_issues
except Exception as e:
print(f"DEBUG: list_assigned_issues error: {e}")
return []
def list_assigned_pull_requests(self, owner: str = "", repo: str = "") -> list[PullRequestModel]:
"""List all pull requests assigned to or authored by the authenticated user."""
try:
user = self.get_authenticated_user()
if not user:
return []
username: str = user.login
if owner and repo:
response = httpx.get(
f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls?state=open",
headers=self.headers,
)
if response.status_code == 404:
return []
response.raise_for_status()
all_prs: list[PullRequestModel] = [PullRequestModel(**pr) for pr in response.json()]
return [
pr for pr in all_prs
if (pr.assignee and pr.assignee.login == username) or (pr.user and pr.user.login == username)
]
all_prs: list[PullRequestModel] = []
repos = self.list_all_user_repos()
for r in repos:
repo_owner = r.owner if hasattr(r, 'owner') else (r.get("owner") or {}).get("login", "")
repo_name = r.name if hasattr(r, 'name') else r.get("name", "")
resp = httpx.get(
f"{self.base_url}/api/v1/repos/{repo_owner}/{repo_name}/pulls?state=open",
headers=self.headers,
)
if resp.status_code == 200:
for pr_data in resp.json():
pr = PullRequestModel(**pr_data)
if (pr.assignee and pr.assignee.login == username) or (pr.user and pr.user.login == username):
# Backfill repository if Gitea omitted it
if pr.repository is None:
pr = pr.model_copy(update={"repository": r})
all_prs.append(pr)
return all_prs
except Exception as e:
print(f"DEBUG: list_assigned_pull_requests error: {e}")
return []
def create_pull_request(
self, owner: str, repo: str, head: str, base: str, title: str, description: str = ""
) -> PullRequestModel:
try:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls"
data: dict[str, str] = {
"title": title,
"body": description,
"head": head,
"base": base,
}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return PullRequestModel(**response.json())
except Exception as e:
print(f"Error creating pull request: {e}")
raise
def create_pr_via_tea(
self, owner: str, repo: str, title: str, description: str, head: str, base: str
) -> PullRequestModel:
return self.create_pull_request(owner, repo, head, base, title, description)
def approve_pr(self, owner: str, repo: str, pr_number: int, comment: str) -> dict[str, Any]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
data: dict[str, Any] = {"event": "APPROVED", "body": comment}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def request_changes_pr(self, owner: str, repo: str, pr_number: int, comment: str) -> dict[str, Any]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
data: dict[str, Any] = {"event": "REQUEST_CHANGES", "body": comment}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def get_pr_reviews(self, owner: str, repo: str, pr_number: int) -> list[dict[str, Any]]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews"
response = client.get(url, headers=self.headers)
if response.status_code == 404:
return []
response.raise_for_status()
return response.json()
def dismiss_review_pr(
self, owner: str, repo: str, pr_number: int, review_id: int, message: str
) -> dict[str, Any]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}/reviews/{review_id}/dismissals"
data: dict[str, str] = {"message": message}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def assign_issue(self, owner: str, repo: str, issue_number: int, username: str) -> IssueModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}"
data: dict[str, list[str]] = {"assignees": [username]}
response = client.patch(url, headers=self.headers, json=data)
response.raise_for_status()
return IssueModel(**response.json())
def create_issue(
self,
owner: str,
repo: str,
title: str,
body: str,
labels: list[str] | None = None,
assignees: list[str] | None = None,
) -> IssueModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues"
data: dict[str, Any] = {"title": title, "body": body}
if labels:
data["labels"] = labels
if assignees:
data["assignees"] = assignees
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return IssueModel(**response.json())
def add_comment(self, owner: str, repo: str, issue_number: int, body: str) -> CommentModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/comments"
data: dict[str, str] = {"body": body}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return CommentModel(**response.json())
def add_label(self, owner: str, repo: str, issue_number: int, label: str) -> LabelModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{issue_number}/labels"
data: list[str] = [label]
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return LabelModel(**response.json())
def add_label_pr(self, owner: str, repo: str, pr_number: int, label: str) -> LabelModel:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/issues/{pr_number}/labels"
data: list[str] = [label]
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return LabelModel(**response.json())
def update_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/git/ref/{ref}"
data: dict[str, str] = {"sha": sha}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def create_ref(self, owner: str, repo: str, ref: str, sha: str) -> dict[str, Any]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/git/refs"
data: dict[str, str] = {"ref": ref, "sha": sha}
response = client.post(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def update_file(
self, owner: str, repo: str, path: str, message: str, content: str, branch: str
) -> dict[str, Any]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
data: dict[str, str] = {
"message": message,
"content": base64.b64encode(content.encode()).decode(),
"branch": branch,
"new_branch": f"{branch}-update-{path}",
}
response = client.put(url, headers=self.headers, json=data)
response.raise_for_status()
return response.json()
def get_file_content(self, owner: str, repo: str, path: str, ref: str = "master") -> str | list[str]:
with httpx.Client() as client:
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
params: dict[str, str] = {"ref": ref}
response = client.get(url, headers=self.headers, params=params)
response.raise_for_status()
data = response.json()
if isinstance(data, list):
return [item.get("content", "") for item in data if item.get("type") == "file"]
return base64.b64decode(data.get("content", "")).decode() if data.get("content") else ""
+34
View File
@@ -0,0 +1,34 @@
"""Configuration for the coding agent."""
from dotenv import load_dotenv
from pydantic import Field
from pydantic_settings import BaseSettings
load_dotenv()
class AgentSettings(BaseSettings):
gitea_url: str = ""
gitea_token: str = ""
gitea_repos_root: str = ""
agent_model_id: str = "qwen/qwen3.6-35b-a3b"
agent_max_retries: int = 2
def get_settings() -> AgentSettings:
return AgentSettings()
# Module-level singleton instance
_agent_settings: AgentSettings = AgentSettings()
# Backwards-compatible exports
GITEA_URL: str = _agent_settings.gitea_url
GITEA_TOKEN: str = _agent_settings.gitea_token
GITEA_REPOS_ROOT: str = _agent_settings.gitea_repos_root
AGENT_MODEL_ID: str = _agent_settings.agent_model_id
AGENT_MAX_RETRIES: int = _agent_settings.agent_max_retries
import os
os.environ["GITEA_SERVER_URL"] = GITEA_URL
os.environ["GITEA_SERVER_TOKEN"] = GITEA_TOKEN
+112
View File
@@ -0,0 +1,112 @@
"""Pydantic models for Gitea API entities."""
from typing import Optional, Any
from pydantic import BaseModel, Field, field_validator
class UserModel(BaseModel):
login: str = ""
id: int = 0
avatar_url: Optional[str] = None
html_url: Optional[str] = None
full_name: Optional[str] = None
email: Optional[str] = None
username: Optional[str] = None
class LabelModel(BaseModel):
id: int = 0
name: str = ""
color: Optional[str] = None
description: Optional[str] = None
class RepositoryModel(BaseModel):
id: int = 0
name: str = ""
full_name: str = ""
owner: str = ""
html_url: Optional[str] = None
description: Optional[str] = None
mirror: bool = False
private: bool = False
fork: bool = False
parent: Optional["RepositoryModel"] = None
empty: Optional[bool] = None
@field_validator("owner", mode="before")
@classmethod
def validate_owner(cls, v):
if isinstance(v, dict):
return v.get("login", "")
return v
class IssueModel(BaseModel):
id: int = 0
number: int = 0
title: str = ""
body: Optional[str] = None
state: str = ""
user: UserModel = Field(default_factory=UserModel)
assignee: Optional[UserModel] = None
labels: list[LabelModel] = Field(default_factory=list)
created_at: Optional[str] = None
updated_at: Optional[str] = None
closed_at: Optional[str] = None
repository: Optional[RepositoryModel] = None
comments: int = 0
class PullRequestModel(BaseModel):
id: int = 0
number: int = 0
title: str = ""
body: Optional[str] = None
state: str = ""
user: UserModel = Field(default_factory=UserModel)
assignee: Optional[UserModel] = None
created_at: Optional[str] = None
updated_at: Optional[str] = None
closed_at: Optional[str] = None
merged_at: Optional[str] = None
head: dict[str, Any] = Field(default_factory=dict)
base: dict[str, Any] = Field(default_factory=dict)
repository: Optional[RepositoryModel] = None
comments: int = 0
comments_url: Optional[str] = None
diff_url: Optional[str] = None
patch_url: Optional[str] = None
html_url: Optional[str] = None
merged: bool = False
class CommentModel(BaseModel):
id: int = 0
body: str = ""
user: UserModel = Field(default_factory=UserModel)
created_at: Optional[str] = None
updated_at: Optional[str] = None
pull_request_url: Optional[str] = None
class PullRequestFileModel(BaseModel):
filename: str = ""
status: str = ""
additions: int = 0
deletions: int = 0
changes: int = 0
blob_url: Optional[str] = None
raw_url: Optional[str] = None
patch: Optional[str] = None
class GiteaConfig(BaseModel):
model_config = {"extra": "allow", "populate_by_name": True}
base_url: str
token: str
repos_root: str
model_id: str = "qwen/qwen3.6-35b-a3b"
max_retries: int = 2
+1
View File
@@ -0,0 +1 @@
"""Gitea tools packages."""
+208
View File
@@ -0,0 +1,208 @@
"""Tools for a coding agent to interact with the filesystem and environment."""
import os
import subprocess
from typing import Any
class CodingTools:
"""Tools for a coding agent to interact with the filesystem and environment."""
def __init__(self, repo_path: str | None = None) -> None:
self.repo_path: str = repo_path or os.getcwd()
def get_working_directory(self) -> str:
"""Get the absolute path of the current local repository workspace directory."""
return self.repo_path
def _resolve_path(self, path: str) -> str:
"""Resolve a path relative to self.repo_path."""
if os.path.isabs(path):
return path
return os.path.abspath(os.path.join(self.repo_path, path))
def list_files(self, path: str = ".") -> str:
"""List all files and directories at the given path (relative to repo root or absolute)."""
resolved: str = self._resolve_path(path)
try:
items: list[str] = os.listdir(resolved)
return "\n".join(items)
except Exception as e:
return f"Error listing files: {str(e)}"
def read_file(self, path: str, offset: int = 1, limit: int = 250) -> str:
"""Read lines from a file, starting at line offset (1-indexed), up to limit lines. Default limit is 250 lines to prevent token bloat. Use 'offset' to scroll/page through larger files."""
resolved: str = self._resolve_path(path)
try:
with open(resolved, 'r', encoding='utf-8', errors='replace') as f:
lines: list[str] = f.readlines()
start_line: int = offset - 1
end_line: int = offset + limit - 1
content_lines: list[str] = lines[start_line:end_line]
if not content_lines:
return "File is empty or offset out of bounds."
formatted_lines: list[str] = [f"{i + 1}: {line}" for i, line in enumerate(content_lines, start=start_line)]
return "\n".join(formatted_lines)
except Exception as e:
return f"Error reading file: {str(e)}"
def write_file(self, path: str, content: str) -> str:
"""Write content to a file, creating directories as needed."""
resolved: str = self._resolve_path(path)
try:
os.makedirs(os.path.dirname(os.path.abspath(resolved)), exist_ok=True)
with open(resolved, 'w', encoding='utf-8') as f:
f.write(content)
return f"File {path} written successfully."
except Exception as e:
return f"Error writing file: {str(e)}"
def edit_file(self, path: str, old_content: str, new_content: str) -> str:
"""Replace occurrences of old_content with new_content in the file."""
resolved: str = self._resolve_path(path)
try:
with open(resolved, 'r', encoding='utf-8', errors='replace') as f:
content: str = f.read()
if old_content not in content:
return f"Error: The specified old content was not found in {path}."
new_content_full: str = content.replace(old_content, new_content)
with open(resolved, 'w', encoding='utf-8') as f:
f.write(new_content_full)
return f"File {path} edited successfully."
except Exception as e:
return f"Error editing file: {str(e)}"
def _parse_verification_commands(self) -> list[str]:
agents_md: str = os.path.join(self.repo_path, "AGENTS.md")
if not os.path.exists(agents_md):
return []
try:
with open(agents_md, "r", encoding="utf-8") as f:
content = f.read()
except Exception:
return []
commands: list[str] = []
in_verification_section = False
in_code_block = False
current_block: list[str] = []
for line in content.splitlines():
line_lower = line.strip().lower()
if line.startswith("#"):
if "verification" in line_lower or "test" in line_lower:
in_verification_section = True
else:
in_verification_section = False
continue
if in_verification_section:
if line.strip().startswith("```"):
if in_code_block:
in_code_block = False
full_cmd = "\n".join(current_block).strip()
if full_cmd:
for cmd in full_cmd.splitlines():
if cmd.strip() and not cmd.strip().startswith("#"):
commands.append(cmd.strip())
current_block = []
else:
in_code_block = True
elif in_code_block:
current_block.append(line)
return commands
def run_verification(self) -> tuple[bool, str]:
commands = self._parse_verification_commands()
if not commands:
return True, "No verification commands found in AGENTS.md."
log_output = []
for cmd in commands:
log_output.append(f"Running: {cmd}")
try:
res = subprocess.run(
cmd, shell=True, cwd=self.repo_path,
capture_output=True, text=True, timeout=120
)
if res.returncode != 0:
log_output.append(
f"Command '{cmd}' failed with exit code {res.returncode}:\n"
f"Stdout:\n{res.stdout}\n"
f"Stderr:\n{res.stderr}"
)
return False, "\n".join(log_output)
log_output.append(res.stdout or "Success")
except subprocess.TimeoutExpired:
log_output.append(f"Command '{cmd}' timed out after 120 seconds.")
return False, "\n".join(log_output)
except Exception as e:
log_output.append(f"Failed to execute command '{cmd}': {e}")
return False, "\n".join(log_output)
return True, "\n".join(log_output)
def run_command(self, command: str, timeout: int = 120) -> str:
"""Execute a shell command in the repository workspace and return stdout and stderr output. Args: command, timeout (default 120 seconds)."""
if "tea pr create" in command:
success, log_msg = self.run_verification()
if not success:
return f"Verification failed! You cannot create a pull request because the tests/checks are failing:\n\n{log_msg}\n\nPlease fix the issues and try again."
try:
process: subprocess.Popen[str] = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.repo_path
)
stdout: str
stderr: str
try:
stdout, stderr = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
return f"Command timed out after {timeout} seconds.\n--- STDOUT ---\n{stdout}\n--- STDERR ---\n{stderr}"
output: str = ""
if stdout:
output += f"--- STDOUT ---\n{stdout}"
if stderr:
output += f"\n--- STDERR ---\n{stderr}"
if process.returncode != 0:
return f"Command failed with exit code {process.returncode}:\n{output}"
return output if output else "Command executed successfully (no output)."
except Exception as e:
return f"Error running command: {str(e)}"
def grep_search(self, pattern: str, path: str = ".") -> str:
"""Search for pattern in files under path using grep (case-insensitive)."""
resolved: str = self._resolve_path(path)
try:
command: str = f"grep -ri '{pattern}' {resolved}"
process: subprocess.Popen[str] = subprocess.Popen(
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=self.repo_path
)
stdout: str
stderr: str
try:
stdout, stderr = process.communicate(timeout=30)
except subprocess.TimeoutExpired:
process.kill()
stdout, stderr = process.communicate()
return f"Grep search timed out after 30 seconds.\nStdout: {stdout}\nStderr: {stderr}"
if process.returncode != 0 and not stdout:
return f"No matches found for '{pattern}'."
output: str = stdout
if stderr:
output += f"\nError: {stderr}"
return output
except Exception as e:
return f"Error during grep search: {str(e)}"
+41
View File
@@ -0,0 +1,41 @@
from typing import Any
from gitea.client import GiteaClient
class FileTools:
"""Tools for Gitea file/content operations."""
def __init__(self, client: GiteaClient) -> None:
self._client = client
def get_file_content(self, owner: str, repo: str, path: str) -> str:
try:
content = self._client.get_file_content(owner, repo, path)
if isinstance(content, list):
return "\n".join(content)
return content
except Exception as e:
return f"Error getting file content: {str(e)}"
def get_file_content_with_ref(self, owner: str, repo: str, path: str, ref: str = "master") -> str:
try:
content = self._client.get_file_content(owner, repo, path, ref)
if isinstance(content, list):
return "\n".join(content)
return content
except Exception as e:
return f"Error getting file content: {str(e)}"
def commit_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
try:
self._client.update_file(owner, repo, path, message, content, branch)
return f"File '{path}' committed successfully to {owner}/{repo}."
except Exception as e:
return f"Error committing file: {str(e)}"
def update_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
try:
self._client.update_file(owner, repo, path, message, content, branch)
return f"File '{path}' updated in {owner}/{repo}."
except Exception as e:
return f"Error updating file: {str(e)}"
+16
View File
@@ -0,0 +1,16 @@
from typing import Any
from gitea.client import GiteaClient
class GitTools:
"""Tools for Gitea git ref operations."""
def __init__(self, client: GiteaClient) -> None:
self._client = client
def create_branch(self, owner: str, repo: str, ref: str, sha: str) -> str:
try:
self._client.create_ref(owner, repo, ref, sha)
return f"Branch '{ref}' created successfully in {owner}/{repo}."
except Exception as e:
return f"Error creating branch: {str(e)}"
+122
View File
@@ -0,0 +1,122 @@
from gitea.tools.issue_tools import IssueTools
from gitea.tools.pr_tools import PRTools
from gitea.tools.file_tools import FileTools
from gitea.tools.git_tools import GitTools
from gitea.client import GiteaClient
class GiteaTools:
"""Facade for Gitea tool operations - delegates to focused tool classes."""
def __init__(self, client: GiteaClient) -> None:
self._client = client
self.issue_tools = IssueTools(client)
self.pr_tools = PRTools(client)
self.file_tools = FileTools(client)
self.git_tools = GitTools(client)
# ---- Issue operations (delegated) ----
def get_issue(self, owner: str, repo: str, issue_number: int) -> str:
"""Get the details of a specific issue. Args: owner (repo owner), repo (repo name), issue_number (issue ID)."""
return self.issue_tools.get_issue(owner, repo, issue_number)
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
"""Get the details of a specific pull request. Args: owner (repo owner), repo (repo name), pull_number (PR ID)."""
return self.pr_tools.get_pull_request(owner, repo, pull_number)
def close_issue(self, owner: str, repo: str, issue_number: int) -> str:
"""Close an issue. Args: owner (repo owner), repo (repo name), issue_number (issue ID)."""
return self.issue_tools.close_issue(owner, repo, issue_number)
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
"""Close a pull request. Args: owner (repo owner), repo (repo name), pull_number (PR ID)."""
return self.pr_tools.close_pull_request(owner, repo, pull_number)
def get_issue_comments(self, owner: str, repo: str, issue_number: int) -> str:
"""Get all comments on an issue. Args: owner (repo owner), repo (repo name), issue_number (issue ID)."""
return self.issue_tools.get_issue_comments(owner, repo, issue_number)
def get_pull_request_comments(self, owner: str, repo: str, pull_number: int) -> str:
"""Get all comments on a pull request. Args: owner (repo owner), repo (repo name), pull_number (PR ID)."""
return self.pr_tools.get_pull_request_comments(owner, repo, pull_number)
def list_assigned_issues(self) -> list[dict]:
"""List all issues assigned to the authenticated user across all repos."""
return self.issue_tools.list_assigned_issues()
def list_assigned_pull_requests(self) -> list[dict]:
"""List all pull requests assigned to the authenticated user across all repos."""
return self.pr_tools.list_assigned_pull_requests()
def list_issues(self, owner: str, repo: str, state: str = "open") -> str:
"""List issues in a repository. Args: owner (repo owner), repo (repo name), state (open, closed, or all)."""
return self.issue_tools.list_issues(owner, repo, state)
def list_pull_requests(self, owner: str, repo: str, state: str = "open") -> str:
"""List pull requests in a repository. Args: owner (repo owner), repo (repo name), state (open, closed, or all)."""
return self.pr_tools.list_pull_requests(owner, repo, state)
def get_file_content(self, owner: str, repo: str, path: str) -> str:
"""Get the content of a file from a repository. Args: owner (repo owner), repo (repo name), path (file path)."""
return self.file_tools.get_file_content(owner, repo, path)
def create_pull_request(self, owner: str, repo: str, head: str, base: str, title: str, description: str = "") -> str:
"""Create a new pull request. Args: owner, repo, head (source branch), base (target branch), title, description."""
return self.pr_tools.create_pull_request(owner, repo, head, base, title, description)
def create_issue(self, owner: str, repo: str, title: str, body: str, labels: list[str] | None = None, assignees: list[str] | None = None) -> str:
"""Create a new issue. Args: owner, repo, title, body, labels (optional), assignees (optional)."""
return self.issue_tools.create_issue(owner, repo, title, body, labels, assignees)
def create_branch(self, owner: str, repo: str, ref: str, sha: str) -> str:
"""Create a new branch in a repository. Args: owner, repo, ref (branch name), sha (commit SHA)."""
return self.git_tools.create_branch(owner, repo, ref, sha)
def commit_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
"""Commit a file to a repository. Args: owner, repo, path, message, content, branch."""
return self.file_tools.commit_file(owner, repo, path, message, content, branch)
def add_label_to_issue(self, owner: str, repo: str, issue_number: int, label: str) -> str:
"""Add a label to an issue. Args: owner, repo, issue_number, label."""
return self.issue_tools.add_label_to_issue(owner, repo, issue_number, label)
def add_label_to_pr(self, owner: str, repo: str, pr_number: int, label: str) -> str:
"""Add a label to a pull request. Args: owner, repo, pr_number, label."""
return self.pr_tools.add_label_to_pr(owner, repo, pr_number, label)
def add_comment_to_issue(self, owner: str, repo: str, issue_number: int, body: str) -> str:
"""Add a comment to an issue. Args: owner, repo, issue_number, body."""
return self.issue_tools.add_comment_to_issue(owner, repo, issue_number, body)
def get_pull_request_diff(self, owner: str, repo: str, pull_number: int) -> str:
"""Get the diff of a pull request. Args: owner, repo, pull_number."""
return self.pr_tools.get_pull_request_diff(owner, repo, pull_number)
def get_pull_request_patch(self, owner: str, repo: str, pull_number: int) -> str:
"""Get the patch of a pull request. Args: owner, repo, pull_number."""
return self.pr_tools.get_pull_request_patch(owner, repo, pull_number)
def approve_pull_request(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
"""Approve a pull request. Args: owner, repo, pull_number, comment."""
return self.pr_tools.approve_pull_request(owner, repo, pull_number, comment)
def request_changes(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
"""Request changes on a pull request. Args: owner, repo, pull_number, comment."""
return self.pr_tools.request_changes(owner, repo, pull_number, comment)
def add_comment(self, owner: str, repo: str, issue_number: int, body: str) -> str:
"""Add a comment to an issue. Args: owner, repo, issue_number, body."""
return self.issue_tools.add_comment(owner, repo, issue_number, body)
def add_label(self, owner: str, repo: str, issue_number: int, label: str) -> str:
"""Add a label to an issue. Args: owner, repo, issue_number, label."""
return self.issue_tools.add_label(owner, repo, issue_number, label)
def get_file_content_with_ref(self, owner: str, repo: str, path: str, ref: str = "master") -> str:
"""Get file content with a specific git ref. Args: owner, repo, path, ref (branch/tag)."""
return self.file_tools.get_file_content_with_ref(owner, repo, path, ref)
def update_file(self, owner: str, repo: str, path: str, message: str, content: str, branch: str) -> str:
"""Update a file in a repository. Args: owner, repo, path, message, content, branch."""
return self.file_tools.update_file(owner, repo, path, message, content, branch)
+94
View File
@@ -0,0 +1,94 @@
"""Tools for Gitea issue operations."""
import json
from typing import Any
from gitea.client import GiteaClient
from gitea.models import IssueModel, CommentModel, LabelModel
class IssueTools:
"""Tools for Gitea issue operations."""
def __init__(self, client: GiteaClient) -> None:
self._client = client
def get_issue(self, owner: str, repo: str, issue_number: int) -> str:
try:
issue: IssueModel = self._client.get_issue(owner, repo, issue_number)
return issue.model_dump_json(indent=2)
except Exception as e:
return f"Error getting issue: {str(e)}"
def close_issue(self, owner: str, repo: str, issue_number: int) -> str:
try:
self._client.close_issue(owner, repo, issue_number)
return f"Issue #{issue_number} closed successfully."
except Exception as e:
return f"Error closing issue: {str(e)}"
def get_issue_comments(self, owner: str, repo: str, issue_number: int) -> str:
try:
comments: list[CommentModel] = self._client.get_issue_comments(owner, repo, issue_number)
return json.dumps([c.model_dump() for c in comments], indent=2)
except Exception as e:
return f"Error getting issue comments: {str(e)}"
def list_assigned_issues(self) -> list[dict[str, Any]]:
try:
repos = self._client.list_all_user_repos()
all_issues: list[dict[str, Any]] = []
for repo in repos:
owner = repo.owner
repo_name = repo.name
issues = self._client.list_assigned_issues(owner, repo_name)
if issues:
all_issues.extend([issue.model_dump() if hasattr(issue, 'model_dump') else issue for issue in issues])
return all_issues
except Exception as e:
print(f"DEBUG: list_assigned_issues error: {e}")
return []
def list_issues(self, owner: str, repo: str, state: str = "open") -> str:
try:
issues = self._client.list_repo_issues(owner, repo, state)
if not issues:
return f"No issues in {owner}/{repo}."
summary = [f"#{issue.number}: {issue.title}" for issue in issues]
return "\n".join(summary)
except Exception as e:
return f"Error listing issues: {str(e)}"
def create_issue(self, owner: str, repo: str, title: str, body: str, labels: list[str] | None = None, assignees: list[str] | None = None) -> str:
try:
issue = self._client.create_issue(owner, repo, title, body, labels, assignees)
return f"Issue #{issue.number} created successfully in {owner}/{repo}."
except Exception as e:
return f"Error creating issue: {str(e)}"
def add_label_to_issue(self, owner: str, repo: str, issue_number: int, label: str) -> str:
try:
self._client.add_label(owner, repo, issue_number, label)
return f"Label '{label}' added to issue #{issue_number}."
except Exception as e:
return f"Error adding label to issue #{issue_number}: {e}"
def add_comment_to_issue(self, owner: str, repo: str, issue_number: int, body: str) -> str:
try:
self._client.add_comment(owner, repo, issue_number, body)
return f"Comment added to issue #{issue_number}."
except Exception as e:
return f"Error adding comment to issue #{issue_number}: {e}"
def add_comment(self, owner: str, repo: str, issue_number: int, body: str) -> str:
try:
comment = self._client.add_comment(owner, repo, issue_number, body)
return f"Comment added to #{issue_number}."
except Exception as e:
return f"Error adding comment: {str(e)}"
def add_label(self, owner: str, repo: str, issue_number: int, label: str) -> str:
try:
self._client.add_label(owner, repo, issue_number, label)
return f"Label '{label}' added to #{issue_number}."
except Exception as e:
return f"Error adding label: {str(e)}"
+99
View File
@@ -0,0 +1,99 @@
"""Tools for Gitea pull request operations."""
import json
from typing import Any
from gitea.client import GiteaClient
from gitea.models import PullRequestModel, CommentModel
class PRTools:
"""Tools for Gitea pull request operations."""
def __init__(self, client: GiteaClient) -> None:
self._client = client
def get_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
try:
pr: PullRequestModel = self._client.get_pull_request(owner, repo, pull_number)
return pr.model_dump_json(indent=2)
except Exception as e:
return f"Error getting pull request: {str(e)}"
def close_pull_request(self, owner: str, repo: str, pull_number: int) -> str:
try:
self._client.close_pull_request(owner, repo, pull_number)
return f"Pull request #{pull_number} closed successfully."
except Exception as e:
return f"Error closing pull request: {str(e)}"
def get_pull_request_comments(self, owner: str, repo: str, pull_number: int) -> str:
try:
comments: list[CommentModel] = self._client.get_pull_request_comments(owner, repo, pull_number)
return json.dumps([c.model_dump() for c in comments], indent=2)
except Exception as e:
return f"Error getting PR comments: {str(e)}"
def list_assigned_pull_requests(self) -> list[dict[str, Any]]:
try:
repos = self._client.list_all_user_repos()
all_prs: list[dict[str, Any]] = []
for repo_info in repos:
repo_owner = repo_info.owner
repo_name = repo_info.name
prs = self._client.list_assigned_pull_requests(repo_owner, repo_name)
if prs:
all_prs.extend([pr.model_dump() if hasattr(pr, 'model_dump') else pr for pr in prs])
return all_prs
except Exception as e:
print(f"DEBUG: list_assigned_pull_requests error: {e}")
return []
def list_pull_requests(self, owner: str, repo: str, state: str = "open") -> str:
try:
prs = self._client.list_repo_pull_requests(owner, repo, state)
if not prs:
return f"No PRs in {owner}/{repo}."
summary = [f"#{pr.number}: {pr.title}" for pr in prs]
return "\n".join(summary)
except Exception as e:
return f"Error listing PRs: {str(e)}"
def create_pull_request(self, owner: str, repo: str, head: str, base: str, title: str, description: str = "") -> str:
try:
pr = self._client.create_pr_via_tea(owner, repo, title, description, head, base)
return pr.model_dump_json(indent=2)
except Exception as e:
return f"Error creating PR: {str(e)}"
def add_label_to_pr(self, owner: str, repo: str, pr_number: int, label: str) -> str:
try:
self._client.add_label_pr(owner, repo, pr_number, label)
return f"Label '{label}' added to PR #{pr_number}."
except Exception as e:
return f"Error adding label to PR #{pr_number}: {e}"
def get_pull_request_diff(self, owner: str, repo: str, pull_number: int) -> str:
try:
return self._client.get_pull_request_diff(owner, repo, pull_number)
except Exception as e:
return f"Error getting PR diff: {str(e)}"
def get_pull_request_patch(self, owner: str, repo: str, pull_number: int) -> str:
try:
return self._client.get_pull_request_patch(owner, repo, pull_number)
except Exception as e:
return f"Error getting PR patch: {str(e)}"
def approve_pull_request(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
try:
self._client.approve_pr(owner, repo, pull_number, comment)
return f"Approved PR #{pull_number}."
except Exception as e:
return f"Error approving PR: {str(e)}"
def request_changes(self, owner: str, repo: str, pull_number: int, comment: str) -> str:
try:
self._client.request_changes_pr(owner, repo, pull_number, comment)
return f"Requested changes on PR #{pull_number}."
except Exception as e:
return f"Error requesting changes: {str(e)}"
+132
View File
@@ -0,0 +1,132 @@
import os
import subprocess
from pathlib import Path
from urllib.parse import urlparse
from .config import GITEA_REPOS_ROOT, GITEA_URL, GITEA_TOKEN
class WorkspaceManager:
"""Manages local workspace for Gitea repositories."""
def __init__(self) -> None:
self.root_dir: Path = Path(GITEA_REPOS_ROOT).expanduser().resolve()
self.root_dir.mkdir(parents=True, exist_ok=True)
self._configure_git_credentials()
def _configure_git_credentials(self) -> None:
try:
# Unset any global configs we might have set previously
subprocess.run(
["git", "config", "--global", "--unset", "credential.helper"],
capture_output=True
)
subprocess.run(
["git", "config", "--global", "--unset", "user.name"],
capture_output=True
)
subprocess.run(
["git", "config", "--global", "--unset", "user.email"],
capture_output=True
)
except Exception as e:
print(f"Error unsetting global configs: {e}")
def _configure_repo_user(self, repo_path: Path) -> None:
try:
# Configure credential helper locally for the repo
subprocess.run(
["git", "-C", str(repo_path), "config", "credential.helper", "store"],
check=True, capture_output=True
)
# Write to ~/.git-credentials
parsed = urlparse(GITEA_URL.rstrip("/"))
cred_line = f"{parsed.scheme}://meeks-ai:{GITEA_TOKEN}@{parsed.netloc}\n"
cred_file = Path("~/.git-credentials").expanduser()
if cred_file.exists():
content = cred_file.read_text()
if cred_line.strip() not in content:
cred_file.write_text(content + cred_line)
else:
cred_file.write_text(cred_line)
from gitea.client import GiteaClient
client = GiteaClient()
user = client.get_authenticated_user()
if user:
name = user.full_name or user.login or "meeks-ai"
email = user.email or "micke_ingvarsson+ai@hotmail.com"
subprocess.run(
["git", "-C", str(repo_path), "config", "user.name", name],
check=True, capture_output=True
)
subprocess.run(
["git", "-C", str(repo_path), "config", "user.email", email],
check=True, capture_output=True
)
except Exception as e:
print(f"Error configuring local git user: {e}")
def get_repo_path(self, repo_full_name: str) -> Path:
parts: list[str] = repo_full_name.split("/")
return self.root_dir / parts[0] / parts[1]
def _get_authenticated_url(self, repo_full_name: str) -> str:
parsed = urlparse(GITEA_URL.rstrip("/"))
path = parsed.path.rstrip("/")
return f"{parsed.scheme}://{parsed.netloc}{path}/{repo_full_name}.git"
def sanitize_repo(self, repo_full_name: str, repo_path: Path) -> None:
try:
auth_url = self._get_authenticated_url(repo_full_name)
subprocess.run(
["git", "-C", str(repo_path), "remote", "set-url", "origin", auth_url],
check=True, capture_output=True,
)
self._configure_repo_user(repo_path)
subprocess.run(
["git", "-C", str(repo_path), "reset", "--hard", "HEAD"],
check=True, capture_output=True,
)
subprocess.run(
["git", "-C", str(repo_path), "clean", "-fdx"],
check=True, capture_output=True,
)
try:
subprocess.run(
["git", "-C", str(repo_path), "checkout", "main"],
check=True, capture_output=True,
)
except subprocess.CalledProcessError:
subprocess.run(
["git", "-C", str(repo_path), "checkout", "master"],
check=True, capture_output=True,
)
try:
subprocess.run(
["git", "-C", str(repo_path), "pull", "origin", "main"],
check=True, capture_output=True,
)
except subprocess.CalledProcessError:
subprocess.run(
["git", "-C", str(repo_path), "pull", "origin", "master"],
check=True, capture_output=True,
)
except Exception as e:
print(f"Error during sanitization: {e}")
def clone_repo(self, repo_full_name: str, clone_url: str | None = None) -> Path:
repo_path: Path = self.get_repo_path(repo_full_name)
if repo_path.exists():
if not (repo_path / ".git").exists():
new_path: Path = repo_path.parent / f"{repo_full_name.replace('/', '_')}_old"
if new_path.exists():
import shutil
shutil.rmtree(new_path)
repo_path.rename(new_path)
return repo_path
print(f"Cloning repository {repo_full_name} to {repo_path}...")
auth_url = self._get_authenticated_url(repo_full_name)
subprocess.run(["git", "clone", auth_url, str(repo_path)], check=True, capture_output=True)
self._configure_repo_user(repo_path)
return repo_path
+110
View File
@@ -0,0 +1,110 @@
import asyncio
import os
import time
import logging
from pathlib import Path
from dotenv import load_dotenv
from gitea.client import GiteaClient
from gitea.tools.gitea_tools import GiteaTools
from gitea.config import AGENT_MODEL_ID, AGENT_MAX_RETRIES
from core.orchestrator import AgentOrchestrator
import json
# Setup logging to logs folder
LOG_DIR: Path = Path(__file__).parent.parent.parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
LOG_FILE: Path = LOG_DIR / "agent.log"
class JSONFormatter(logging.Formatter):
"""Formats log records as JSON objects for structured logging."""
def format(self, record: logging.LogRecord) -> str:
log_data = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info:
log_data["exception"] = self.formatException(record.exc_info)
return json.dumps(log_data)
file_handler = logging.FileHandler(LOG_FILE)
file_handler.setFormatter(JSONFormatter())
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logging.basicConfig(
level=logging.INFO,
handlers=[file_handler, stream_handler]
)
logger: logging.Logger = logging.getLogger("coding-agent")
async def main() -> None:
load_dotenv()
# Ensure we're in the repo directory
repo_root: Path = Path(__file__).parent.parent.parent
if Path.cwd() != repo_root:
os.chdir(repo_root)
logger.info(f"Changed directory to {repo_root}")
# Initialize Gitea components
client: GiteaClient = GiteaClient()
tools: GiteaTools = GiteaTools(client)
model_name: str = AGENT_MODEL_ID
# Initialize orchestrator
orchestrator: AgentOrchestrator = AgentOrchestrator(client, tools, model_name, AGENT_MAX_RETRIES)
logger.info("--- Autonomous Coding Agent Active ---")
logger.info(f"Model: {model_name}")
logger.info(f"Max retries: {AGENT_MAX_RETRIES}")
logger.info(f"Checking Gitea at {client.base_url}")
logger.info("Press Ctrl+C to stop.")
consecutive_errors: int = 0
max_consecutive_errors: int = 5
while True:
try:
logger.info("=== Checking for pending tasks ===")
# Use orchestrator to poll and dispatch
await orchestrator.poll_and_dispatch()
# Reset error counter on successful run
consecutive_errors = 0
logger.info("=== Waiting 60 seconds before next check ===")
await asyncio.sleep(60)
except KeyboardInterrupt:
logger.info("Agent stopped.")
break
except Exception as e:
consecutive_errors += 1
logger.error(f"Error in main loop (attempt {consecutive_errors}/{max_consecutive_errors}): {e}")
# If too many consecutive errors, wait longer
if consecutive_errors >= max_consecutive_errors:
logger.error(f"Too many consecutive errors ({max_consecutive_errors}). Waiting 5 minutes before retry.")
await asyncio.sleep(300)
consecutive_errors = 0
else:
await asyncio.sleep(60)
if __name__ == "__main__":
asyncio.run(main())
def start_agent() -> None:
"""Entry point for uv run."""
asyncio.run(main())
+34
View File
@@ -0,0 +1,34 @@
[project]
name = "gitea-agent"
version = "0.1.0"
description = "An AI agent that automates Gitea tasks."
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"httpx>=0.28.1",
"python-dotenv>=1.0.0",
"openai>=1.0.0",
"pydantic>=2.13.4",
"pydantic-settings>=2.0.0",
"lmstudio>=1.5.0",
]
[project.scripts]
start-agent = "main:start_agent"
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
py-modules = ["main"]
[tool.setuptools.packages.find]
include = ["core", "gitea"]
[dependency-groups]
dev = [
"pytest>=9.1.1",
"ty>=0.0.55",
]
+1
View File
@@ -0,0 +1 @@
# Tests package.
+142
View File
@@ -0,0 +1,142 @@
import os
import json
import logging
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from pathlib import Path
from main import JSONFormatter
from gitea.tools.coding_tools import CodingTools
from core.dispatcher import AgentDispatcher
from core.queue import WorkItem
from gitea.client import GiteaClient
from gitea.tools.gitea_tools import GiteaTools
from gitea.models import IssueModel
pytestmark = pytest.mark.anyio
def test_json_formatter() -> None:
formatter = JSONFormatter()
record = logging.LogRecord(
name="test-logger",
level=logging.INFO,
pathname="test.py",
lineno=10,
msg="Test log message",
args=(),
exc_info=None
)
formatted = formatter.format(record)
data = json.loads(formatted)
assert data["level"] == "INFO"
assert data["logger"] == "test-logger"
assert data["message"] == "Test log message"
assert "timestamp" in data
def test_read_file_limit(tmp_path: Path) -> None:
# Create a large file
large_file = tmp_path / "large_file.txt"
lines = [f"Line {i}\n" for i in range(1, 400)]
large_file.write_text("".join(lines), encoding="utf-8")
coding_tools = CodingTools(str(tmp_path))
# Test read with default limit (250)
result = coding_tools.read_file("large_file.txt", offset=1)
result_lines = [line.strip() for line in result.splitlines() if line.strip()]
assert len(result_lines) == 250
assert result_lines[0] == "1: Line 1"
assert result_lines[-1] == "250: Line 250"
# Test read with custom limit
result_custom = coding_tools.read_file("large_file.txt", offset=10, limit=10)
result_custom_lines = [line.strip() for line in result_custom.splitlines() if line.strip()]
assert len(result_custom_lines) == 10
assert result_custom_lines[0] == "10: Line 10"
assert result_custom_lines[-1] == "19: Line 19"
def test_parse_verification_commands(tmp_path: Path) -> None:
agents_md = tmp_path / "AGENTS.md"
agents_md.write_text(
"# Agent Instructions\n\n"
"## Verification\n"
"Please verify your changes with:\n"
"```bash\n"
"pytest -v\n"
"npm run lint\n"
"```\n\n"
"## Something Else\n"
"```bash\n"
"ignored command\n"
"```\n",
encoding="utf-8"
)
coding_tools = CodingTools(str(tmp_path))
commands = coding_tools._parse_verification_commands()
assert commands == ["pytest -v", "npm run lint"]
def test_run_verification_failure(tmp_path: Path) -> None:
agents_md = tmp_path / "AGENTS.md"
agents_md.write_text(
"## Verification\n"
"```bash\n"
"false\n"
"```\n",
encoding="utf-8"
)
coding_tools = CodingTools(str(tmp_path))
success, log_msg = coding_tools.run_verification()
assert not success
assert "false" in log_msg
@patch("core.dispatcher.CodingAgent")
async def test_dispatch_planning_and_coding_phases(mock_agent_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
# Mock no existing PRs
mock_client.list_repo_pull_requests.return_value = []
mock_client.get_issue_comments.return_value = []
# Mock CodingAgent instances
mock_planning_agent = MagicMock()
mock_planning_agent.run_with_tools = AsyncMock(return_value="Plan: Modify file A")
mock_coding_agent = MagicMock()
mock_coding_agent.run_with_tools = AsyncMock(return_value="PR #1 Created")
mock_agent_class.side_effect = [mock_planning_agent, mock_coding_agent]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
task_number=42,
task_info=IssueModel(number=42, title="fix bug", body="bug details"),
priority=0
)
# We mock os.path.isdir to return True so os.chdir won't fail or crash in test
with patch("os.path.isdir", return_value=True), patch("os.chdir"):
results = await dispatcher.dispatch("meeks/repo1", [work_item])
assert len(results) == 1
assert results[0] == "PR #1 Created"
# Verify both agents ran
assert mock_planning_agent.run_with_tools.call_count == 1
assert mock_coding_agent.run_with_tools.call_count == 1
# Verify planning prompt was passed correct context
planning_call_args = mock_planning_agent.run_with_tools.call_args[0]
assert "PHASE 1: PLANNING PHASE" in planning_call_args[0]
# Verify coding prompt received the generated plan
coding_call_args = mock_coding_agent.run_with_tools.call_args[0]
assert "PHASE 2: EXECUTION/CODING PHASE" in coding_call_args[0]
assert "Plan: Modify file A" in coding_call_args[0]
+95
View File
@@ -0,0 +1,95 @@
from unittest.mock import MagicMock, patch
from gitea.client import GiteaClient
def test_gitea_client_list_repo_issues() -> None:
client: GiteaClient = GiteaClient()
with patch("httpx.Client.get") as mock_get:
mock_response: MagicMock = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = []
mock_get.return_value = mock_response
# Test default parameter ("open")
client.list_repo_issues("owner", "repo")
mock_get.assert_called_once()
args, _ = mock_get.call_args
assert "type=issues" in args[0]
assert "state=open" in args[0]
mock_get.reset_mock()
# Test custom parameter ("closed")
client.list_repo_issues("owner", "repo", state="closed")
mock_get.assert_called_once()
args, _ = mock_get.call_args
assert "type=issues" in args[0]
assert "state=closed" in args[0]
def test_gitea_client_list_repo_pull_requests() -> None:
client: GiteaClient = GiteaClient()
with patch("httpx.Client.get") as mock_get:
mock_response: MagicMock = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = []
mock_get.return_value = mock_response
# Test default parameter ("open")
client.list_repo_pull_requests("owner", "repo")
mock_get.assert_called_once()
args, _ = mock_get.call_args
assert "state=open" in args[0]
mock_get.reset_mock()
# Test custom parameter ("closed")
client.list_repo_pull_requests("owner", "repo", state="closed")
mock_get.assert_called_once()
args, _ = mock_get.call_args
assert "state=closed" in args[0]
def test_gitea_client_list_assigned_issues() -> None:
client: GiteaClient = GiteaClient()
user_mock: MagicMock = MagicMock()
user_mock.login = "testuser"
with patch.object(client, "get_authenticated_user", return_value=user_mock), \
patch("httpx.get") as mock_get:
mock_response: MagicMock = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = []
mock_get.return_value = mock_response
client.list_assigned_issues("owner", "repo")
mock_get.assert_called_once()
args, _ = mock_get.call_args
assert "type=issues" in args[0]
assert "state=open" in args[0]
def test_gitea_client_list_assigned_pull_requests() -> None:
client: GiteaClient = GiteaClient()
user_mock: MagicMock = MagicMock()
user_mock.login = "testuser"
with patch.object(client, "get_authenticated_user", return_value=user_mock), \
patch("httpx.get") as mock_get:
mock_response: MagicMock = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = [
{"number": 1, "title": "PR 1", "assignee": {"login": "testuser"}, "user": {"login": "otheruser"}},
{"number": 2, "title": "PR 2", "assignee": None, "user": {"login": "testuser"}},
{"number": 3, "title": "PR 3", "assignee": {"login": "otheruser"}, "user": {"login": "otheruser"}}
]
mock_get.return_value = mock_response
res = client.list_assigned_pull_requests("owner", "repo")
mock_get.assert_called_once()
assert len(res) == 2
numbers = [pr.number for pr in res]
assert 1 in numbers
assert 2 in numbers
assert 3 not in numbers
+147
View File
@@ -0,0 +1,147 @@
import os
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from gitea.tools.coding_tools import CodingTools
def test_list_files(tmp_path: Path) -> None:
d: Path = tmp_path / "sub"
d.mkdir()
f: Path = d / "hello.txt"
f.write_text("content")
res: str = CodingTools().list_files(str(d))
assert "hello.txt" in res
def test_list_files_error() -> None:
res: str = CodingTools().list_files("/nonexistent/directory/path/here")
assert "Error listing files" in res
def test_read_file(tmp_path: Path) -> None:
f: Path = tmp_path / "test.txt"
f.write_text("line1\nline2\nline3\n")
res: str = CodingTools().read_file(str(f), offset=1, limit=2)
assert "1: line1" in res
assert "2: line2" in res
assert "3: line3" not in res
def test_read_file_empty_or_out_of_bounds(tmp_path: Path) -> None:
f: Path = tmp_path / "test.txt"
f.write_text("")
res: str = CodingTools().read_file(str(f), offset=10, limit=2)
assert res == "File is empty or offset out of bounds."
def test_read_file_error() -> None:
res: str = CodingTools().read_file("/nonexistent/file/path/here")
assert "Error reading file" in res
def test_write_file(tmp_path: Path) -> None:
f: Path = tmp_path / "new_dir" / "test.txt"
res: str = CodingTools().write_file(str(f), "content")
assert "written successfully" in res
assert f.read_text() == "content"
def test_write_file_error() -> None:
res: str = CodingTools().write_file("", "content")
assert "Error writing file" in res
def test_edit_file(tmp_path: Path) -> None:
f: Path = tmp_path / "test.txt"
f.write_text("hello world")
res: str = CodingTools().edit_file(str(f), "world", "there")
assert "edited successfully" in res
assert f.read_text() == "hello there"
def test_edit_file_not_found(tmp_path: Path) -> None:
f: Path = tmp_path / "test.txt"
f.write_text("hello world")
res: str = CodingTools().edit_file(str(f), "nonexistent", "there")
assert "not found" in res
def test_edit_file_error() -> None:
res: str = CodingTools().edit_file("/nonexistent/file/path/here", "world", "there")
assert "Error editing file" in res
@patch("subprocess.Popen")
def test_run_command_success(mock_popen: MagicMock) -> None:
mock_process: MagicMock = MagicMock()
mock_process.communicate.return_value = ("output_stdout", "output_stderr")
mock_process.returncode = 0
mock_popen.return_value = mock_process
res: str = CodingTools().run_command("echo hello")
assert "output_stdout" in res
assert "output_stderr" in res
@patch("subprocess.Popen")
def test_run_command_failure(mock_popen: MagicMock) -> None:
mock_process: MagicMock = MagicMock()
mock_process.communicate.return_value = ("output_stdout", "output_stderr")
mock_process.returncode = 1
mock_popen.return_value = mock_process
res: str = CodingTools().run_command("false")
assert "Command failed with exit code 1" in res
def test_run_command_error() -> None:
with patch("subprocess.Popen", side_effect=ValueError("Invalid run")):
res: str = CodingTools().run_command("echo")
assert "Error running command" in res
@patch("subprocess.Popen")
def test_run_command_timeout(mock_popen: MagicMock) -> None:
mock_process: MagicMock = MagicMock()
mock_process.communicate.side_effect = [
subprocess.TimeoutExpired(cmd="test", timeout=1),
("stdout_after_kill", "stderr_after_kill")
]
mock_popen.return_value = mock_process
res: str = CodingTools().run_command("hang_cmd", timeout=1)
assert "Command timed out after 1 seconds" in res
assert "stdout_after_kill" in res
mock_process.kill.assert_called_once()
@patch("subprocess.Popen")
def test_grep_search_success(mock_popen: MagicMock) -> None:
mock_process: MagicMock = MagicMock()
mock_process.communicate.return_value = ("match_line", "")
mock_process.returncode = 0
mock_popen.return_value = mock_process
res: str = CodingTools().grep_search("pattern", "/path")
assert res == "match_line"
@patch("subprocess.Popen")
def test_grep_search_no_matches(mock_popen: MagicMock) -> None:
mock_process: MagicMock = MagicMock()
mock_process.communicate.return_value = ("", "")
mock_process.returncode = 1
mock_popen.return_value = mock_process
res: str = CodingTools().grep_search("pattern", "/path")
assert "No matches found" in res
def test_grep_search_error() -> None:
with patch("subprocess.Popen", side_effect=ValueError("Invalid run")):
res: str = CodingTools().grep_search("pattern")
assert "Error during grep search" in res
+250
View File
@@ -0,0 +1,250 @@
import pytest
from unittest.mock import MagicMock, AsyncMock, patch
from core.dispatcher import AgentDispatcher
from core.queue import WorkItem
from gitea.client import GiteaClient
from gitea.tools.gitea_tools import GiteaTools
from gitea.models import PullRequestModel, IssueModel, CommentModel
pytestmark = pytest.mark.anyio
async def test_dispatch_skips_issue_with_existing_pr() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
# Mock list_repo_pull_requests to return a PR that closes issue #42
pr = PullRequestModel(
number=101,
title="fix: resolve bug",
body="closes #42"
)
mock_client.list_repo_pull_requests.return_value = [pr]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
task_number=42,
task_info=IssueModel(number=42),
priority=0
)
results = await dispatcher.dispatch("meeks/repo1", [work_item])
assert len(results) == 1
assert "SKIP: A pull request (PR #101) addressing issue #42 already exists" in results[0]
mock_client.list_repo_pull_requests.assert_called_once_with("meeks", "repo1")
@patch("core.dispatcher.CodingAgent")
async def test_dispatch_processes_issue_without_pr(mock_agent_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
# Mock list_repo_pull_requests to return PRs that don't address issue #42
pr = PullRequestModel(
number=101,
title="feat: add something",
body="closes #99"
)
mock_client.list_repo_pull_requests.return_value = [pr]
mock_client.get_issue_comments.return_value = []
# Mock CodingAgent run_with_tools
mock_agent_instance = MagicMock()
mock_agent_instance.run_with_tools = AsyncMock(return_value="Issue resolved.")
mock_agent_class.return_value = mock_agent_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="issue",
task_number=42,
task_info=IssueModel(number=42, title="test", body="test desc"),
priority=0
)
results = await dispatcher.dispatch("meeks/repo1", [work_item])
assert len(results) == 1
assert results[0] == "Issue resolved."
@patch("core.dispatcher.CodingAgent")
async def test_build_pr_mission_injects_issue_context(mock_agent_class: MagicMock) -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
# Mock get_pull_request, get_pull_request_diff, etc.
pr = PullRequestModel(
number=101,
title="fix: resolve bug",
body="closes #42",
head={"ref": "branch1"},
base={"ref": "master"}
)
mock_client.get_pull_request.return_value = pr
mock_client.get_pull_request_diff.return_value = "diff context"
mock_client.get_pull_request_files.return_value = []
mock_client.get_pull_request_comments.return_value = []
# Mock the connected issue and its comments
issue = IssueModel(number=42, title="bug description")
mock_client.get_issue.return_value = issue
comment = CommentModel(id=1, body="First comment")
mock_client.get_issue_comments.return_value = [comment]
# Mock CodingAgent
mock_agent_instance = MagicMock()
mock_agent_instance.run_with_tools = AsyncMock(return_value="PR Reviewed.")
mock_agent_class.return_value = mock_agent_instance
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="pr",
task_number=101,
task_info=PullRequestModel(number=101, title="fix: resolve bug", body="closes #42"),
priority=0
)
# We will patch the dispatcher._build_pr_mission output validation
mission = dispatcher._build_pr_mission(work_item)
assert "CONNECTED ISSUE CONTEXT" in mission
assert "Connected Issue #42" in mission
assert "bug description" in mission
assert "First comment" in mission
mock_client.get_issue.assert_called_once_with("meeks", "repo1", 42)
mock_client.get_issue_comments.assert_called_once_with("meeks", "repo1", 42)
async def test_find_pr_for_issue_by_branch() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
# 1. Matches fix/issue-42-some-desc
pr1 = PullRequestModel(number=102, head={"ref": "fix/issue-42-some-desc"})
mock_client.list_repo_pull_requests.return_value = [pr1]
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
# 2. Matches fix/42
pr2 = PullRequestModel(number=102, head={"ref": "fix/42"})
mock_client.list_repo_pull_requests.return_value = [pr2]
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
# 3. Matches fix-42_desc
pr3 = PullRequestModel(number=102, head={"ref": "fix-42_desc"})
mock_client.list_repo_pull_requests.return_value = [pr3]
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is not None
# 4. Does NOT match fix/142
pr4 = PullRequestModel(number=102, head={"ref": "fix/142"})
mock_client.list_repo_pull_requests.return_value = [pr4]
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
# 5. Does NOT match fix/421
pr5 = PullRequestModel(number=102, head={"ref": "fix/421"})
mock_client.list_repo_pull_requests.return_value = [pr5]
assert dispatcher._find_pr_for_issue("meeks/repo1", 42) is None
async def test_find_pr_for_issue_by_raw_mention() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
# PR body mentions #42
pr = PullRequestModel(
number=103,
title="some fix",
body="This is for #42 to fix the bug",
head={"ref": "some-branch"}
)
mock_client.list_repo_pull_requests.return_value = [pr]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
res = dispatcher._find_pr_for_issue("meeks/repo1", 42)
assert res is not None
assert res.number == 103
async def test_dispatch_skips_already_reviewed_pr() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_tools: MagicMock = MagicMock(spec=GiteaTools)
pr = PullRequestModel(
number=104,
title="already reviewed PR",
body="closes #42"
)
mock_client.get_pull_request.return_value = pr
mock_client.get_pull_request_diff.return_value = "diff"
mock_client.get_pull_request_comments.return_value = [
CommentModel(id=1, body="Reviewed by AI Agent: Looks good.")
]
dispatcher = AgentDispatcher(client=mock_client, tools=mock_tools)
work_item = WorkItem(
repo_full_name="meeks/repo1",
task_type="pr",
task_number=104,
task_info=PullRequestModel(number=104, title="already reviewed PR", body="closes #42"),
priority=0
)
results = await dispatcher.dispatch("meeks/repo1", [work_item])
assert len(results) == 1
assert "SKIP: PR #104 has already been addressed by AI" in results[0]
# ── _is_awaiting_reply tests ─────────────────────────────────────────────────
def _make_comment(login: str, body: str) -> CommentModel:
from gitea.models import UserModel
user = UserModel(login=login)
return CommentModel(id=1, body=body, user=user)
def test_is_awaiting_reply_no_comments() -> None:
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
assert dispatcher._is_awaiting_reply([]) is False
def test_is_awaiting_reply_no_question() -> None:
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
comments = [_make_comment("meeks-ai", "I will fix this now.")]
assert dispatcher._is_awaiting_reply(comments) is False
def test_is_awaiting_reply_agent_question_no_human_reply() -> None:
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
body = "Should I use approach A or B?\n<!-- agent:awaiting-reply -->"
comments = [_make_comment("meeks-ai", body)]
assert dispatcher._is_awaiting_reply(comments) is True
def test_is_awaiting_reply_agent_question_human_replied() -> None:
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
body = "Should I use approach A or B?\n<!-- agent:awaiting-reply -->"
comments = [
_make_comment("meeks-ai", body),
_make_comment("michael", "Use approach A please."),
]
assert dispatcher._is_awaiting_reply(comments) is False
def test_is_awaiting_reply_no_marker_not_detected() -> None:
"""Agent asked a question but forgot the marker — should NOT be skipped."""
dispatcher = AgentDispatcher(client=MagicMock(), tools=MagicMock())
comments = [_make_comment("meeks-ai", "Should I use approach A or B?")]
assert dispatcher._is_awaiting_reply(comments) is False
+97
View File
@@ -0,0 +1,97 @@
from unittest.mock import MagicMock
from gitea.client import GiteaClient
from gitea.tools.file_tools import FileTools
def test_get_file_content_string_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_file_content.return_value = "file content here"
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
assert res == "file content here"
mock_client.get_file_content.assert_called_once_with("owner", "repo", "path/to/file")
def test_get_file_content_list_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_file_content.return_value = ["line1", "line2"]
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
assert res == "line1\nline2"
def test_get_file_content_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_file_content.side_effect = Exception("API Error")
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.get_file_content("owner", "repo", "path/to/file")
assert "Error getting file content: API Error" in res
def test_get_file_content_with_ref_string_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_file_content.return_value = "file content here"
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.get_file_content_with_ref("owner", "repo", "path/to/file", "main")
assert res == "file content here"
mock_client.get_file_content.assert_called_once_with("owner", "repo", "path/to/file", "main")
def test_get_file_content_with_ref_list_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_file_content.return_value = ["line1", "line2"]
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.get_file_content_with_ref("owner", "repo", "path/to/file", "main")
assert res == "line1\nline2"
def test_get_file_content_with_ref_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_file_content.side_effect = Exception("API Error")
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.get_file_content_with_ref("owner", "repo", "path/to/file", "main")
assert "Error getting file content: API Error" in res
def test_commit_file_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.update_file.return_value = {}
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.commit_file("owner", "repo", "path/to/file", "msg", "content", "branch")
assert "committed successfully" in res
mock_client.update_file.assert_called_once_with("owner", "repo", "path/to/file", "msg", "content", "branch")
def test_commit_file_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.update_file.side_effect = Exception("API Error")
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.commit_file("owner", "repo", "path/to/file", "msg", "content", "branch")
assert "Error committing file: API Error" in res
def test_update_file_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.update_file.return_value = {}
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.update_file("owner", "repo", "path/to/file", "msg", "content", "branch")
assert "updated in" in res
mock_client.update_file.assert_called_once_with("owner", "repo", "path/to/file", "msg", "content", "branch")
def test_update_file_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.update_file.side_effect = Exception("API Error")
file_tools: FileTools = FileTools(mock_client)
res: str = file_tools.update_file("owner", "repo", "path/to/file", "msg", "content", "branch")
assert "Error updating file: API Error" in res
+22
View File
@@ -0,0 +1,22 @@
from unittest.mock import MagicMock
from gitea.client import GiteaClient
from gitea.tools.git_tools import GitTools
def test_create_branch_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.create_ref.return_value = {}
git_tools: GitTools = GitTools(mock_client)
res: str = git_tools.create_branch("owner", "repo", "ref", "sha")
assert res == "Branch 'ref' created successfully in owner/repo."
mock_client.create_ref.assert_called_once_with("owner", "repo", "ref", "sha")
def test_create_branch_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.create_ref.side_effect = Exception("API Error")
git_tools: GitTools = GitTools(mock_client)
res: str = git_tools.create_branch("owner", "repo", "ref", "sha")
assert res == "Error creating branch: API Error"
+113
View File
@@ -0,0 +1,113 @@
from unittest.mock import MagicMock
from gitea.client import GiteaClient
from gitea.tools.gitea_tools import GiteaTools
def test_gitea_tools_delegation() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
gitea_tools: GiteaTools = GiteaTools(mock_client)
# 1. get_issue
gitea_tools.get_issue("owner", "repo", 1)
mock_client.get_issue.assert_called_once_with("owner", "repo", 1)
# 2. get_pull_request
gitea_tools.get_pull_request("owner", "repo", 2)
mock_client.get_pull_request.assert_called_once_with("owner", "repo", 2)
# 3. close_issue
gitea_tools.close_issue("owner", "repo", 3)
mock_client.close_issue.assert_called_once_with("owner", "repo", 3)
# 4. close_pull_request
gitea_tools.close_pull_request("owner", "repo", 4)
mock_client.close_pull_request.assert_called_once_with("owner", "repo", 4)
# 5. get_issue_comments
gitea_tools.get_issue_comments("owner", "repo", 5)
mock_client.get_issue_comments.assert_called_once_with("owner", "repo", 5)
# 6. get_pull_request_comments
gitea_tools.get_pull_request_comments("owner", "repo", 6)
mock_client.get_pull_request_comments.assert_called_once_with("owner", "repo", 6)
# 7. list_assigned_issues
mock_client.list_all_user_repos.return_value = []
gitea_tools.list_assigned_issues()
mock_client.list_all_user_repos.assert_called()
# 8. list_assigned_pull_requests
gitea_tools.list_assigned_pull_requests()
mock_client.list_all_user_repos.assert_called()
# 9. list_issues
gitea_tools.list_issues("owner", "repo")
mock_client.list_repo_issues.assert_called_once_with("owner", "repo", "open")
# 10. list_pull_requests
gitea_tools.list_pull_requests("owner", "repo")
mock_client.list_repo_pull_requests.assert_called_once_with("owner", "repo", "open")
# 11. get_file_content
gitea_tools.get_file_content("owner", "repo", "path")
mock_client.get_file_content.assert_called_with("owner", "repo", "path")
# 12. create_pull_request
gitea_tools.create_pull_request("owner", "repo", "head", "base", "title", "desc")
mock_client.create_pr_via_tea.assert_called_once_with("owner", "repo", "title", "desc", "head", "base")
# 13. create_issue
gitea_tools.create_issue("owner", "repo", "title", "body")
mock_client.create_issue.assert_called_once_with("owner", "repo", "title", "body", None, None)
# 14. create_branch
gitea_tools.create_branch("owner", "repo", "branch", "sha")
mock_client.create_ref.assert_called_once_with("owner", "repo", "branch", "sha")
# 15. commit_file
gitea_tools.commit_file("owner", "repo", "path", "msg", "content", "branch")
mock_client.update_file.assert_called_with("owner", "repo", "path", "msg", "content", "branch")
# 16. add_label_to_issue
gitea_tools.add_label_to_issue("owner", "repo", 1, "bug")
mock_client.add_label.assert_called_with("owner", "repo", 1, "bug")
# 17. add_label_to_pr
gitea_tools.add_label_to_pr("owner", "repo", 1, "bug")
mock_client.add_label_pr.assert_called_once_with("owner", "repo", 1, "bug")
# 18. add_comment_to_issue
gitea_tools.add_comment_to_issue("owner", "repo", 1, "body")
mock_client.add_comment.assert_called_with("owner", "repo", 1, "body")
# 19. get_pull_request_diff
gitea_tools.get_pull_request_diff("owner", "repo", 1)
mock_client.get_pull_request_diff.assert_called_once_with("owner", "repo", 1)
# 20. get_pull_request_patch
gitea_tools.get_pull_request_patch("owner", "repo", 1)
mock_client.get_pull_request_patch.assert_called_once_with("owner", "repo", 1)
# 21. approve_pull_request
gitea_tools.approve_pull_request("owner", "repo", 1, "good")
mock_client.approve_pr.assert_called_once_with("owner", "repo", 1, "good")
# 22. request_changes
gitea_tools.request_changes("owner", "repo", 1, "bad")
mock_client.request_changes_pr.assert_called_once_with("owner", "repo", 1, "bad")
# 23. add_comment
gitea_tools.add_comment("owner", "repo", 1, "body")
mock_client.add_comment.assert_called_with("owner", "repo", 1, "body")
# 24. add_label
gitea_tools.add_label("owner", "repo", 1, "bug")
mock_client.add_label.assert_called_with("owner", "repo", 1, "bug")
# 25. get_file_content_with_ref
gitea_tools.get_file_content_with_ref("owner", "repo", "path", "ref")
mock_client.get_file_content.assert_called_with("owner", "repo", "path", "ref")
# 26. update_file
gitea_tools.update_file("owner", "repo", "path", "msg", "content", "branch")
mock_client.update_file.assert_called_with("owner", "repo", "path", "msg", "content", "branch")
+213
View File
@@ -0,0 +1,213 @@
import json
from typing import Any
from unittest.mock import MagicMock
from gitea.client import GiteaClient
from gitea.models import IssueModel, CommentModel, LabelModel, RepositoryModel
from gitea.tools.issue_tools import IssueTools
def test_get_issue_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
issue: IssueModel = IssueModel(number=1, title="Test Issue", state="open")
mock_client.get_issue.return_value = issue
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.get_issue("owner", "repo", 1)
data: dict[str, Any] = json.loads(res)
assert data["number"] == 1
assert data["title"] == "Test Issue"
mock_client.get_issue.assert_called_once_with("owner", "repo", 1)
def test_get_issue_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_issue.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.get_issue("owner", "repo", 1)
assert "Error getting issue: API Error" in res
def test_close_issue_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.close_issue.return_value = IssueModel(number=1, state="closed")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.close_issue("owner", "repo", 1)
assert res == "Issue #1 closed successfully."
mock_client.close_issue.assert_called_once_with("owner", "repo", 1)
def test_close_issue_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.close_issue.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.close_issue("owner", "repo", 1)
assert "Error closing issue: API Error" in res
def test_get_issue_comments_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
comment: CommentModel = CommentModel(id=123, body="Comment body")
mock_client.get_issue_comments.return_value = [comment]
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.get_issue_comments("owner", "repo", 1)
data: list[dict[str, Any]] = json.loads(res)
assert len(data) == 1
assert data[0]["body"] == "Comment body"
def test_get_issue_comments_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_issue_comments.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.get_issue_comments("owner", "repo", 1)
assert "Error getting issue comments: API Error" in res
def test_list_assigned_issues_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
repo: RepositoryModel = RepositoryModel(name="repo1", owner="owner1")
issue: IssueModel = IssueModel(number=1, title="Test Issue")
mock_client.list_all_user_repos.return_value = [repo]
mock_client.list_assigned_issues.return_value = [issue]
issue_tools: IssueTools = IssueTools(mock_client)
res: list[dict[str, Any]] = issue_tools.list_assigned_issues()
assert len(res) == 1
assert res[0]["number"] == 1
mock_client.list_all_user_repos.assert_called_once()
mock_client.list_assigned_issues.assert_called_once_with("owner1", "repo1")
def test_list_assigned_issues_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.list_all_user_repos.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: list[dict[str, Any]] = issue_tools.list_assigned_issues()
assert res == []
def test_list_issues_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
issue: IssueModel = IssueModel(number=1, title="Test Issue")
mock_client.list_repo_issues.return_value = [issue]
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.list_issues("owner", "repo")
assert "#1: Test Issue" in res
def test_list_issues_empty() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.list_repo_issues.return_value = []
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.list_issues("owner", "repo")
assert res == "No issues in owner/repo."
def test_list_issues_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.list_repo_issues.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.list_issues("owner", "repo")
assert "Error listing issues: API Error" in res
def test_create_issue_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
issue: IssueModel = IssueModel(number=2)
mock_client.create_issue.return_value = issue
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.create_issue("owner", "repo", "Title", "Body", ["label1"], ["assignee1"])
assert res == "Issue #2 created successfully in owner/repo."
mock_client.create_issue.assert_called_once_with("owner", "repo", "Title", "Body", ["label1"], ["assignee1"])
def test_create_issue_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.create_issue.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.create_issue("owner", "repo", "Title", "Body")
assert "Error creating issue: API Error" in res
def test_add_label_to_issue_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_label.return_value = LabelModel(name="bug")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_label_to_issue("owner", "repo", 1, "bug")
assert res == "Label 'bug' added to issue #1."
def test_add_label_to_issue_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_label.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_label_to_issue("owner", "repo", 1, "bug")
assert "Error adding label to issue #1: API Error" in res
def test_add_comment_to_issue_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_comment.return_value = CommentModel(id=1)
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_comment_to_issue("owner", "repo", 1, "body")
assert res == "Comment added to issue #1."
def test_add_comment_to_issue_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_comment.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_comment_to_issue("owner", "repo", 1, "body")
assert "Error adding comment to issue #1: API Error" in res
def test_add_comment_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_comment.return_value = CommentModel(id=1)
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_comment("owner", "repo", 1, "body")
assert res == "Comment added to #1."
def test_add_comment_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_comment.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_comment("owner", "repo", 1, "body")
assert "Error adding comment: API Error" in res
def test_add_label_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_label.return_value = LabelModel(name="bug")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_label("owner", "repo", 1, "bug")
assert res == "Label 'bug' added to #1."
def test_add_label_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_label.side_effect = Exception("API Error")
issue_tools: IssueTools = IssueTools(mock_client)
res: str = issue_tools.add_label("owner", "repo", 1, "bug")
assert "Error adding label: API Error" in res
+232
View File
@@ -0,0 +1,232 @@
import json
from typing import Any
from unittest.mock import MagicMock
from gitea.client import GiteaClient
from gitea.models import PullRequestModel, CommentModel, RepositoryModel
from gitea.tools.pr_tools import PRTools
def test_get_pull_request_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR", state="open")
mock_client.get_pull_request.return_value = pr
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request("owner", "repo", 1)
data: dict[str, Any] = json.loads(res)
assert data["number"] == 1
assert data["title"] == "Test PR"
mock_client.get_pull_request.assert_called_once_with("owner", "repo", 1)
def test_get_pull_request_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_pull_request.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request("owner", "repo", 1)
assert "Error getting pull request: API Error" in res
def test_close_pull_request_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.close_pull_request.return_value = PullRequestModel(number=1, state="closed")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.close_pull_request("owner", "repo", 1)
assert res == "Pull request #1 closed successfully."
mock_client.close_pull_request.assert_called_once_with("owner", "repo", 1)
def test_close_pull_request_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.close_pull_request.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.close_pull_request("owner", "repo", 1)
assert "Error closing pull request: API Error" in res
def test_get_pull_request_comments_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
comment: CommentModel = CommentModel(id=123, body="Comment body")
mock_client.get_pull_request_comments.return_value = [comment]
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request_comments("owner", "repo", 1)
data: list[dict[str, Any]] = json.loads(res)
assert len(data) == 1
assert data[0]["body"] == "Comment body"
def test_get_pull_request_comments_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_pull_request_comments.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request_comments("owner", "repo", 1)
assert "Error getting PR comments: API Error" in res
def test_list_assigned_pull_requests_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
repo: RepositoryModel = RepositoryModel(name="repo1", owner="owner1")
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR")
mock_client.list_all_user_repos.return_value = [repo]
mock_client.list_assigned_pull_requests.return_value = [pr]
pr_tools: PRTools = PRTools(mock_client)
res: list[dict[str, Any]] = pr_tools.list_assigned_pull_requests()
assert len(res) == 1
assert res[0]["number"] == 1
mock_client.list_all_user_repos.assert_called_once()
mock_client.list_assigned_pull_requests.assert_called_once_with("owner1", "repo1")
def test_list_assigned_pull_requests_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.list_all_user_repos.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: list[dict[str, Any]] = pr_tools.list_assigned_pull_requests()
assert res == []
def test_list_pull_requests_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
pr: PullRequestModel = PullRequestModel(number=1, title="Test PR")
mock_client.list_repo_pull_requests.return_value = [pr]
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.list_pull_requests("owner", "repo")
assert "#1: Test PR" in res
def test_list_pull_requests_empty() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.list_repo_pull_requests.return_value = []
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.list_pull_requests("owner", "repo")
assert res == "No PRs in owner/repo."
def test_list_pull_requests_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.list_repo_pull_requests.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.list_pull_requests("owner", "repo")
assert "Error listing PRs: API Error" in res
def test_create_pull_request_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
pr: PullRequestModel = PullRequestModel(number=2, title="Title")
mock_client.create_pr_via_tea.return_value = pr
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.create_pull_request("owner", "repo", "head", "base", "Title", "Desc")
data: dict[str, Any] = json.loads(res)
assert data["number"] == 2
mock_client.create_pr_via_tea.assert_called_once_with("owner", "repo", "Title", "Desc", "head", "base")
def test_create_pull_request_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.create_pr_via_tea.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.create_pull_request("owner", "repo", "head", "base", "Title")
assert "Error creating PR: API Error" in res
def test_add_label_to_pr_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_label_pr.return_value = {}
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.add_label_to_pr("owner", "repo", 1, "bug")
assert res == "Label 'bug' added to PR #1."
def test_add_label_to_pr_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.add_label_pr.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.add_label_to_pr("owner", "repo", 1, "bug")
assert "Error adding label to PR #1: API Error" in res
def test_get_pull_request_diff_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_pull_request_diff.return_value = "diff content"
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request_diff("owner", "repo", 1)
assert res == "diff content"
def test_get_pull_request_diff_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_pull_request_diff.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request_diff("owner", "repo", 1)
assert "Error getting PR diff: API Error" in res
def test_get_pull_request_patch_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_pull_request_patch.return_value = "patch content"
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request_patch("owner", "repo", 1)
assert res == "patch content"
def test_get_pull_request_patch_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.get_pull_request_patch.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.get_pull_request_patch("owner", "repo", 1)
assert "Error getting PR patch: API Error" in res
def test_approve_pull_request_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.approve_pr.return_value = {}
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.approve_pull_request("owner", "repo", 1, "good")
assert res == "Approved PR #1."
def test_approve_pull_request_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.approve_pr.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.approve_pull_request("owner", "repo", 1, "good")
assert "Error approving PR: API Error" in res
def test_request_changes_success() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.request_changes_pr.return_value = {}
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.request_changes("owner", "repo", 1, "bad")
assert res == "Requested changes on PR #1."
def test_request_changes_failure() -> None:
mock_client: MagicMock = MagicMock(spec=GiteaClient)
mock_client.request_changes_pr.side_effect = Exception("API Error")
pr_tools: PRTools = PRTools(mock_client)
res: str = pr_tools.request_changes("owner", "repo", 1, "bad")
assert "Error requesting changes: API Error" in res
Generated
+700
View File
@@ -0,0 +1,700 @@
version = 1
revision = 3
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.13'",
"python_full_version < '3.13'",
]
[[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 = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ 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 = "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 = "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 = "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 = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "gitea-agent"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "httpx" },
{ name = "lmstudio" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "python-dotenv" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "ty" },
]
[package.metadata]
requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "lmstudio", specifier = ">=1.5.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pydantic", specifier = ">=2.13.4" },
{ name = "pydantic-settings", specifier = ">=2.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.1.1" },
{ name = "ty", specifier = ">=0.0.55" },
]
[[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 = "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 = "httpx-ws"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpcore" },
{ name = "httpx" },
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/cd/ca91a07ae446451f7476bf3fcc909e98cb942ff032ebfda0e3fe449aca7b/httpx_ws-0.9.0.tar.gz", hash = "sha256:797373326f70eec1ae96f6e43ae9f12002fd7d73aee139a4985eaab964338a08", size = 107105, upload-time = "2026-03-28T14:11:10.781Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/f8/a6bc80313a9e93c888fa10534dfce2ad76ff86911b6f485777ce6de6a073/httpx_ws-0.9.0-py3-none-any.whl", hash = "sha256:71640d2fb1bf9a225775015b33cd755cfd4c5f7e21c885192fe3adc4c387b248", size = 15759, upload-time = "2026-03-28T14:11:11.887Z" },
]
[[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 = "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/1d/da/76a2c7e510ba15fe323d9509c223ab272da79ea59f54488f4a78da6426db/jiter-0.15.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:edebcf7d1f601199084bb6e844d7dc67e03e04f6ac786b0332d616635c4ff7a4", size = 310849, upload-time = "2026-05-19T10:06:51.944Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8e/827be942883a4dc0862c48626ff41af3320b1902d136a0bf4b9041f2c567/jiter-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f924585cdacf631cd382b657966847bb537bf9ed0a6f9b991da5f05a631480f", size = 314991, upload-time = "2026-05-19T10:06:53.522Z" },
{ url = "https://files.pythonhosted.org/packages/6d/38/be2832be361ba1b9517c76f46d30b64e985be1dd43c974f4c3a4b1844436/jiter-0.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abbf258599526ad0326fe51e252e24f2bd6f24f1852681b4b78feda3808f1d18", size = 340843, upload-time = "2026-05-19T10:06:55.071Z" },
{ url = "https://files.pythonhosted.org/packages/6d/d8/90f01fb83c0c7ba509303ec93e32a308fbfa167d264860b01c0fd0dbbd06/jiter-0.15.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c468136b8bd6bb18c8786e4236a1fa27362f24cb23450ba0cb204ab379b8e6f", size = 365116, upload-time = "2026-05-19T10:06:56.893Z" },
{ url = "https://files.pythonhosted.org/packages/91/38/94593d34f8c67a0b6f6cbc027f016ffa9780b3a858a7a86f6fd7a15bcc1e/jiter-0.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05906b93d72f03339e6bb7cf8dc10ebda64a0266126eed6beba79e20abcf5fd4", size = 457970, upload-time = "2026-05-19T10:06:58.707Z" },
{ url = "https://files.pythonhosted.org/packages/df/04/d79962dd49d00c97e2a9b4cacea1947904d02135936960351f9a96d4c1a6/jiter-0.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30ce785d2adb8e32c3f7741442370a74834ec4c01f3c48f0750227a0b4ef27d6", size = 375744, upload-time = "2026-05-19T10:07:00.471Z" },
{ url = "https://files.pythonhosted.org/packages/c3/2e/5d37abe2be0e819c21e2338bebd410e481763ce526a9138c8c3652fa0123/jiter-0.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd73e3da91a0a722d67165e849ce2cdc10de0e0d48738c142be8c6c5f310f4c", size = 349609, upload-time = "2026-05-19T10:07:01.829Z" },
{ url = "https://files.pythonhosted.org/packages/7a/90/98768ad2ed90c1fda15d64157de2dfbf73c1c074d4b1bfaca915480bc7cf/jiter-0.15.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:ceb8fc27d38793f9c97149be8302720c5b22e5c195a37bf2c45dc36c4600a512", size = 354366, upload-time = "2026-05-19T10:07:03.587Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c4/fbfb806209f1fe4b7dccdfb07bc62bb044300734a945b06fd64db446ef6a/jiter-0.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d726e3ceeb337191324b49de298142f27c3ad10886341555d1d5315b5f252c6a", size = 393519, upload-time = "2026-05-19T10:07:05.08Z" },
{ url = "https://files.pythonhosted.org/packages/37/1c/b9c257cd70cb453b6d10f3ebf0402cdb11669ab455389096f09839670290/jiter-0.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2c8aea7781d2a372227871de4e1a1332aa96f5a89fd76c5e835dafdbad102887", size = 519952, upload-time = "2026-05-19T10:07:06.589Z" },
{ url = "https://files.pythonhosted.org/packages/a9/1a/aa85027db7ab15829c12feebbc33b404f53fc399bd559d85fd0d6365ff0d/jiter-0.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf4bd113a69c0a740e27cb962ce10630c36d2b8f59d759a651b955ee9d18a823", size = 550770, upload-time = "2026-05-19T10:07:08.228Z" },
{ url = "https://files.pythonhosted.org/packages/d4/54/8c3f65c8a5687925e84708f19d63f7f37d28e2b86a48d951702ad94424d8/jiter-0.15.0-cp310-cp310-win32.whl", hash = "sha256:d92a5cd21fdb083931d546c207aa29633787c5dc5b02daab2d32b843f88a2c53", size = 209303, upload-time = "2026-05-19T10:07:10.006Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/0528a1eb9f42dd2d8228a0711458628f35924d131f623eaebc35fd23d3d4/jiter-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:e58585a58209d72691ce2d62a9147445f5a87beb0bde97fde284c96ae392a3d1", size = 200404, upload-time = "2026-05-19T10:07:11.426Z" },
{ 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 = "lmstudio"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "httpx" },
{ name = "httpx-ws" },
{ name = "msgspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/6d/40876f14c759fa2072ccbc844cc3ef7c74c1824e17e7d19b2cf4ff2cea2c/lmstudio-1.5.0.tar.gz", hash = "sha256:458c34fe1f94a7dcc521d4226b4cee82b8af7ea3da8c40b31bbdac558d9a74d4", size = 200230, upload-time = "2025-08-22T13:52:42.487Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/51/dcf7a86872a9de6529d67c68db0319b6f2afa093294d1fb8f1affcc86cc8/lmstudio-1.5.0-py3-none-any.whl", hash = "sha256:0b1e5a1cf013744a6ce0a80960204af5bf468dbbb2a505acb4dfb67185fffbe9", size = 139490, upload-time = "2025-08-22T13:52:41.456Z" },
]
[[package]]
name = "msgspec"
version = "0.21.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/38/d591d9f66d43d897ecbd249f2833665823d19c8b043f16619bc8343e23df/msgspec-0.21.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72d9cd03241b8b2edb2e12dcc66c500fa480d8cbd71a8bac105809d468882064", size = 195172, upload-time = "2026-04-12T21:43:45.062Z" },
{ url = "https://files.pythonhosted.org/packages/69/1a/6899188b5982ec1324e0c629b7801eed2db987f6634fab58abd9fc82d317/msgspec-0.21.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed2ab278200e743a1d2610a4e0c8fc74f6cecb8548544cdec43f927bd9265238", size = 188316, upload-time = "2026-04-12T21:43:46.641Z" },
{ url = "https://files.pythonhosted.org/packages/9e/95/7e591b4fa11fdbbf9891164473c23420a8c781ef553295abe416bf335f42/msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce", size = 216565, upload-time = "2026-04-12T21:43:48.081Z" },
{ url = "https://files.pythonhosted.org/packages/19/86/714feeaf3b84cf2027235681725593840153dedd2868578f9f2715e296bb/msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027", size = 222689, upload-time = "2026-04-12T21:43:49.385Z" },
{ url = "https://files.pythonhosted.org/packages/7d/b9/4384243e814f2579e5205e17d170b9c1a30121afd1393298d904817a7fa7/msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06", size = 222343, upload-time = "2026-04-12T21:43:50.612Z" },
{ url = "https://files.pythonhosted.org/packages/04/01/4b227d9c4057346271043632bad41979cf8c3dca372e41bb1f7d546395b2/msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae", size = 225607, upload-time = "2026-04-12T21:43:52.129Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ce/27021d1c3e5da837743092a7b7a5e8818397e1f4c05ee8b068bd7d1fd78a/msgspec-0.21.1-cp310-cp310-win_amd64.whl", hash = "sha256:f041a2279f31e3a53319005e4d60ba77c085cfcbe394cdc7ce803c2d01fe9449", size = 188392, upload-time = "2026-04-12T21:43:53.384Z" },
{ url = "https://files.pythonhosted.org/packages/80/2b/daf7a8d6d7cf00e0dcd0439178b284ade701234abdcadf3385601da04fbd/msgspec-0.21.1-cp310-cp310-win_arm64.whl", hash = "sha256:1bf17cbd7b28a5dffc7e764c654eed8ccde5e0f1de7970628608304640d4ce4e", size = 174191, upload-time = "2026-04-12T21:43:54.6Z" },
{ url = "https://files.pythonhosted.org/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131, upload-time = "2026-04-12T21:43:56.094Z" },
{ url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" },
{ url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" },
{ url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" },
{ url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" },
{ url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" },
{ url = "https://files.pythonhosted.org/packages/09/af/8bf15736a6dd3cb4f90c5467f6dc39197d2daaf10754490cdc0aa17b7312/msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e", size = 188554, upload-time = "2026-04-12T21:44:04.151Z" },
{ url = "https://files.pythonhosted.org/packages/ef/29/cc7db3a165b62d16e64a83f82eccb79655055cb5bc1f60459a6f9d7c82f2/msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99", size = 174517, upload-time = "2026-04-12T21:44:05.66Z" },
{ url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" },
{ url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" },
{ url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" },
{ url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" },
{ url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" },
{ url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" },
{ url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" },
{ url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" },
{ url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" },
{ url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" },
{ url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" },
{ url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" },
{ url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" },
{ url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" },
{ url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" },
{ url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" },
{ url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" },
{ url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" },
{ url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" },
{ url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" },
{ url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" },
{ url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" },
{ url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" },
{ url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" },
{ url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" },
{ url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" },
{ url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" },
{ url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" },
{ url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" },
]
[[package]]
name = "openai"
version = "2.43.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/f3/fa/88d0c58a0c58df7e6758e66b99c5d028d5e0bb49f8812d7203940cd9dbf1/openai-2.43.0.tar.gz", hash = "sha256:e74d238200a26868977002190fb6631613480a93dfe0c9c982e77021ed60a017", size = 785369, upload-time = "2026-06-17T17:06:56.06Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/d2/ba767f4bbb30776c03d40906a2d3afad716a165ffa1771fc23b8992f7920/openai-2.43.0-py3-none-any.whl", hash = "sha256:65a670b54fadf2268c9e1330133373c963eb779ee969e5cbad419ec2c21dce97", size = 1355077, upload-time = "2026-06-17T17:06:53.614Z" },
]
[[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 = "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 = "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/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
{ 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 = "pydantic-settings"
version = "2.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
]
[[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 = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[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 = "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 = "tqdm"
version = "4.68.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" },
]
[[package]]
name = "ty"
version = "0.0.55"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/08/48/f687c8d268e3581f2f104d1f2ac5944d5b5e841b3695c613b3f263e5bbf7/ty-0.0.55.tar.gz", hash = "sha256:88ca87073825a79a8327c550efcc86cec94344890244c5946f84c9e44a969f31", size = 6040230, upload-time = "2026-06-27T00:27:29.385Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/a3/1a90ba7e5a61c6d09adb92346ddba97668095fc257b577af433e5ac4f404/ty-0.0.55-py3-none-linux_armv6l.whl", hash = "sha256:31e83eef512d066542fe990fe1a3b814423abd1616376c54e48af7045b3e1749", size = 11677249, upload-time = "2026-06-27T00:26:52.18Z" },
{ url = "https://files.pythonhosted.org/packages/82/3a/669f9aa478c38243e213a2684db1502086026cfadc15bb1b29b7cbde030d/ty-0.0.55-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab4bca857950608fea73e269e2da369d43e6467131de85160d68e2fa466fa248", size = 11444180, upload-time = "2026-06-27T00:26:54.576Z" },
{ url = "https://files.pythonhosted.org/packages/15/a4/6a4b2507a53ce6530c66c5b4fe0d58551eb1748ffa9e0696c32fdd55bbd4/ty-0.0.55-py3-none-macosx_11_0_arm64.whl", hash = "sha256:55032bfd31bf2c5355ee81bdc6407b144a1cc7ee41e5681dd1368e4cef2ba327", size = 10963134, upload-time = "2026-06-27T00:26:57.348Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ae/a3b1a0f1cc83b7d258662cb98aa80a720c2e671d0e8fa0d17a4d5d057a7a/ty-0.0.55-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1e049f69ce65b3c269af67624607f435e1c32319786c1e453ef9611502f295", size = 11493517, upload-time = "2026-06-27T00:26:59.26Z" },
{ url = "https://files.pythonhosted.org/packages/0d/9f/311ce39065a979ef40a9b847f685c8e02464e53adf1671e081eea90640ca/ty-0.0.55-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:631409975c681d5a280fc5a99b7b32e9e801f33be7567c6b42ec331362f59d7d", size = 11460590, upload-time = "2026-06-27T00:27:01.425Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8f/3bf29aa77bd78aae48275153135a2052fa7d3ccdf1ecabeb99c8773abd66/ty-0.0.55-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e08cb0436e68b9351555ae8f2697138c9009b4d5b4ae4272232988b2a431a98f", size = 12098430, upload-time = "2026-06-27T00:27:03.596Z" },
{ url = "https://files.pythonhosted.org/packages/bc/6e/e88411a88240b94640bba06fb6d0d92b247fbeef47ee2bc71f39e58c2558/ty-0.0.55-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16c215ad9f823829409b94ee188cfaa4563f6e1384f6ce3fecb1db75f6c7cf7c", size = 12673086, upload-time = "2026-06-27T00:27:05.589Z" },
{ url = "https://files.pythonhosted.org/packages/6c/7e/8f1762fb7f9245a68ba5ae338d73c59403ce57554e5d311b8bb55027b0ec/ty-0.0.55-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b510eb8f4032baf11b7aee2f1d53babc3b4ca03939b9cdcf6a9d15761d575188", size = 12242559, upload-time = "2026-06-27T00:27:07.714Z" },
{ url = "https://files.pythonhosted.org/packages/72/1f/143657daf2670d977dac83435f1fe03d4843efb798d8e1e75950e541aadd/ty-0.0.55-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ddc05e7959709c3b9b83aa627128a80446865e3c1a4882638dcff6d776dc34a", size = 12021409, upload-time = "2026-06-27T00:27:09.881Z" },
{ url = "https://files.pythonhosted.org/packages/6d/30/69487c439dd1fad3a4a3d96f0a472193de297eaba6fc4b8ea687ce434ac2/ty-0.0.55-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:636e8e5078787b8c6916c94e1406719f10189a4ca6b37b813a5922ce5857a8c7", size = 12303807, upload-time = "2026-06-27T00:27:11.986Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ca/cd88b6493dafc7db077f5e17c0438eb3af6e2d6d08f616dbb52a8ddfd567/ty-0.0.55-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ef7d6deaacb73fec603666b5471f1dc5a5699aa84e11a6d4d644dd07ca72121e", size = 11441263, upload-time = "2026-06-27T00:27:14.087Z" },
{ url = "https://files.pythonhosted.org/packages/aa/fe/66b6915671653ab739f71e4f1b0528e69da64429b7ebf3840c625b6e43f2/ty-0.0.55-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9aeea0fe5875d3cf37faf0e44d0fdf9669335467749741b8fc0103916fb5cd32", size = 11484584, upload-time = "2026-06-27T00:27:16.311Z" },
{ url = "https://files.pythonhosted.org/packages/4a/4f/7a9c0bbac8b899e9f6c0ec110c6612f52e4db35f6bb17ddc0ef60384fa3e/ty-0.0.55-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0b699c01310dbd2705a07c97c5f4aaeedef61bd9adeea2e7c46aed32401d3576", size = 11759309, upload-time = "2026-06-27T00:27:18.471Z" },
{ url = "https://files.pythonhosted.org/packages/ca/de/b6f8b1b69aa631b5716ef3f985c3b56de0e46c2499cc00d30c402b41f714/ty-0.0.55-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:32cbeba543e46de2a983ec6d525d8b56514f7422bd1e1b57c44ccf7bfa72c38a", size = 12128755, upload-time = "2026-06-27T00:27:20.55Z" },
{ url = "https://files.pythonhosted.org/packages/7d/90/a912531e51ee7e076b42972479290fa687c0f5e747b7e773f3033164acaa/ty-0.0.55-py3-none-win32.whl", hash = "sha256:52b968e24eb4f7a5c3bd251db1f99f60dd385890356d38fc619d84f1b423446a", size = 11117501, upload-time = "2026-06-27T00:27:22.714Z" },
{ url = "https://files.pythonhosted.org/packages/4c/7a/99d59843bf8908a7f9f4d13fda107dbad07b7faa28ecd7860eacf363fb1c/ty-0.0.55-py3-none-win_amd64.whl", hash = "sha256:bf39cbfdc0add44d94bd3fff1f53c351418d134b6a66b87efdb7876d7b7a2224", size = 12150106, upload-time = "2026-06-27T00:27:24.881Z" },
{ url = "https://files.pythonhosted.org/packages/b3/44/20987505cedf2a865b08482f0eabc181fd9599b062964057ec8a128a4296/ty-0.0.55-py3-none-win_arm64.whl", hash = "sha256:f7f3700a9a060e8f1af11e4fb63fafcaf272b041781f4ccdfda2b3b5c6c1e439", size = 11560157, upload-time = "2026-06-27T00:27:27.332Z" },
]
[[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 = "wsproto"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" },
]